mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
Auth/PM-5268 - DeviceTrustCryptoService state provider migration (#7882)
* PM-5268 - Add DEVICE_TRUST_DISK to state definitions * PM-5268 - DeviceTrustCryptoService - Get most of state provider refactor done - WIP - commented out stuff for now. * PM-5268 - DeviceTrustCryptoServiceStateProviderMigrator - WIP - got first draft of migrator in place and working on tests. Rollback tests are failing for some reason TBD. * PM-5268 - more WIP on device trust crypto service migrator tests * PM-5268 - DeviceTrustCryptoServiceStateProviderMigrator - Refactor based on call with platform * PM-5268 - DeviceTrustCryptoServiceStateProviderMigrator - tests passing * PM-5268 - Update DeviceTrustCryptoService to convert over to state providers + update all service instantiations / dependencies to ensure state provider is passed in or injected. * PM-5268 - Register new migration * PM-5268 - Temporarily remove device trust crypto service from migrator to ease merge conflicts as there are 6 more migrators before I can apply mine in main. * PM-5268 - Update migration numbers of DeviceTrustCryptoServiceStateProviderMigrator based on latest migrations from main. * PM-5268 - (1) Export new KeyDefinitions from DeviceTrustCryptoService for use in test suite (2) Update DeviceTrustCryptoService test file to use state provider. * PM-5268 - Fix DeviceTrustCryptoServiceStateProviderMigrator tests to use proper versions * PM-5268 - Actually fix all instances of DeviceTrustCryptoServiceStateProviderMigrator test failures * PM-5268 - Clean up state service, account, and login strategy of all migrated references * PM-5268 - Account - finish cleaning up device key * PM-5268 - StateService - clean up last reference to device key * PM-5268 - Remove even more device key refs. *facepalm* * PM-5268 - Finish resolving merge conflicts by incrementing migration version from 22 to 23 * PM-5268 - bump migration versions * PM-5268 - DeviceTrustCryptoService - Implement secure storage functionality for getDeviceKey and setDeviceKey (to achieve feature parity with the ElectronStateService implementation prior to the state provider migration). Tests to follow shortly. * PM-5268 - DeviceTrustCryptoService tests - getDeviceKey now tested with all new secure storage scenarios. SetDeviceKey tests to follow. * PM-5268 - DeviceTrustCryptoService tests - test all setDeviceKey scenarios with state provider & secure storage * PM-5268 - Update DeviceTrustCryptoService deps to actually use secure storage svc on platforms that support it. * PM-5268 - Bump migration version due to merge conflicts. * PM-5268 - Bump migration version * PM-5268 - tweak jsdocs to be single line per PR feedback * PM-5268 - DeviceTrustCryptoSvc - improve debuggability. * PM-5268 - Remove state service as a dependency on the device trust crypto service (woo!) * PM-5268 - Update migration test json to correctly reflect reality. * PM-5268 - DeviceTrustCryptoSvc - getDeviceKey - add throw error for active user id missing. * PM-5268 - Fix tests * PM-5268 - WIP start on adding user id to every method on device trust crypto service. * PM-5268 - Update lock comp dependencies across clients * PM-5268 - Update login via auth request deps across clients to add acct service. * PM-5268 - UserKeyRotationSvc - add acct service to get active acct id for call to rotateDevicesTrust and then update tests. * PM-5268 - WIP on trying to fix device trust crypto svc tests. * PM-5268 - More WIP device trust crypto svc tests passing * PM-5268 - Device Trust crypto service - get all tests passing * PM-5268 - DeviceTrustCryptoService.getDeviceKey - fix secure storage b64 to symmetric crypto key conversion * PM-5268 - Add more tests and update test names * PM-5268 - rename state to indicate it was disk local * PM-5268 - DeviceTrustCryptoService - save symmetric key in JSON format * PM-5268 - Fix lock comp tests by adding acct service dep * PM-5268 - Update set device key tests to pass * PM-5268 - Bump migration versions again * PM-5268 - Fix user key rotation svc tests * PM-5268 - Update web jest config to allow use of common spec in user-key-rotation-svc tests * PM-5268 - Bump migration version * PM-5268 - Per PR feedback, save off user id * PM-5268 - bump migration version * PM-5268 - Per PR feedback, remove unnecessary await. * PM-5268 - Bump migration verson
This commit is contained in:
parent
94843bdd8b
commit
c202c93378
@ -39,9 +39,13 @@ import {
|
|||||||
platformUtilsServiceFactory,
|
platformUtilsServiceFactory,
|
||||||
} from "../../../platform/background/service-factories/platform-utils-service.factory";
|
} from "../../../platform/background/service-factories/platform-utils-service.factory";
|
||||||
import {
|
import {
|
||||||
StateServiceInitOptions,
|
StateProviderInitOptions,
|
||||||
stateServiceFactory,
|
stateProviderFactory,
|
||||||
} from "../../../platform/background/service-factories/state-service.factory";
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
import {
|
||||||
|
SecureStorageServiceInitOptions,
|
||||||
|
secureStorageServiceFactory,
|
||||||
|
} from "../../../platform/background/service-factories/storage-service.factory";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserDecryptionOptionsServiceInitOptions,
|
UserDecryptionOptionsServiceInitOptions,
|
||||||
@ -55,11 +59,12 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor
|
|||||||
CryptoFunctionServiceInitOptions &
|
CryptoFunctionServiceInitOptions &
|
||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
EncryptServiceInitOptions &
|
EncryptServiceInitOptions &
|
||||||
StateServiceInitOptions &
|
|
||||||
AppIdServiceInitOptions &
|
AppIdServiceInitOptions &
|
||||||
DevicesApiServiceInitOptions &
|
DevicesApiServiceInitOptions &
|
||||||
I18nServiceInitOptions &
|
I18nServiceInitOptions &
|
||||||
PlatformUtilsServiceInitOptions &
|
PlatformUtilsServiceInitOptions &
|
||||||
|
StateProviderInitOptions &
|
||||||
|
SecureStorageServiceInitOptions &
|
||||||
UserDecryptionOptionsServiceInitOptions;
|
UserDecryptionOptionsServiceInitOptions;
|
||||||
|
|
||||||
export function deviceTrustCryptoServiceFactory(
|
export function deviceTrustCryptoServiceFactory(
|
||||||
@ -76,11 +81,12 @@ export function deviceTrustCryptoServiceFactory(
|
|||||||
await cryptoFunctionServiceFactory(cache, opts),
|
await cryptoFunctionServiceFactory(cache, opts),
|
||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
await encryptServiceFactory(cache, opts),
|
await encryptServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
|
||||||
await appIdServiceFactory(cache, opts),
|
await appIdServiceFactory(cache, opts),
|
||||||
await devicesApiServiceFactory(cache, opts),
|
await devicesApiServiceFactory(cache, opts),
|
||||||
await i18nServiceFactory(cache, opts),
|
await i18nServiceFactory(cache, opts),
|
||||||
await platformUtilsServiceFactory(cache, opts),
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
|
await secureStorageServiceFactory(cache, opts),
|
||||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
@ -62,6 +63,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
private routerService: BrowserRouterService,
|
private routerService: BrowserRouterService,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
|
accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
@ -84,6 +86,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
this.successRoute = "/tabs/current";
|
this.successRoute = "/tabs/current";
|
||||||
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
@ -49,6 +50,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
authRequestService: AuthRequestServiceAbstraction,
|
authRequestService: AuthRequestServiceAbstraction,
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
|
accountService: AccountService,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@ -70,6 +72,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||||||
deviceTrustCryptoService,
|
deviceTrustCryptoService,
|
||||||
authRequestService,
|
authRequestService,
|
||||||
loginStrategyService,
|
loginStrategyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
super.onSuccessfulLogin = async () => {
|
super.onSuccessfulLogin = async () => {
|
||||||
await syncService.fullSync(true);
|
await syncService.fullSync(true);
|
||||||
|
@ -556,11 +556,12 @@ export default class MainBackground {
|
|||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.stateService,
|
|
||||||
this.appIdService,
|
this.appIdService,
|
||||||
this.devicesApiService,
|
this.devicesApiService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
|
this.stateProvider,
|
||||||
|
this.secureStorageService,
|
||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -455,11 +455,12 @@ export class Main {
|
|||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.stateService,
|
|
||||||
this.appIdService,
|
this.appIdService,
|
||||||
this.devicesApiService,
|
this.devicesApiService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
|
this.stateProvider,
|
||||||
|
this.secureStorageService,
|
||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
@ -23,7 +24,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { LockComponent } from "./lock.component";
|
import { LockComponent } from "./lock.component";
|
||||||
@ -49,6 +53,9 @@ describe("LockComponent", () => {
|
|||||||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||||
let activatedRouteMock: MockProxy<ActivatedRoute>;
|
let activatedRouteMock: MockProxy<ActivatedRoute>;
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
stateServiceMock = mock<StateService>();
|
stateServiceMock = mock<StateService>();
|
||||||
stateServiceMock.activeAccount$ = of(null);
|
stateServiceMock.activeAccount$ = of(null);
|
||||||
@ -147,6 +154,10 @@ describe("LockComponent", () => {
|
|||||||
provide: BiometricStateService,
|
provide: BiometricStateService,
|
||||||
useValue: biometricStateService,
|
useValue: biometricStateService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AccountService,
|
||||||
|
useValue: accountService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@ -9,6 +9,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
@ -59,6 +60,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
|
accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
@ -81,6 +83,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
@ -57,6 +58,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
authRequestService: AuthRequestServiceAbstraction,
|
authRequestService: AuthRequestServiceAbstraction,
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
|
accountService: AccountService,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@ -78,6 +80,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||||||
deviceTrustCryptoService,
|
deviceTrustCryptoService,
|
||||||
authRequestService,
|
authRequestService,
|
||||||
loginStrategyService,
|
loginStrategyService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
super.onSuccessfulLogin = () => {
|
super.onSuccessfulLogin = () => {
|
||||||
|
@ -1,47 +1,12 @@
|
|||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
||||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
import { DeviceKey } from "@bitwarden/common/types/key";
|
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
|
|
||||||
export class ElectronStateService extends BaseStateService<GlobalState, Account> {
|
export class ElectronStateService extends BaseStateService<GlobalState, Account> {
|
||||||
private partialKeys = {
|
|
||||||
deviceKey: "_deviceKey",
|
|
||||||
};
|
|
||||||
|
|
||||||
async addAccount(account: Account) {
|
async addAccount(account: Account) {
|
||||||
// Apply desktop overides to default account values
|
// Apply desktop overides to default account values
|
||||||
account = new Account(account);
|
account = new Account(account);
|
||||||
await super.addAccount(account);
|
await super.addAccount(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (b64DeviceKey == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SymmetricCryptoKey(Utils.fromB64ToArray(b64DeviceKey)) 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,11 @@ module.exports = {
|
|||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(
|
||||||
prefix: "<rootDir>/",
|
// lets us use @bitwarden/common/spec in web tests
|
||||||
}),
|
{ "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||||
|
{
|
||||||
|
prefix: "<rootDir>/",
|
||||||
|
},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
@ -6,10 +6,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
|
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
@ -41,6 +44,9 @@ describe("KeyRotationService", () => {
|
|||||||
let mockStateService: MockProxy<StateService>;
|
let mockStateService: MockProxy<StateService>;
|
||||||
let mockConfigService: MockProxy<ConfigService>;
|
let mockConfigService: MockProxy<ConfigService>;
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockApiService = mock<UserKeyRotationApiService>();
|
mockApiService = mock<UserKeyRotationApiService>();
|
||||||
mockCipherService = mock<CipherService>();
|
mockCipherService = mock<CipherService>();
|
||||||
@ -65,6 +71,7 @@ describe("KeyRotationService", () => {
|
|||||||
mockCryptoService,
|
mockCryptoService,
|
||||||
mockEncryptService,
|
mockEncryptService,
|
||||||
mockStateService,
|
mockStateService,
|
||||||
|
mockAccountService,
|
||||||
mockConfigService,
|
mockConfigService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
@ -34,6 +35,7 @@ export class UserKeyRotationService {
|
|||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private accountService: AccountService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -90,7 +92,12 @@ export class UserKeyRotationService {
|
|||||||
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
await this.deviceTrustCryptoService.rotateDevicesTrust(
|
||||||
|
activeAccount.id,
|
||||||
|
newUserKey,
|
||||||
|
masterPasswordHash,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
|
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
|
||||||
|
@ -8,6 +8,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@ -47,6 +48,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
|
accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
@ -69,6 +71,7 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||||
@ -34,6 +35,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
NewUser,
|
NewUser,
|
||||||
@ -65,6 +67,8 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||||||
protected data?: Data;
|
protected data?: Data;
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
|
|
||||||
|
activeAccountId: UserId;
|
||||||
|
|
||||||
// Remember device means for the user to trust the device
|
// Remember device means for the user to trust the device
|
||||||
rememberDeviceForm = this.formBuilder.group({
|
rememberDeviceForm = this.formBuilder.group({
|
||||||
rememberDevice: [true],
|
rememberDevice: [true],
|
||||||
@ -94,10 +98,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||||||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
|
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
|
||||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
|
protected accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
this.setupRememberDeviceValueChanges();
|
this.setupRememberDeviceValueChanges();
|
||||||
|
|
||||||
@ -150,7 +156,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async setRememberDeviceDefaultValue() {
|
private async setRememberDeviceDefaultValue() {
|
||||||
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice();
|
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice(
|
||||||
|
this.activeAccountId,
|
||||||
|
);
|
||||||
|
|
||||||
const rememberDevice = rememberDeviceFromState ?? true;
|
const rememberDevice = rememberDeviceFromState ?? true;
|
||||||
|
|
||||||
@ -161,7 +169,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||||||
this.rememberDevice.valueChanges
|
this.rememberDevice.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((value) =>
|
switchMap((value) =>
|
||||||
defer(() => this.deviceTrustCryptoService.setShouldTrustDevice(value)),
|
defer(() =>
|
||||||
|
this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
@ -278,7 +288,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||||||
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
|
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
|
||||||
|
|
||||||
if (this.rememberDeviceForm.value.rememberDevice) {
|
if (this.rememberDeviceForm.value.rememberDevice) {
|
||||||
await this.deviceTrustCryptoService.trustDevice();
|
await this.deviceTrustCryptoService.trustDevice(this.activeAccountId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.validationService.showError(error);
|
this.validationService.showError(error);
|
||||||
|
@ -10,6 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
|
|||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
@ -75,6 +76,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
protected userVerificationService: UserVerificationService,
|
protected userVerificationService: UserVerificationService,
|
||||||
protected pinCryptoService: PinCryptoServiceAbstraction,
|
protected pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
protected biometricStateService: BiometricStateService,
|
protected biometricStateService: BiometricStateService,
|
||||||
|
protected accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -269,7 +271,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Now that we have a decrypted user key in memory, we can check if we
|
// Now that we have a decrypted user key in memory, we can check if we
|
||||||
// need to establish trust on the current device
|
// need to establish trust on the current device
|
||||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id);
|
||||||
|
|
||||||
await this.doContinue(evaluatePasswordAfterUnlock);
|
await this.doContinue(evaluatePasswordAfterUnlock);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { IsActiveMatchOptions, Router } from "@angular/router";
|
import { IsActiveMatchOptions, Router } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthRequestLoginCredentials,
|
AuthRequestLoginCredentials,
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
@ -87,6 +88,7 @@ export class LoginViaAuthRequestComponent
|
|||||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
private authRequestService: AuthRequestServiceAbstraction,
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(environmentService, i18nService, platformUtilsService);
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
|
||||||
@ -388,7 +390,8 @@ export class LoginViaAuthRequestComponent
|
|||||||
|
|
||||||
// Now that we have a decrypted user key in memory, we can check if we
|
// Now that we have a decrypted user key in memory, we can check if we
|
||||||
// need to establish trust on the current device
|
// need to establish trust on the current device
|
||||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id);
|
||||||
|
|
||||||
// TODO: don't forget to use auto enrollment service everywhere we trust device
|
// TODO: don't forget to use auto enrollment service everywhere we trust device
|
||||||
|
|
||||||
|
@ -912,11 +912,12 @@ const safeProviders: SafeProvider[] = [
|
|||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
StateServiceAbstraction,
|
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
DevicesApiServiceAbstraction,
|
DevicesApiServiceAbstraction,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
SECURE_STORAGE,
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
@ -16,6 +16,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||||
@ -128,8 +129,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
||||||
} else {
|
} else {
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey();
|
||||||
|
|
||||||
|
const userId = (await this.stateService.getUserId()) as UserId;
|
||||||
// Establish trust if required after setting user key
|
// Establish trust if required after setting user key
|
||||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
} from "@bitwarden/common/tools/password-strength";
|
} from "@bitwarden/common/tools/password-strength";
|
||||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
|
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||||
@ -215,29 +215,6 @@ describe("LoginStrategy", () => {
|
|||||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("persists a device key for trusted device encryption when it exists on login", async () => {
|
|
||||||
// Arrange
|
|
||||||
const idTokenResponse = identityTokenResponseFactory();
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
|
||||||
|
|
||||||
const deviceKey = new SymmetricCryptoKey(
|
|
||||||
new Uint8Array(userKeyBytesLength).buffer as CsprngArray,
|
|
||||||
) as DeviceKey;
|
|
||||||
|
|
||||||
stateService.getDeviceKey.mockResolvedValue(deviceKey);
|
|
||||||
|
|
||||||
const accountKeys = new AccountKeys();
|
|
||||||
accountKeys.deviceKey = deviceKey;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await passwordLoginStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(stateService.addAccount).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ keys: accountKeys }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds AuthResult", async () => {
|
it("builds AuthResult", async () => {
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
const tokenResponse = identityTokenResponseFactory();
|
||||||
tokenResponse.forcePasswordReset = true;
|
tokenResponse.forcePasswordReset = true;
|
||||||
|
@ -26,7 +26,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import {
|
import {
|
||||||
AccountKeys,
|
|
||||||
Account,
|
Account,
|
||||||
AccountProfile,
|
AccountProfile,
|
||||||
AccountTokens,
|
AccountTokens,
|
||||||
@ -160,18 +159,8 @@ export abstract class LoginStrategy {
|
|||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<void> {
|
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||||
const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken);
|
const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken);
|
||||||
|
|
||||||
// Must persist existing device key if it exists for trusted device decryption to work
|
|
||||||
// However, we must provide a user id so that the device key can be retrieved
|
|
||||||
// as the state service won't have an active account at this point in time
|
|
||||||
// even though the data exists in local storage.
|
|
||||||
const userId = accountInformation.sub;
|
const userId = accountInformation.sub;
|
||||||
|
|
||||||
const deviceKey = await this.stateService.getDeviceKey({ userId });
|
|
||||||
const accountKeys = new AccountKeys();
|
|
||||||
if (deviceKey) {
|
|
||||||
accountKeys.deviceKey = deviceKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you don't persist existing admin auth requests on login, they will get deleted.
|
// If you don't persist existing admin auth requests on login, they will get deleted.
|
||||||
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
|
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
|
||||||
|
|
||||||
@ -204,7 +193,6 @@ export abstract class LoginStrategy {
|
|||||||
tokens: {
|
tokens: {
|
||||||
...new AccountTokens(),
|
...new AccountTokens(),
|
||||||
},
|
},
|
||||||
keys: accountKeys,
|
|
||||||
adminAuthRequest: adminAuthRequest?.toJSON(),
|
adminAuthRequest: adminAuthRequest?.toJSON(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -20,6 +20,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
@ -284,7 +285,8 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
if (await this.cryptoService.hasUserKey()) {
|
if (await this.cryptoService.hasUserKey()) {
|
||||||
// Now that we have a decrypted user key in memory, we can check if we
|
// Now that we have a decrypted user key in memory, we can check if we
|
||||||
// need to establish trust on the current device
|
// need to establish trust on the current device
|
||||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
const userId = (await this.stateService.getUserId()) as UserId;
|
||||||
|
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
|
||||||
|
|
||||||
// if we successfully decrypted the user key, we can delete the admin auth request out of state
|
// if we successfully decrypted the user key, we can delete the admin auth request out of state
|
||||||
// TODO: eventually we post and clean up DB as well once consumed on client
|
// TODO: eventually we post and clean up DB as well once consumed on client
|
||||||
@ -298,7 +300,9 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||||
const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption;
|
const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption;
|
||||||
|
|
||||||
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey();
|
const userId = (await this.stateService.getUserId()) as UserId;
|
||||||
|
|
||||||
|
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(userId);
|
||||||
const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey;
|
const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey;
|
||||||
const encUserKey = trustedDeviceOption?.encryptedUserKey;
|
const encUserKey = trustedDeviceOption?.encryptedUserKey;
|
||||||
|
|
||||||
@ -307,6 +311,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||||
|
userId,
|
||||||
encDevicePrivateKey,
|
encDevicePrivateKey,
|
||||||
encUserKey,
|
encUserKey,
|
||||||
deviceKey,
|
deviceKey,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, UserKey } from "../../types/key";
|
import { DeviceKey, UserKey } from "../../types/key";
|
||||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||||
|
|
||||||
@ -10,17 +11,24 @@ export abstract class DeviceTrustCryptoServiceAbstraction {
|
|||||||
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
||||||
* Note: this value should only be used once and then reset
|
* Note: this value should only be used once and then reset
|
||||||
*/
|
*/
|
||||||
getShouldTrustDevice: () => Promise<boolean | null>;
|
getShouldTrustDevice: (userId: UserId) => Promise<boolean | null>;
|
||||||
setShouldTrustDevice: (value: boolean) => Promise<void>;
|
setShouldTrustDevice: (userId: UserId, value: boolean) => Promise<void>;
|
||||||
|
|
||||||
trustDeviceIfRequired: () => Promise<void>;
|
trustDeviceIfRequired: (userId: UserId) => Promise<void>;
|
||||||
|
|
||||||
trustDevice: () => Promise<DeviceResponse>;
|
trustDevice: (userId: UserId) => Promise<DeviceResponse>;
|
||||||
getDeviceKey: () => Promise<DeviceKey>;
|
|
||||||
|
/** Retrieves the device key if it exists from state or secure storage if supported for the active user. */
|
||||||
|
getDeviceKey: (userId: UserId) => Promise<DeviceKey | null>;
|
||||||
decryptUserKeyWithDeviceKey: (
|
decryptUserKeyWithDeviceKey: (
|
||||||
|
userId: UserId,
|
||||||
encryptedDevicePrivateKey: EncString,
|
encryptedDevicePrivateKey: EncString,
|
||||||
encryptedUserKey: EncString,
|
encryptedUserKey: EncString,
|
||||||
deviceKey?: DeviceKey,
|
deviceKey: DeviceKey,
|
||||||
) => Promise<UserKey | null>;
|
) => Promise<UserKey | null>;
|
||||||
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
|
rotateDevicesTrust: (
|
||||||
|
userId: UserId,
|
||||||
|
newUserKey: UserKey,
|
||||||
|
masterPasswordHash: string,
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,13 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
|||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
|
||||||
|
import { StorageLocation } from "../../platform/enums";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { DEVICE_TRUST_DISK_LOCAL, KeyDefinition, StateProvider } from "../../platform/state";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { UserKey, DeviceKey } from "../../types/key";
|
import { UserKey, DeviceKey } from "../../types/key";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||||
@ -22,7 +26,25 @@ import {
|
|||||||
UpdateDevicesTrustRequest,
|
UpdateDevicesTrustRequest,
|
||||||
} from "../models/request/update-devices-trust.request";
|
} from "../models/request/update-devices-trust.request";
|
||||||
|
|
||||||
|
/** Uses disk storage so that the device key can persist after log out and tab removal. */
|
||||||
|
export const DEVICE_KEY = new KeyDefinition<DeviceKey>(DEVICE_TRUST_DISK_LOCAL, "deviceKey", {
|
||||||
|
deserializer: (deviceKey) => SymmetricCryptoKey.fromJSON(deviceKey) as DeviceKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Uses disk storage so that the shouldTrustDevice bool can persist across login. */
|
||||||
|
export const SHOULD_TRUST_DEVICE = new KeyDefinition<boolean>(
|
||||||
|
DEVICE_TRUST_DISK_LOCAL,
|
||||||
|
"shouldTrustDevice",
|
||||||
|
{
|
||||||
|
deserializer: (shouldTrustDevice) => shouldTrustDevice,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
|
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
|
||||||
|
private readonly platformSupportsSecureStorage =
|
||||||
|
this.platformUtilsService.supportsSecureStorage();
|
||||||
|
private readonly deviceKeySecureStorageKey: string = "_deviceKey";
|
||||||
|
|
||||||
supportsDeviceTrust$: Observable<boolean>;
|
supportsDeviceTrust$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -30,11 +52,12 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||||||
private cryptoFunctionService: CryptoFunctionService,
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private stateService: StateService,
|
|
||||||
private appIdService: AppIdService,
|
private appIdService: AppIdService,
|
||||||
private devicesApiService: DevicesApiServiceAbstraction,
|
private devicesApiService: DevicesApiServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private secureStorageService: AbstractStorageService,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
||||||
@ -46,24 +69,44 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||||||
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
||||||
* Note: this value should only be used once and then reset
|
* Note: this value should only be used once and then reset
|
||||||
*/
|
*/
|
||||||
async getShouldTrustDevice(): Promise<boolean> {
|
async getShouldTrustDevice(userId: UserId): Promise<boolean> {
|
||||||
return await this.stateService.getShouldTrustDevice();
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot get should trust device.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTrustDevice = await firstValueFrom(
|
||||||
|
this.stateProvider.getUserState$(SHOULD_TRUST_DEVICE, userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldTrustDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setShouldTrustDevice(value: boolean): Promise<void> {
|
async setShouldTrustDevice(userId: UserId, value: boolean): Promise<void> {
|
||||||
await this.stateService.setShouldTrustDevice(value);
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot set should trust device.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(SHOULD_TRUST_DEVICE, value, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async trustDeviceIfRequired(): Promise<void> {
|
async trustDeviceIfRequired(userId: UserId): Promise<void> {
|
||||||
const shouldTrustDevice = await this.getShouldTrustDevice();
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot trust device if required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTrustDevice = await this.getShouldTrustDevice(userId);
|
||||||
if (shouldTrustDevice) {
|
if (shouldTrustDevice) {
|
||||||
await this.trustDevice();
|
await this.trustDevice(userId);
|
||||||
// reset the trust choice
|
// reset the trust choice
|
||||||
await this.setShouldTrustDevice(false);
|
await this.setShouldTrustDevice(userId, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async trustDevice(): Promise<DeviceResponse> {
|
async trustDevice(userId: UserId): Promise<DeviceResponse> {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot trust device.");
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to get user key
|
// Attempt to get user key
|
||||||
const userKey: UserKey = await this.cryptoService.getUserKey();
|
const userKey: UserKey = await this.cryptoService.getUserKey();
|
||||||
|
|
||||||
@ -104,15 +147,23 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||||||
);
|
);
|
||||||
|
|
||||||
// store device key in local/secure storage if enc keys posted to server successfully
|
// store device key in local/secure storage if enc keys posted to server successfully
|
||||||
await this.setDeviceKey(deviceKey);
|
await this.setDeviceKey(userId, deviceKey);
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
|
||||||
|
|
||||||
return deviceResponse;
|
return deviceResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rotateDevicesTrust(newUserKey: UserKey, masterPasswordHash: string): Promise<void> {
|
async rotateDevicesTrust(
|
||||||
const currentDeviceKey = await this.getDeviceKey();
|
userId: UserId,
|
||||||
|
newUserKey: UserKey,
|
||||||
|
masterPasswordHash: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot rotate device's trust.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDeviceKey = await this.getDeviceKey(userId);
|
||||||
if (currentDeviceKey == null) {
|
if (currentDeviceKey == null) {
|
||||||
// If the current device doesn't have a device key available to it, then we can't
|
// If the current device doesn't have a device key available to it, then we can't
|
||||||
// rotate any trust at all, so early return.
|
// rotate any trust at all, so early return.
|
||||||
@ -165,26 +216,59 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||||||
await this.devicesApiService.updateTrust(trustRequest, deviceIdentifier);
|
await this.devicesApiService.updateTrust(trustRequest, deviceIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeviceKey(): Promise<DeviceKey> {
|
async getDeviceKey(userId: UserId): Promise<DeviceKey | null> {
|
||||||
return await this.stateService.getDeviceKey();
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot get device key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.platformSupportsSecureStorage) {
|
||||||
|
const deviceKeyB64 = await this.secureStorageService.get<
|
||||||
|
ReturnType<SymmetricCryptoKey["toJSON"]>
|
||||||
|
>(`${userId}${this.deviceKeySecureStorageKey}`, this.getSecureStorageOptions(userId));
|
||||||
|
|
||||||
|
const deviceKey = SymmetricCryptoKey.fromJSON(deviceKeyB64) as DeviceKey;
|
||||||
|
|
||||||
|
return deviceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceKey = await firstValueFrom(this.stateProvider.getUserState$(DEVICE_KEY, userId));
|
||||||
|
|
||||||
|
return deviceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setDeviceKey(deviceKey: DeviceKey | null): Promise<void> {
|
private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void> {
|
||||||
await this.stateService.setDeviceKey(deviceKey);
|
if (!userId) {
|
||||||
|
throw new Error("UserId is required. Cannot set device key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.platformSupportsSecureStorage) {
|
||||||
|
await this.secureStorageService.save<DeviceKey>(
|
||||||
|
`${userId}${this.deviceKeySecureStorageKey}`,
|
||||||
|
deviceKey,
|
||||||
|
this.getSecureStorageOptions(userId),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async makeDeviceKey(): Promise<DeviceKey> {
|
private async makeDeviceKey(): Promise<DeviceKey> {
|
||||||
// Create 512-bit device key
|
// Create 512-bit device key
|
||||||
return (await this.keyGenerationService.createKey(512)) as DeviceKey;
|
const deviceKey = (await this.keyGenerationService.createKey(512)) as DeviceKey;
|
||||||
|
|
||||||
|
return deviceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptUserKeyWithDeviceKey(
|
async decryptUserKeyWithDeviceKey(
|
||||||
|
userId: UserId,
|
||||||
encryptedDevicePrivateKey: EncString,
|
encryptedDevicePrivateKey: EncString,
|
||||||
encryptedUserKey: EncString,
|
encryptedUserKey: EncString,
|
||||||
deviceKey?: DeviceKey,
|
deviceKey: DeviceKey,
|
||||||
): Promise<UserKey | null> {
|
): Promise<UserKey | null> {
|
||||||
// If device key provided use it, otherwise try to retrieve from storage
|
if (!userId) {
|
||||||
deviceKey ||= await this.getDeviceKey();
|
throw new Error("UserId is required. Cannot decrypt user key with device key.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!deviceKey) {
|
if (!deviceKey) {
|
||||||
// User doesn't have a device key anymore so device is untrusted
|
// User doesn't have a device key anymore so device is untrusted
|
||||||
@ -207,9 +291,17 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||||||
return new SymmetricCryptoKey(userKey) as UserKey;
|
return new SymmetricCryptoKey(userKey) as UserKey;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If either decryption effort fails, we want to remove the device key
|
// If either decryption effort fails, we want to remove the device key
|
||||||
await this.setDeviceKey(null);
|
await this.setDeviceKey(userId, null);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSecureStorageOptions(userId: UserId): StorageOptions {
|
||||||
|
return {
|
||||||
|
storageLocation: StorageLocation.Disk,
|
||||||
|
useSecureStorage: true,
|
||||||
|
userId: userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import { BehaviorSubject, of } from "rxjs";
|
|||||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
|
||||||
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
|
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
|
import { FakeActiveUserState } from "../../../spec/fake-state";
|
||||||
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
import { DeviceType } from "../../enums";
|
import { DeviceType } from "../../enums";
|
||||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||||
@ -12,18 +15,26 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
|||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
|
||||||
|
import { StorageLocation } from "../../platform/enums";
|
||||||
import { EncryptionType } from "../../platform/enums/encryption-type.enum";
|
import { EncryptionType } from "../../platform/enums/encryption-type.enum";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, UserKey } from "../../types/key";
|
import { DeviceKey, UserKey } from "../../types/key";
|
||||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||||
|
|
||||||
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
|
import {
|
||||||
|
SHOULD_TRUST_DEVICE,
|
||||||
|
DEVICE_KEY,
|
||||||
|
DeviceTrustCryptoService,
|
||||||
|
} from "./device-trust-crypto.service.implementation";
|
||||||
|
|
||||||
describe("deviceTrustCryptoService", () => {
|
describe("deviceTrustCryptoService", () => {
|
||||||
let deviceTrustCryptoService: DeviceTrustCryptoService;
|
let deviceTrustCryptoService: DeviceTrustCryptoService;
|
||||||
@ -32,33 +43,34 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||||
const cryptoService = mock<CryptoService>();
|
const cryptoService = mock<CryptoService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
const stateService = mock<StateService>();
|
|
||||||
const appIdService = mock<AppIdService>();
|
const appIdService = mock<AppIdService>();
|
||||||
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
||||||
const i18nService = mock<I18nService>();
|
const i18nService = mock<I18nService>();
|
||||||
const platformUtilsService = mock<PlatformUtilsService>();
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
const secureStorageService = mock<AbstractStorageService>();
|
||||||
|
|
||||||
|
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||||
|
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
|
||||||
|
const deviceKeyPartialSecureStorageKey = "_deviceKey";
|
||||||
|
const deviceKeySecureStorageKey = `${mockUserId}${deviceKeyPartialSecureStorageKey}`;
|
||||||
|
|
||||||
|
const secureStorageOptions: StorageOptions = {
|
||||||
|
storageLocation: StorageLocation.Disk,
|
||||||
|
useSecureStorage: true,
|
||||||
|
userId: mockUserId,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
const supportsSecureStorage = false; // default to false; tests will override as needed
|
||||||
decryptionOptions.next({} as any);
|
// By default all the tests will have a mocked active user in state provider.
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions;
|
deviceTrustCryptoService = createDeviceTrustCryptoService(mockUserId, supportsSecureStorage);
|
||||||
|
|
||||||
deviceTrustCryptoService = new DeviceTrustCryptoService(
|
|
||||||
keyGenerationService,
|
|
||||||
cryptoFunctionService,
|
|
||||||
cryptoService,
|
|
||||||
encryptService,
|
|
||||||
stateService,
|
|
||||||
appIdService,
|
|
||||||
devicesApiService,
|
|
||||||
i18nService,
|
|
||||||
platformUtilsService,
|
|
||||||
userDecryptionOptionsService,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("instantiates", () => {
|
it("instantiates", () => {
|
||||||
@ -67,27 +79,26 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
|
|
||||||
describe("User Trust Device Choice For Decryption", () => {
|
describe("User Trust Device Choice For Decryption", () => {
|
||||||
describe("getShouldTrustDevice", () => {
|
describe("getShouldTrustDevice", () => {
|
||||||
it("gets the user trust device choice for decryption from the state service", async () => {
|
it("gets the user trust device choice for decryption", async () => {
|
||||||
const stateSvcGetShouldTrustDeviceSpy = jest.spyOn(stateService, "getShouldTrustDevice");
|
const newValue = true;
|
||||||
|
|
||||||
const expectedValue = true;
|
await stateProvider.setUserState(SHOULD_TRUST_DEVICE, newValue, mockUserId);
|
||||||
stateSvcGetShouldTrustDeviceSpy.mockResolvedValue(expectedValue);
|
|
||||||
const result = await deviceTrustCryptoService.getShouldTrustDevice();
|
|
||||||
|
|
||||||
expect(stateSvcGetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId);
|
||||||
expect(result).toEqual(expectedValue);
|
|
||||||
|
expect(result).toEqual(newValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setShouldTrustDevice", () => {
|
describe("setShouldTrustDevice", () => {
|
||||||
it("sets the user trust device choice for decryption in the state service", async () => {
|
it("sets the user trust device choice for decryption ", async () => {
|
||||||
const stateSvcSetShouldTrustDeviceSpy = jest.spyOn(stateService, "setShouldTrustDevice");
|
await stateProvider.setUserState(SHOULD_TRUST_DEVICE, false, mockUserId);
|
||||||
|
|
||||||
const newValue = true;
|
const newValue = true;
|
||||||
await deviceTrustCryptoService.setShouldTrustDevice(newValue);
|
await deviceTrustCryptoService.setShouldTrustDevice(mockUserId, newValue);
|
||||||
|
|
||||||
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId);
|
||||||
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledWith(newValue);
|
expect(result).toEqual(newValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -98,11 +109,11 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse);
|
jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse);
|
||||||
jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue();
|
jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue();
|
||||||
|
|
||||||
await deviceTrustCryptoService.trustDeviceIfRequired();
|
await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId);
|
||||||
|
|
||||||
expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1);
|
expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1);
|
||||||
expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1);
|
expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1);
|
||||||
expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(false);
|
expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not trust device nor reset when getShouldTrustDevice returns false", async () => {
|
it("should not trust device nor reset when getShouldTrustDevice returns false", async () => {
|
||||||
@ -112,7 +123,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice");
|
const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice");
|
||||||
const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice");
|
const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice");
|
||||||
|
|
||||||
await deviceTrustCryptoService.trustDeviceIfRequired();
|
await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId);
|
||||||
|
|
||||||
expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(trustDeviceSpy).not.toHaveBeenCalled();
|
expect(trustDeviceSpy).not.toHaveBeenCalled();
|
||||||
@ -126,53 +137,140 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
|
|
||||||
describe("getDeviceKey", () => {
|
describe("getDeviceKey", () => {
|
||||||
let existingDeviceKey: DeviceKey;
|
let existingDeviceKey: DeviceKey;
|
||||||
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
|
let existingDeviceKeyB64: { keyB64: string };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
existingDeviceKey = new SymmetricCryptoKey(
|
existingDeviceKey = new SymmetricCryptoKey(
|
||||||
new Uint8Array(deviceKeyBytesLength) as CsprngArray,
|
new Uint8Array(deviceKeyBytesLength) as CsprngArray,
|
||||||
) as DeviceKey;
|
) as DeviceKey;
|
||||||
|
|
||||||
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
|
existingDeviceKeyB64 = existingDeviceKey.toJSON();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when there is not an existing device key", async () => {
|
describe("Secure Storage not supported", () => {
|
||||||
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
|
it("returns null when there is not an existing device key", async () => {
|
||||||
|
await stateProvider.setUserState(DEVICE_KEY, null, mockUserId);
|
||||||
|
|
||||||
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
|
const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId);
|
||||||
|
|
||||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
expect(deviceKey).toBeNull();
|
||||||
|
expect(secureStorageService.get).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
expect(deviceKey).toBeNull();
|
it("returns the device key when there is an existing device key", async () => {
|
||||||
|
await stateProvider.setUserState(DEVICE_KEY, existingDeviceKey, mockUserId);
|
||||||
|
|
||||||
|
const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId);
|
||||||
|
|
||||||
|
expect(deviceKey).not.toBeNull();
|
||||||
|
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||||
|
expect(deviceKey).toEqual(existingDeviceKey);
|
||||||
|
expect(secureStorageService.get).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the device key when there is an existing device key", async () => {
|
describe("Secure Storage supported", () => {
|
||||||
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
|
beforeEach(() => {
|
||||||
|
const supportsSecureStorage = true;
|
||||||
|
deviceTrustCryptoService = createDeviceTrustCryptoService(
|
||||||
|
mockUserId,
|
||||||
|
supportsSecureStorage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
|
it("returns null when there is not an existing device key for the passed in user id", async () => {
|
||||||
|
secureStorageService.get.mockResolvedValue(null);
|
||||||
|
|
||||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
// Act
|
||||||
|
const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId);
|
||||||
|
|
||||||
expect(deviceKey).not.toBeNull();
|
// Assert
|
||||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
expect(deviceKey).toBeNull();
|
||||||
expect(deviceKey).toEqual(existingDeviceKey);
|
});
|
||||||
|
|
||||||
|
it("returns the device key when there is an existing device key for the passed in user id", async () => {
|
||||||
|
// Arrange
|
||||||
|
secureStorageService.get.mockResolvedValue(existingDeviceKeyB64);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(deviceKey).not.toBeNull();
|
||||||
|
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||||
|
expect(deviceKey).toEqual(existingDeviceKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error when no user id is passed in", async () => {
|
||||||
|
await expect(deviceTrustCryptoService.getDeviceKey(null)).rejects.toThrow(
|
||||||
|
"UserId is required. Cannot get device key.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setDeviceKey", () => {
|
describe("setDeviceKey", () => {
|
||||||
it("sets the device key in the state service", async () => {
|
describe("Secure Storage not supported", () => {
|
||||||
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
|
it("successfully sets the device key in state provider", async () => {
|
||||||
|
await stateProvider.setUserState(DEVICE_KEY, null, mockUserId);
|
||||||
|
|
||||||
const deviceKey = new SymmetricCryptoKey(
|
const newDeviceKey = new SymmetricCryptoKey(
|
||||||
|
new Uint8Array(deviceKeyBytesLength) as CsprngArray,
|
||||||
|
) as DeviceKey;
|
||||||
|
|
||||||
|
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||||
|
// This is a hacky workaround, but it allows for cleaner tests
|
||||||
|
await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenLastCalledWith(
|
||||||
|
DEVICE_KEY,
|
||||||
|
newDeviceKey.toJSON(),
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Secure Storage supported", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const supportsSecureStorage = true;
|
||||||
|
deviceTrustCryptoService = createDeviceTrustCryptoService(
|
||||||
|
mockUserId,
|
||||||
|
supportsSecureStorage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("successfully sets the device key in secure storage", async () => {
|
||||||
|
// Arrange
|
||||||
|
await stateProvider.setUserState(DEVICE_KEY, null, mockUserId);
|
||||||
|
|
||||||
|
secureStorageService.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const newDeviceKey = new SymmetricCryptoKey(
|
||||||
|
new Uint8Array(deviceKeyBytesLength) as CsprngArray,
|
||||||
|
) as DeviceKey;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||||
|
// This is a hacky workaround, but it allows for cleaner tests
|
||||||
|
await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stateProvider.mock.setUserState).not.toHaveBeenCalledTimes(2);
|
||||||
|
expect(secureStorageService.save).toHaveBeenCalledWith(
|
||||||
|
deviceKeySecureStorageKey,
|
||||||
|
newDeviceKey,
|
||||||
|
secureStorageOptions,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error when a null user id is passed in", async () => {
|
||||||
|
const newDeviceKey = new SymmetricCryptoKey(
|
||||||
new Uint8Array(deviceKeyBytesLength) as CsprngArray,
|
new Uint8Array(deviceKeyBytesLength) as CsprngArray,
|
||||||
) as DeviceKey;
|
) as DeviceKey;
|
||||||
|
|
||||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
await expect(
|
||||||
// This is a hacky workaround, but it allows for cleaner tests
|
(deviceTrustCryptoService as any).setDeviceKey(null, newDeviceKey),
|
||||||
await (deviceTrustCryptoService as any).setDeviceKey(deviceKey);
|
).rejects.toThrow("UserId is required. Cannot set device key.");
|
||||||
|
|
||||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -300,7 +398,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
|
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
|
||||||
const response = await deviceTrustCryptoService.trustDevice();
|
const response = await deviceTrustCryptoService.trustDevice(mockUserId);
|
||||||
|
|
||||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||||
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
|
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
|
||||||
@ -331,7 +429,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
// setup the spy to return null
|
// setup the spy to return null
|
||||||
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
|
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
|
||||||
// check if the expected error is thrown
|
// check if the expected error is thrown
|
||||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
|
await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(
|
||||||
"User symmetric key not found",
|
"User symmetric key not found",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -341,7 +439,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
// setup the spy to return undefined
|
// setup the spy to return undefined
|
||||||
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
|
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
|
||||||
// check if the expected error is thrown
|
// check if the expected error is thrown
|
||||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
|
await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(
|
||||||
"User symmetric key not found",
|
"User symmetric key not found",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -381,7 +479,9 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
it(`throws an error if ${method} fails`, async () => {
|
it(`throws an error if ${method} fails`, async () => {
|
||||||
const methodSpy = spy();
|
const methodSpy = spy();
|
||||||
methodSpy.mockRejectedValue(new Error(errorText));
|
methodSpy.mockRejectedValue(new Error(errorText));
|
||||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(errorText);
|
await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(
|
||||||
|
errorText,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([null, undefined])(
|
test.each([null, undefined])(
|
||||||
@ -389,11 +489,17 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
async (invalidValue) => {
|
async (invalidValue) => {
|
||||||
const methodSpy = spy();
|
const methodSpy = spy();
|
||||||
methodSpy.mockResolvedValue(invalidValue);
|
methodSpy.mockResolvedValue(invalidValue);
|
||||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow();
|
await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("throws an error when a null user id is passed in", async () => {
|
||||||
|
await expect(deviceTrustCryptoService.trustDevice(null)).rejects.toThrow(
|
||||||
|
"UserId is required. Cannot trust device.",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("decryptUserKeyWithDeviceKey", () => {
|
describe("decryptUserKeyWithDeviceKey", () => {
|
||||||
@ -422,19 +528,26 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when device key isn't provided and isn't in state", async () => {
|
it("throws an error when a null user id is passed in", async () => {
|
||||||
const getDeviceKeySpy = jest
|
await expect(
|
||||||
.spyOn(deviceTrustCryptoService, "getDeviceKey")
|
deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||||
.mockResolvedValue(null);
|
null,
|
||||||
|
mockEncryptedDevicePrivateKey,
|
||||||
|
mockEncryptedUserKey,
|
||||||
|
mockDeviceKey,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("UserId is required. Cannot decrypt user key with device key.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when device key isn't provided", async () => {
|
||||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||||
|
mockUserId,
|
||||||
mockEncryptedDevicePrivateKey,
|
mockEncryptedDevicePrivateKey,
|
||||||
mockEncryptedUserKey,
|
mockEncryptedUserKey,
|
||||||
|
mockDeviceKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
|
|
||||||
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("successfully returns the user key when provided keys (including device key) can decrypt it", async () => {
|
it("successfully returns the user key when provided keys (including device key) can decrypt it", async () => {
|
||||||
@ -446,6 +559,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||||
|
|
||||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||||
|
mockUserId,
|
||||||
mockEncryptedDevicePrivateKey,
|
mockEncryptedDevicePrivateKey,
|
||||||
mockEncryptedUserKey,
|
mockEncryptedUserKey,
|
||||||
mockDeviceKey,
|
mockDeviceKey,
|
||||||
@ -456,31 +570,6 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
|
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("successfully returns the user key when a device key is not provided (retrieves device key from state)", async () => {
|
|
||||||
const getDeviceKeySpy = jest
|
|
||||||
.spyOn(deviceTrustCryptoService, "getDeviceKey")
|
|
||||||
.mockResolvedValue(mockDeviceKey);
|
|
||||||
|
|
||||||
const decryptToBytesSpy = jest
|
|
||||||
.spyOn(encryptService, "decryptToBytes")
|
|
||||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
|
||||||
const rsaDecryptSpy = jest
|
|
||||||
.spyOn(cryptoService, "rsaDecrypt")
|
|
||||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
|
||||||
|
|
||||||
// Call without providing a device key
|
|
||||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
|
||||||
mockEncryptedDevicePrivateKey,
|
|
||||||
mockEncryptedUserKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockUserKey);
|
|
||||||
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null and removes device key when the decryption fails", async () => {
|
it("returns null and removes device key when the decryption fails", async () => {
|
||||||
const decryptToBytesSpy = jest
|
const decryptToBytesSpy = jest
|
||||||
.spyOn(encryptService, "decryptToBytes")
|
.spyOn(encryptService, "decryptToBytes")
|
||||||
@ -488,6 +577,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey");
|
const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey");
|
||||||
|
|
||||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||||
|
mockUserId,
|
||||||
mockEncryptedDevicePrivateKey,
|
mockEncryptedDevicePrivateKey,
|
||||||
mockEncryptedUserKey,
|
mockEncryptedUserKey,
|
||||||
mockDeviceKey,
|
mockDeviceKey,
|
||||||
@ -496,7 +586,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
|
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(setDeviceKeySpy).toHaveBeenCalledTimes(1);
|
expect(setDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||||
expect(setDeviceKeySpy).toHaveBeenCalledWith(null);
|
expect(setDeviceKeySpy).toHaveBeenCalledWith(mockUserId, null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -514,19 +604,28 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
cryptoService.activeUserKey$ = of(fakeNewUserKey);
|
cryptoService.activeUserKey$ = of(fakeNewUserKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does an early exit when the current device is not a trusted device", async () => {
|
it("throws an error when a null user id is passed in", async () => {
|
||||||
stateService.getDeviceKey.mockResolvedValue(null);
|
await expect(
|
||||||
|
deviceTrustCryptoService.rotateDevicesTrust(null, fakeNewUserKey, ""),
|
||||||
|
).rejects.toThrow("UserId is required. Cannot rotate device's trust.");
|
||||||
|
});
|
||||||
|
|
||||||
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "");
|
it("does an early exit when the current device is not a trusted device", async () => {
|
||||||
|
const deviceKeyState: FakeActiveUserState<DeviceKey> =
|
||||||
|
stateProvider.activeUser.getFake(DEVICE_KEY);
|
||||||
|
deviceKeyState.nextState(null);
|
||||||
|
|
||||||
|
await deviceTrustCryptoService.rotateDevicesTrust(mockUserId, fakeNewUserKey, "");
|
||||||
|
|
||||||
expect(devicesApiService.updateTrust).not.toHaveBeenCalled();
|
expect(devicesApiService.updateTrust).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("is on a trusted device", () => {
|
describe("is on a trusted device", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
stateService.getDeviceKey.mockResolvedValue(
|
const mockDeviceKey = new SymmetricCryptoKey(
|
||||||
new SymmetricCryptoKey(new Uint8Array(deviceKeyBytesLength)) as DeviceKey,
|
new Uint8Array(deviceKeyBytesLength),
|
||||||
);
|
) as DeviceKey;
|
||||||
|
await stateProvider.setUserState(DEVICE_KEY, mockDeviceKey, mockUserId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rotates current device keys and calls api service when the current device is trusted", async () => {
|
it("rotates current device keys and calls api service when the current device is trusted", async () => {
|
||||||
@ -592,7 +691,11 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "my_password_hash");
|
await deviceTrustCryptoService.rotateDevicesTrust(
|
||||||
|
mockUserId,
|
||||||
|
fakeNewUserKey,
|
||||||
|
"my_password_hash",
|
||||||
|
);
|
||||||
|
|
||||||
expect(devicesApiService.updateTrust).toHaveBeenCalledWith(
|
expect(devicesApiService.updateTrust).toHaveBeenCalledWith(
|
||||||
matches((updateTrustModel: UpdateDevicesTrustRequest) => {
|
matches((updateTrustModel: UpdateDevicesTrustRequest) => {
|
||||||
@ -608,4 +711,32 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
function createDeviceTrustCryptoService(
|
||||||
|
mockUserId: UserId | null,
|
||||||
|
supportsSecureStorage: boolean,
|
||||||
|
) {
|
||||||
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
|
platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage);
|
||||||
|
|
||||||
|
decryptionOptions.next({} as any);
|
||||||
|
userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions;
|
||||||
|
|
||||||
|
return new DeviceTrustCryptoService(
|
||||||
|
keyGenerationService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
cryptoService,
|
||||||
|
encryptService,
|
||||||
|
appIdService,
|
||||||
|
devicesApiService,
|
||||||
|
i18nService,
|
||||||
|
platformUtilsService,
|
||||||
|
stateProvider,
|
||||||
|
secureStorageService,
|
||||||
|
userDecryptionOptionsService,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
|||||||
import { SendData } from "../../tools/send/models/data/send.data";
|
import { SendData } from "../../tools/send/models/data/send.data";
|
||||||
import { SendView } from "../../tools/send/models/view/send.view";
|
import { SendView } from "../../tools/send/models/view/send.view";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, MasterKey } from "../../types/key";
|
import { MasterKey } from "../../types/key";
|
||||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
@ -161,15 +161,11 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
||||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
|
|
||||||
setDeviceKey: (value: DeviceKey | null, options?: StorageOptions) => Promise<void>;
|
|
||||||
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
|
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
|
||||||
setAdminAuthRequest: (
|
setAdminAuthRequest: (
|
||||||
adminAuthRequest: AdminAuthRequestStorable,
|
adminAuthRequest: AdminAuthRequestStorable,
|
||||||
options?: StorageOptions,
|
options?: StorageOptions,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
|
|
||||||
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { makeStaticByteArray } from "../../../../spec";
|
import { makeStaticByteArray } from "../../../../spec";
|
||||||
import { CsprngArray } from "../../../types/csprng";
|
|
||||||
import { DeviceKey } from "../../../types/key";
|
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
import { AccountKeys, EncryptionPair } from "./account";
|
import { AccountKeys, EncryptionPair } from "./account";
|
||||||
@ -24,23 +22,6 @@ describe("AccountKeys", () => {
|
|||||||
const json = JSON.stringify(keys);
|
const json = JSON.stringify(keys);
|
||||||
expect(json).toContain('"publicKey":"hello"');
|
expect(json).toContain('"publicKey":"hello"');
|
||||||
});
|
});
|
||||||
|
|
||||||
// As the accountKeys.toJSON doesn't really serialize the device key
|
|
||||||
// this method just checks the persistence of the deviceKey
|
|
||||||
it("should persist deviceKey", () => {
|
|
||||||
// Arrange
|
|
||||||
const accountKeys = new AccountKeys();
|
|
||||||
const deviceKeyBytesLength = 64;
|
|
||||||
accountKeys.deviceKey = new SymmetricCryptoKey(
|
|
||||||
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray,
|
|
||||||
) as DeviceKey;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const serializedKeys = accountKeys.toJSON();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(serializedKeys.deviceKey).toEqual(accountKeys.deviceKey);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fromJSON", () => {
|
describe("fromJSON", () => {
|
||||||
@ -64,24 +45,5 @@ describe("AccountKeys", () => {
|
|||||||
} as any);
|
} as any);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deserialize deviceKey", () => {
|
|
||||||
// Arrange
|
|
||||||
const expectedKeyB64 =
|
|
||||||
"ZJNnhx9BbJeb2EAq1hlMjqt6GFsg9G/GzoFf6SbPKsaiMhKGDcbHcwcyEg56Lh8lfilpZz4SRM6UA7oFCg+lSg==";
|
|
||||||
|
|
||||||
const symmetricCryptoKeyFromJsonSpy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const accountKeys = AccountKeys.fromJSON({
|
|
||||||
deviceKey: {
|
|
||||||
keyB64: expectedKeyB64,
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(symmetricCryptoKeyFromJsonSpy).toHaveBeenCalled();
|
|
||||||
expect(accountKeys.deviceKey.keyB64).toEqual(expectedKeyB64);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -95,7 +95,6 @@ export class AccountData {
|
|||||||
export class AccountKeys {
|
export class AccountKeys {
|
||||||
masterKey?: MasterKey;
|
masterKey?: MasterKey;
|
||||||
masterKeyEncryptedUserKey?: string;
|
masterKeyEncryptedUserKey?: string;
|
||||||
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
|
|
||||||
publicKey?: Uint8Array;
|
publicKey?: Uint8Array;
|
||||||
|
|
||||||
/** @deprecated July 2023, left for migration purposes*/
|
/** @deprecated July 2023, left for migration purposes*/
|
||||||
@ -125,7 +124,6 @@ export class AccountKeys {
|
|||||||
}
|
}
|
||||||
return Object.assign(new AccountKeys(), obj, {
|
return Object.assign(new AccountKeys(), obj, {
|
||||||
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
|
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
|
||||||
deviceKey: obj?.deviceKey,
|
|
||||||
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
|
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
|
||||||
cryptoSymmetricKey: EncryptionPair.fromJSON(
|
cryptoSymmetricKey: EncryptionPair.fromJSON(
|
||||||
obj?.cryptoSymmetricKey,
|
obj?.cryptoSymmetricKey,
|
||||||
@ -185,7 +183,6 @@ export class AccountSettings {
|
|||||||
vaultTimeout?: number;
|
vaultTimeout?: number;
|
||||||
vaultTimeoutAction?: string = "lock";
|
vaultTimeoutAction?: string = "lock";
|
||||||
approveLoginRequests?: boolean;
|
approveLoginRequests?: boolean;
|
||||||
trustDeviceChoiceForDecryption?: boolean;
|
|
||||||
|
|
||||||
/** @deprecated July 2023, left for migration purposes*/
|
/** @deprecated July 2023, left for migration purposes*/
|
||||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||||
|
@ -14,7 +14,7 @@ import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
|||||||
import { SendData } from "../../tools/send/models/data/send.data";
|
import { SendData } from "../../tools/send/models/data/send.data";
|
||||||
import { SendView } from "../../tools/send/models/view/send.view";
|
import { SendView } from "../../tools/send/models/view/send.view";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, MasterKey } from "../../types/key";
|
import { MasterKey } from "../../types/key";
|
||||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
@ -650,39 +650,6 @@ export class StateService<
|
|||||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
: 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);
|
|
||||||
|
|
||||||
const existingDeviceKey = account?.keys?.deviceKey;
|
|
||||||
|
|
||||||
// Must manually instantiate the SymmetricCryptoKey class from the JSON object
|
|
||||||
if (existingDeviceKey != null) {
|
|
||||||
return SymmetricCryptoKey.fromJSON(existingDeviceKey) as DeviceKey;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDeviceKey(value: DeviceKey | null, 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?.toJSON() ?? null;
|
|
||||||
|
|
||||||
await this.saveAccount(account, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> {
|
async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> {
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||||
|
|
||||||
@ -714,31 +681,6 @@ export class StateService<
|
|||||||
await this.saveAccount(account, options);
|
await this.saveAccount(account, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShouldTrustDevice(options?: StorageOptions): Promise<boolean | null> {
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
|
||||||
|
|
||||||
if (options?.userId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = await this.getAccount(options);
|
|
||||||
|
|
||||||
return account?.settings?.trustDeviceChoiceForDecryption ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setShouldTrustDevice(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
|
||||||
if (options?.userId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = await this.getAccount(options);
|
|
||||||
|
|
||||||
account.settings.trustDeviceChoiceForDecryption = value;
|
|
||||||
|
|
||||||
await this.saveAccount(account, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEmail(options?: StorageOptions): Promise<string> {
|
async getEmail(options?: StorageOptions): Promise<string> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
@ -1633,7 +1575,6 @@ export class StateService<
|
|||||||
protected resetAccount(account: TAccount) {
|
protected resetAccount(account: TAccount) {
|
||||||
const persistentAccountInformation = {
|
const persistentAccountInformation = {
|
||||||
settings: account.settings,
|
settings: account.settings,
|
||||||
keys: { deviceKey: account.keys.deviceKey },
|
|
||||||
adminAuthRequest: account.adminAuthRequest,
|
adminAuthRequest: account.adminAuthRequest,
|
||||||
};
|
};
|
||||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||||
|
@ -48,6 +48,9 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
|||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||||
|
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
|
||||||
|
web: "disk-local",
|
||||||
|
});
|
||||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||||
|
|
||||||
// Autofill
|
// Autofill
|
||||||
|
@ -49,6 +49,7 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-
|
|||||||
import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider";
|
import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider";
|
||||||
import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers";
|
import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers";
|
||||||
import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version";
|
import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version";
|
||||||
|
import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers";
|
||||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||||
@ -56,8 +57,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 52;
|
export const CURRENT_VERSION = 53;
|
||||||
|
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@ -111,7 +111,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(AccountServerConfigMigrator, 48, 49)
|
.with(AccountServerConfigMigrator, 48, 49)
|
||||||
.with(KeyConnectorMigrator, 49, 50)
|
.with(KeyConnectorMigrator, 49, 50)
|
||||||
.with(RememberedEmailMigrator, 50, 51)
|
.with(RememberedEmailMigrator, 50, 51)
|
||||||
.with(DeleteInstalledVersion, 51, CURRENT_VERSION);
|
.with(DeleteInstalledVersion, 51, 52)
|
||||||
|
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
import { MockProxy, any } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEVICE_KEY,
|
||||||
|
DeviceTrustCryptoServiceStateProviderMigrator,
|
||||||
|
SHOULD_TRUST_DEVICE,
|
||||||
|
} from "./53-migrate-device-trust-crypto-svc-to-state-providers";
|
||||||
|
|
||||||
|
// Represents data in state service pre-migration
|
||||||
|
function preMigrationJson() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user1", "user2", "user3"],
|
||||||
|
user1: {
|
||||||
|
keys: {
|
||||||
|
deviceKey: {
|
||||||
|
keyB64: "user1_deviceKey",
|
||||||
|
},
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
trustDeviceChoiceForDecryption: true,
|
||||||
|
otherStuff: "overStuff3",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
keys: {
|
||||||
|
// no device key
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
// no trust device choice
|
||||||
|
otherStuff: "overStuff6",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff7",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
// use pattern user_{userId}_{stateDefinitionName}_{keyDefinitionKey} for each user
|
||||||
|
// User1 migrated data
|
||||||
|
user_user1_deviceTrust_deviceKey: {
|
||||||
|
keyB64: "user1_deviceKey",
|
||||||
|
},
|
||||||
|
user_user1_deviceTrust_shouldTrustDevice: true,
|
||||||
|
|
||||||
|
// User2 does not have migrated data
|
||||||
|
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user1", "user2", "user3"],
|
||||||
|
user1: {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
otherStuff: "overStuff3",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
otherStuff: "overStuff6",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff6",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DeviceTrustCryptoServiceStateProviderMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: DeviceTrustCryptoServiceStateProviderMigrator;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(preMigrationJson(), 52);
|
||||||
|
sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53);
|
||||||
|
});
|
||||||
|
|
||||||
|
// it should remove deviceKey and trustDeviceChoiceForDecryption from all accounts
|
||||||
|
it("should remove deviceKey and trustDeviceChoiceForDecryption from all accounts that have it", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||||
|
keys: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
otherStuff: "overStuff3",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should migrate deviceKey and trustDeviceChoiceForDecryption to state providers for accounts that have the data", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user1", DEVICE_KEY, {
|
||||||
|
keyB64: "user1_deviceKey",
|
||||||
|
});
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user1", SHOULD_TRUST_DEVICE, true);
|
||||||
|
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", DEVICE_KEY, any());
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", SHOULD_TRUST_DEVICE, any());
|
||||||
|
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", DEVICE_KEY, any());
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", SHOULD_TRUST_DEVICE, any());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 53);
|
||||||
|
sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should null out newly migrated entries in state provider framework", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user1", DEVICE_KEY, null);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user1", SHOULD_TRUST_DEVICE, null);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user2", DEVICE_KEY, null);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user2", SHOULD_TRUST_DEVICE, null);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user3", DEVICE_KEY, null);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user3", SHOULD_TRUST_DEVICE, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add back deviceKey and trustDeviceChoiceForDecryption to all accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||||
|
keys: {
|
||||||
|
deviceKey: {
|
||||||
|
keyB64: "user1_deviceKey",
|
||||||
|
},
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
trustDeviceChoiceForDecryption: true,
|
||||||
|
otherStuff: "overStuff3",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add data back if data wasn't migrated or acct doesn't exist", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
// no data to add back for user2 (acct exists but no migrated data) and user3 (no acct)
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,95 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
// Types to represent data as it is stored in JSON
|
||||||
|
type DeviceKeyJsonType = {
|
||||||
|
keyB64: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
keys?: {
|
||||||
|
deviceKey?: DeviceKeyJsonType;
|
||||||
|
};
|
||||||
|
settings?: {
|
||||||
|
trustDeviceChoiceForDecryption?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEVICE_KEY: KeyDefinitionLike = {
|
||||||
|
key: "deviceKey", // matches KeyDefinition.key in DeviceTrustCryptoService
|
||||||
|
stateDefinition: {
|
||||||
|
name: "deviceTrust", // matches StateDefinition.name in StateDefinitions
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SHOULD_TRUST_DEVICE: KeyDefinitionLike = {
|
||||||
|
key: "shouldTrustDevice",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "deviceTrust",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DeviceTrustCryptoServiceStateProviderMigrator extends Migrator<52, 53> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
let updatedAccount = false;
|
||||||
|
|
||||||
|
// Migrate deviceKey
|
||||||
|
const existingDeviceKey = account?.keys?.deviceKey;
|
||||||
|
|
||||||
|
if (existingDeviceKey != null) {
|
||||||
|
// Only migrate data that exists
|
||||||
|
await helper.setToUser(userId, DEVICE_KEY, existingDeviceKey);
|
||||||
|
delete account.keys.deviceKey;
|
||||||
|
updatedAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate shouldTrustDevice
|
||||||
|
const existingShouldTrustDevice = account?.settings?.trustDeviceChoiceForDecryption;
|
||||||
|
|
||||||
|
if (existingShouldTrustDevice != null) {
|
||||||
|
await helper.setToUser(userId, SHOULD_TRUST_DEVICE, existingShouldTrustDevice);
|
||||||
|
delete account.settings.trustDeviceChoiceForDecryption;
|
||||||
|
updatedAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAccount) {
|
||||||
|
// Save the migrated account
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
// Rollback deviceKey
|
||||||
|
const migratedDeviceKey: DeviceKeyJsonType = await helper.getFromUser(userId, DEVICE_KEY);
|
||||||
|
|
||||||
|
if (account?.keys && migratedDeviceKey != null) {
|
||||||
|
account.keys.deviceKey = migratedDeviceKey;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
await helper.setToUser(userId, DEVICE_KEY, null);
|
||||||
|
|
||||||
|
// Rollback shouldTrustDevice
|
||||||
|
const migratedShouldTrustDevice = await helper.getFromUser<boolean>(
|
||||||
|
userId,
|
||||||
|
SHOULD_TRUST_DEVICE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (account?.settings && migratedShouldTrustDevice != null) {
|
||||||
|
account.settings.trustDeviceChoiceForDecryption = migratedShouldTrustDevice;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
await helper.setToUser(userId, SHOULD_TRUST_DEVICE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user