mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
Feature/[PM-1378] - Trusted Device Encryption - Establish trust logic for all clients (#5339)
* PM1378 - (1) Create state service methods for securely storing a device symmetric key while following existing pattern of DuckDuckGoKey generation (2) Create makeDeviceKey method on crypto service which leverages the new state service methods for storing the device key. * PM-1378 - Document CSPRNG types w/ comments explaining what they are and when they should be used. * PM-1378 - TODO to add tests for makeDeviceKey method * PM-1378 - Create Devices API service for creating and updating device encrypted master keys + move models according to latest code standards ( I think) * PM-1378 - TODO clean up - DeviceResponse properly moved next to device api service abstraction per ADR 0013 * PM-1378 - CryptoService makeDeviceKey test written * PM-1378 - Tweak crypto service makeDeviceKey test to leverage a describe for the function to better group related code. * PM-1378 - Move known devices call out of API service and into new devices-api.service and update all references. All clients building. * PM-1378 - Comment clean up * PM-1378 - Refactor out master key naming as that is a reserved specific key generated from the MP key derivation process + use same property on request object as back end. * PM-1378 - Missed a use of master key * PM-1378 - More abstraction updates to remove master key. * PM-1378 - Convert crypto service makeDeviceKey into getDeviceKey method to consolidate service logic based on PR feedback * PM-1378- Updating makeDeviceKey --> getDeviceKey tests to match updated code * PM-1378 - Current work on updating establish trusted device logic in light of new encryption mechanisms (introduction of a device asymmetric key pair in order to allow for key rotation while maintaining trusted devices) * PM-1378 - (1) CryptoService.TrustDevice() naming refactors (2) Lots of test additions and tweaks for trustDevice() * PM-1378 - Updated TrustedDeviceKeysRequest names to be consistent across the client side board. * PM-1378 - Move trusted device crypto service methods out of crypto service into new DeviceCryptoService for better single responsibility design * PM-1378 - (1) Add getDeviceByIdentifier endpoint to devices api as will need it later (2) Update TrustedDeviceKeysRequest and DeviceResponse models to match latest server side generic encrypted key names * PM-1378 - PR feedback fix - use JSDOC comments and move from abstraction to implementation * PM-1378 - Per PR feedback, makeDeviceKey should be private - updated tests with workaround. * PM-1378- Per PR feedback, refactored deviceKey to use partialKey dict so we can associate userId with specific device keys. * PM-1378 - Replace deviceId with deviceIdentifier per PR feedback * PM-1378 - Remove unnecessary createTrustedDeviceKey methods * PM-1378 - Update device crypto service to leverage updateTrustedDeviceKeys + update tests * PM-1378 - Update trustDevice logic - (1) Use getEncKey to get user symmetric key as it's the correct method and (2) Attempt to retrieve the userSymKey earlier on and short circuit if it is not found. * PM-1378 - Replace deviceId with deviceIdentifier because they are not the same thing * PM-1378 - Per PR feedback, (1) on web/browser extension, store device key in local storage under account.keys existing structure (2) on desktop, store deviceKey in secure storage. (3) Exempt account.keys.deviceKey from being cleared on account reset * PM-1378 - Desktop testing revealed that I forgot to add userId existence and options reconciliation checks back * PM-1378 - Per discussion with Jake, create DeviceKey custom type which is really just an opaque<SymmetricCryptoKey> so we can more easily differentiate between key types. * PM-1378 - Update symmetric-crypto-key.ts opaque DeviceKey to properly setup Opaque type. * PM-1378 - Fix wrong return type for getDeviceKey on DeviceCryptoServiceAbstraction per PR feedback
This commit is contained in:
parent
b9d3b0aff7
commit
0fcfe883b5
@ -3,9 +3,9 @@ import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
@ -27,7 +27,7 @@ import { flagEnabled } from "../../flags";
|
||||
export class LoginComponent extends BaseLoginComponent {
|
||||
showPasswordless = false;
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
@ -46,7 +46,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
authService,
|
||||
router,
|
||||
|
@ -6,10 +6,10 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
@ -52,7 +52,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
@ -74,7 +74,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
authService,
|
||||
router,
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
||||
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
|
||||
import { StorageOptions } from "@bitwarden/common/models/domain/storage-options";
|
||||
import {
|
||||
DeviceKey,
|
||||
SymmetricCryptoKey,
|
||||
} from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
|
||||
|
||||
import { Account } from "../models/account";
|
||||
@ -11,6 +16,10 @@ export class ElectronStateService
|
||||
extends BaseStateService<GlobalState, Account>
|
||||
implements ElectronStateServiceAbstraction
|
||||
{
|
||||
private partialKeys = {
|
||||
deviceKey: "_deviceKey",
|
||||
};
|
||||
|
||||
async addAccount(account: Account) {
|
||||
// Apply desktop overides to default account values
|
||||
account = new Account(account);
|
||||
@ -77,4 +86,27 @@ export class ElectronStateService
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
override async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const b64DeviceKey = await this.secureStorageService.get<string>(
|
||||
`${options.userId}${this.partialKeys.deviceKey}`,
|
||||
options
|
||||
);
|
||||
|
||||
return new SymmetricCryptoKey(Utils.fromB64ToArray(b64DeviceKey).buffer) as DeviceKey;
|
||||
}
|
||||
|
||||
override async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveSecureStorageKey(this.partialKeys.deviceKey, value.keyB64, options);
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
@ -41,7 +41,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
@ -63,7 +63,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
authService,
|
||||
router,
|
||||
|
@ -3,9 +3,9 @@ import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
@ -55,7 +55,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected devicesApiService: DevicesApiServiceAbstraction,
|
||||
protected appIdService: AppIdService,
|
||||
protected authService: AuthService,
|
||||
protected router: Router,
|
||||
@ -295,7 +295,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
async getLoginWithDevice(email: string) {
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const res = await this.apiService.getKnownDevice(email, deviceIdentifier);
|
||||
const res = await this.devicesApiService.getKnownDevice(email, deviceIdentifier);
|
||||
//ensure the application is not self-hosted
|
||||
this.showLoginWithDevice = res && !this.selfHosted;
|
||||
} catch (e) {
|
||||
|
@ -10,6 +10,8 @@ import { ConfigApiServiceAbstraction } from "@bitwarden/common/abstractions/conf
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DeviceCryptoServiceAbstraction } from "@bitwarden/common/abstractions/device-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@ -90,6 +92,8 @@ import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service
|
||||
import { CryptoService } from "@bitwarden/common/services/crypto.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DeviceCryptoService } from "@bitwarden/common/services/device-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { EnvironmentService } from "@bitwarden/common/services/environment.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
@ -351,6 +355,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
PlatformUtilsServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
DevicesApiServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -656,6 +662,23 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
useClass: OrgDomainApiService,
|
||||
deps: [OrgDomainServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: DevicesApiServiceAbstraction,
|
||||
useClass: DevicesApiServiceImplementation,
|
||||
deps: [ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: DeviceCryptoServiceAbstraction,
|
||||
useClass: DeviceCryptoService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
StateServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
DevicesApiServiceAbstraction,
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
|
@ -361,7 +361,6 @@ export abstract class ApiService {
|
||||
putDeviceVerificationSettings: (
|
||||
request: DeviceVerificationRequest
|
||||
) => Promise<DeviceVerificationResponse>;
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>;
|
||||
getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>;
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { DeviceKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { DeviceResponse } from "./devices/responses/device.response";
|
||||
|
||||
export abstract class DeviceCryptoServiceAbstraction {
|
||||
trustDevice: () => Promise<DeviceResponse>;
|
||||
getDeviceKey: () => Promise<DeviceKey>;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { DeviceResponse } from "./responses/device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
}
|
@ -7,6 +7,9 @@ export class DeviceResponse extends BaseResponse {
|
||||
identifier: string;
|
||||
type: DeviceType;
|
||||
creationDate: string;
|
||||
encryptedUserKey: string;
|
||||
encryptedPublicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@ -15,5 +18,8 @@ export class DeviceResponse extends BaseResponse {
|
||||
this.identifier = this.getResponseProperty("Identifier");
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
|
||||
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
|
||||
this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey");
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountSettingsSettings } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { WindowState } from "../models/domain/window-state";
|
||||
import { GeneratedPasswordHistory } from "../tools/generator/password";
|
||||
import { SendData } from "../tools/send/models/data/send.data";
|
||||
@ -163,6 +163,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
|
||||
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
|
@ -23,7 +23,7 @@ import { EventData } from "../data/event.data";
|
||||
import { ServerConfigData } from "../data/server-config.data";
|
||||
|
||||
import { EncString } from "./enc-string";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
@ -107,6 +107,7 @@ export class AccountKeys {
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
deviceKey?: DeviceKey;
|
||||
organizationKeys?: EncryptionPair<
|
||||
{ [orgId: string]: EncryptedOrganizationKeyData },
|
||||
Record<string, SymmetricCryptoKey>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
@ -75,3 +75,6 @@ export class SymmetricCryptoKey {
|
||||
return SymmetricCryptoKey.fromString(obj?.keyB64);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup all separate key types as opaque types
|
||||
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
||||
|
@ -1110,14 +1110,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new DeviceVerificationResponse(r);
|
||||
}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.send("GET", "/devices/knowndevice", null, false, true, null, (headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
});
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
// Emergency Access APIs
|
||||
|
||||
async getEmergencyAccessTrusted(): Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>> {
|
||||
|
@ -0,0 +1,85 @@
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { DeviceCryptoServiceAbstraction } from "../abstractions/device-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../types/csprng";
|
||||
|
||||
export class DeviceCryptoService implements DeviceCryptoServiceAbstraction {
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected encryptService: EncryptService,
|
||||
protected stateService: StateService,
|
||||
protected appIdService: AppIdService,
|
||||
protected devicesApiService: DevicesApiServiceAbstraction
|
||||
) {}
|
||||
|
||||
async trustDevice(): Promise<DeviceResponse> {
|
||||
// Attempt to get user symmetric key
|
||||
const userSymKey: SymmetricCryptoKey = await this.cryptoService.getEncKey();
|
||||
|
||||
// If user symmetric key is not found, throw error
|
||||
if (!userSymKey) {
|
||||
throw new Error("User symmetric key not found");
|
||||
}
|
||||
|
||||
// Generate deviceKey
|
||||
const deviceKey = await this.makeDeviceKey();
|
||||
|
||||
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
|
||||
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
|
||||
2048
|
||||
);
|
||||
|
||||
const [
|
||||
devicePublicKeyEncryptedUserSymKey,
|
||||
userSymKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user symmetric key with the DevicePublicKey
|
||||
this.cryptoService.rsaEncrypt(userSymKey.encKey, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user symmetric key
|
||||
this.encryptService.encrypt(devicePublicKey, userSymKey),
|
||||
|
||||
// Encrypt devicePrivateKey with deviceKey
|
||||
this.encryptService.encrypt(devicePrivateKey, deviceKey),
|
||||
]);
|
||||
|
||||
// Send encrypted keys to server
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
return this.devicesApiService.updateTrustedDeviceKeys(
|
||||
deviceIdentifier,
|
||||
devicePublicKeyEncryptedUserSymKey.encryptedString,
|
||||
userSymKeyEncryptedDevicePublicKey.encryptedString,
|
||||
deviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
}
|
||||
|
||||
async getDeviceKey(): Promise<DeviceKey> {
|
||||
// Check if device key is already stored
|
||||
const existingDeviceKey = await this.stateService.getDeviceKey();
|
||||
|
||||
if (existingDeviceKey != null) {
|
||||
return existingDeviceKey;
|
||||
} else {
|
||||
return this.makeDeviceKey();
|
||||
}
|
||||
}
|
||||
|
||||
private async makeDeviceKey(): Promise<DeviceKey> {
|
||||
// Create 512-bit device key
|
||||
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
|
||||
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
|
||||
|
||||
// Save device key in secure storage
|
||||
await this.stateService.setDeviceKey(deviceKey);
|
||||
|
||||
return deviceKey;
|
||||
}
|
||||
}
|
317
libs/common/src/services/device-crypto.service.spec.ts
Normal file
317
libs/common/src/services/device-crypto.service.spec.ts
Normal file
@ -0,0 +1,317 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EncryptionType } from "../enums/encryption-type.enum";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../services/crypto.service";
|
||||
import { CsprngArray } from "../types/csprng";
|
||||
|
||||
import { DeviceCryptoService } from "./device-crypto.service.implementation";
|
||||
|
||||
describe("deviceCryptoService", () => {
|
||||
let deviceCryptoService: DeviceCryptoService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const stateService = mock<StateService>();
|
||||
const appIdService = mock<AppIdService>();
|
||||
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(cryptoFunctionService);
|
||||
mockReset(encryptService);
|
||||
mockReset(stateService);
|
||||
mockReset(appIdService);
|
||||
mockReset(devicesApiService);
|
||||
|
||||
deviceCryptoService = new DeviceCryptoService(
|
||||
cryptoFunctionService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
stateService,
|
||||
appIdService,
|
||||
devicesApiService
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(deviceCryptoService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("Trusted Device Encryption", () => {
|
||||
const deviceKeyBytesLength = 64;
|
||||
const userSymKeyBytesLength = 64;
|
||||
|
||||
describe("getDeviceKey", () => {
|
||||
let mockRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: SymmetricCryptoKey;
|
||||
let existingDeviceKey: DeviceKey;
|
||||
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
|
||||
existingDeviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
|
||||
makeDeviceKeySpy = jest.spyOn(deviceCryptoService as any, "makeDeviceKey");
|
||||
});
|
||||
|
||||
it("gets a device key when there is not an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
|
||||
makeDeviceKeySpy.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
const deviceKey = await deviceCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(mockDeviceKey);
|
||||
});
|
||||
|
||||
it("returns the existing device key without creating a new one when there is an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
|
||||
|
||||
const deviceKey = await deviceCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(makeDeviceKeySpy).not.toHaveBeenCalled();
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(existingDeviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeDeviceKey", () => {
|
||||
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
|
||||
const cryptoFuncSvcRandomBytesSpy = jest
|
||||
.spyOn(cryptoFunctionService, "randomBytes")
|
||||
.mockResolvedValue(mockRandomBytes);
|
||||
|
||||
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
// This is a hacky workaround, but it allows for cleaner tests
|
||||
const deviceKey = await (deviceCryptoService as any).makeDeviceKey();
|
||||
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trustDevice", () => {
|
||||
let mockDeviceKeyRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: DeviceKey;
|
||||
|
||||
let mockUserSymKeyRandomBytes: CsprngArray;
|
||||
let mockUserSymKey: SymmetricCryptoKey;
|
||||
|
||||
const deviceRsaKeyLength = 2048;
|
||||
let mockDeviceRsaKeyPair: [ArrayBuffer, ArrayBuffer];
|
||||
let mockDevicePrivateKey: ArrayBuffer;
|
||||
let mockDevicePublicKey: ArrayBuffer;
|
||||
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
|
||||
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
|
||||
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
|
||||
|
||||
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
|
||||
Id: "mockId",
|
||||
Name: "mockName",
|
||||
Identifier: "mockIdentifier",
|
||||
Type: "mockType",
|
||||
CreationDate: "mockCreationDate",
|
||||
});
|
||||
|
||||
const mockDeviceId = "mockDeviceId";
|
||||
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
let rsaGenerateKeyPairSpy: jest.SpyInstance;
|
||||
let cryptoSvcGetEncKeySpy: jest.SpyInstance;
|
||||
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
|
||||
let encryptServiceEncryptSpy: jest.SpyInstance;
|
||||
let appIdServiceGetAppIdSpy: jest.SpyInstance;
|
||||
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup all spies and default return values for the happy path
|
||||
|
||||
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray;
|
||||
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
|
||||
|
||||
mockDeviceRsaKeyPair = [
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
];
|
||||
|
||||
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
|
||||
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
|
||||
|
||||
mockDevicePublicKeyEncryptedUserSymKey = new EncString(
|
||||
EncryptionType.Rsa2048_OaepSha1_B64,
|
||||
"mockDevicePublicKeyEncryptedUserSymKey"
|
||||
);
|
||||
|
||||
mockUserSymKeyEncryptedDevicePublicKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockUserSymKeyEncryptedDevicePublicKey"
|
||||
);
|
||||
|
||||
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockDeviceKeyEncryptedDevicePrivateKey"
|
||||
);
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
makeDeviceKeySpy = jest
|
||||
.spyOn(deviceCryptoService as any, "makeDeviceKey")
|
||||
.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
rsaGenerateKeyPairSpy = jest
|
||||
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
|
||||
.mockResolvedValue(mockDeviceRsaKeyPair);
|
||||
|
||||
cryptoSvcGetEncKeySpy = jest
|
||||
.spyOn(cryptoService, "getEncKey")
|
||||
.mockResolvedValue(mockUserSymKey);
|
||||
|
||||
cryptoSvcRsaEncryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaEncrypt")
|
||||
.mockResolvedValue(mockDevicePublicKeyEncryptedUserSymKey);
|
||||
|
||||
encryptServiceEncryptSpy = jest
|
||||
.spyOn(encryptService, "encrypt")
|
||||
.mockImplementation((plainValue, key) => {
|
||||
if (plainValue === mockDevicePublicKey && key === mockUserSymKey) {
|
||||
return Promise.resolve(mockUserSymKeyEncryptedDevicePublicKey);
|
||||
}
|
||||
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
|
||||
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
|
||||
}
|
||||
});
|
||||
|
||||
appIdServiceGetAppIdSpy = jest
|
||||
.spyOn(appIdService, "getAppId")
|
||||
.mockResolvedValue(mockDeviceId);
|
||||
|
||||
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
|
||||
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
|
||||
.mockResolvedValue(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
|
||||
const response = await deviceCryptoService.trustDevice();
|
||||
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoSvcGetEncKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
|
||||
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
|
||||
mockDeviceId,
|
||||
mockDevicePublicKeyEncryptedUserSymKey.encryptedString,
|
||||
mockUserSymKeyEncryptedDevicePublicKey.encryptedString,
|
||||
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
|
||||
expect(response).toBeInstanceOf(DeviceResponse);
|
||||
expect(response).toEqual(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("throws specific error if user symmetric key is not found", async () => {
|
||||
// setup the spy to return null
|
||||
cryptoSvcGetEncKeySpy.mockResolvedValue(null);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
|
||||
// reset the spy
|
||||
cryptoSvcGetEncKeySpy.mockReset();
|
||||
|
||||
// setup the spy to return undefined
|
||||
cryptoSvcGetEncKeySpy.mockResolvedValue(undefined);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
});
|
||||
|
||||
const methodsToTestForErrorsOrInvalidReturns = [
|
||||
{
|
||||
method: "makeDeviceKey",
|
||||
spy: () => makeDeviceKeySpy,
|
||||
errorText: "makeDeviceKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaGenerateKeyPair",
|
||||
spy: () => rsaGenerateKeyPairSpy,
|
||||
errorText: "rsaGenerateKeyPair error",
|
||||
},
|
||||
{
|
||||
method: "getEncKey",
|
||||
spy: () => cryptoSvcGetEncKeySpy,
|
||||
errorText: "getEncKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaEncrypt",
|
||||
spy: () => cryptoSvcRsaEncryptSpy,
|
||||
errorText: "rsaEncrypt error",
|
||||
},
|
||||
{
|
||||
method: "encryptService.encrypt",
|
||||
spy: () => encryptServiceEncryptSpy,
|
||||
errorText: "encryptService.encrypt error",
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(methodsToTestForErrorsOrInvalidReturns)(
|
||||
"trustDevice error handling and invalid return testing",
|
||||
({ method, spy, errorText }) => {
|
||||
// ensures that error propagation works correctly
|
||||
it(`throws an error if ${method} fails`, async () => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockRejectedValue(new Error(errorText));
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(errorText);
|
||||
});
|
||||
|
||||
test.each([null, undefined])(
|
||||
`throws an error if ${method} returns %s`,
|
||||
async (invalidValue) => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockResolvedValue(invalidValue);
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { ApiService } from "../api.service";
|
||||
|
||||
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
|
||||
|
||||
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/devices/knowndevice",
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
(headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
}
|
||||
);
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device by identifier
|
||||
* @param deviceIdentifier - client generated id (not device id in DB)
|
||||
*/
|
||||
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/devices/identifier/${deviceIdentifier}`,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new DeviceResponse(r);
|
||||
}
|
||||
|
||||
async updateTrustedDeviceKeys(
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
): Promise<DeviceResponse> {
|
||||
const request = new TrustedDeviceKeysRequest(
|
||||
devicePublicKeyEncryptedUserSymKey,
|
||||
userSymKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey
|
||||
);
|
||||
|
||||
const result = await this.apiService.send(
|
||||
"PUT",
|
||||
`/devices/${deviceIdentifier}/keys`,
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return new DeviceResponse(result);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export class TrustedDeviceKeysRequest {
|
||||
constructor(
|
||||
public encryptedUserKey: string,
|
||||
public encryptedPublicKey: string,
|
||||
public encryptedPrivateKey: string
|
||||
) {}
|
||||
}
|
@ -35,7 +35,7 @@ import { EncString } from "../models/domain/enc-string";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
import { State } from "../models/domain/state";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { WindowState } from "../models/domain/window-state";
|
||||
import { GeneratedPasswordHistory } from "../tools/generator/password";
|
||||
import { SendData } from "../tools/send/models/data/send.data";
|
||||
@ -1054,6 +1054,32 @@ export class StateService<
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
||||
}
|
||||
|
||||
async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
return account?.keys?.deviceKey as DeviceKey;
|
||||
}
|
||||
|
||||
async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
account.keys.deviceKey = value;
|
||||
|
||||
await this.saveAccount(account, options);
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
@ -2751,7 +2777,10 @@ export class StateService<
|
||||
|
||||
// settings persist even on reset, and are not effected by this method
|
||||
protected resetAccount(account: TAccount) {
|
||||
const persistentAccountInformation = { settings: account.settings };
|
||||
const persistentAccountInformation = {
|
||||
settings: account.settings,
|
||||
keys: { deviceKey: account.keys.deviceKey },
|
||||
};
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
@ -2830,7 +2859,7 @@ export class StateService<
|
||||
return this.reconcileOptions(options, defaultOptions);
|
||||
}
|
||||
|
||||
private async saveSecureStorageKey<T extends JsonValue>(
|
||||
protected async saveSecureStorageKey<T extends JsonValue>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?: StorageOptions
|
||||
|
4
libs/common/src/types/csprng.d.ts
vendored
4
libs/common/src/types/csprng.d.ts
vendored
@ -1,5 +1,9 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
// You would typically use these types when you want to create a type that
|
||||
// represents an array or string value generated from a
|
||||
// cryptographic secure pseudorandom number generator (CSPRNG)
|
||||
|
||||
type CsprngArray = Opaque<ArrayBuffer, "CSPRNG">;
|
||||
|
||||
type CsprngString = Opaque<string, "CSPRNG">;
|
||||
|
Loading…
Reference in New Issue
Block a user