mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 14:53:33 +00:00
Add state for everHadUserKey (#7208)
* Migrate ever had user key * Add DI for state providers * Add state for everHadUserKey * Use ever had user key migrator Co-authored-by: SmithThe4th <gsmithwalter@gmail.com> Co-authored-by: Carlos Gonçalves <LRNcardozoWDF@users.noreply.github.com> Co-authored-by: Jason Ng <Jcory.ng@gmail.com> * Fix test from merge * Prefer stored observables to getters getters create a new observable every time they're called, whereas one set in the constructor is created only once. * Fix another merge issue * Fix cli background build --------- Co-authored-by: SmithThe4th <gsmithwalter@gmail.com> Co-authored-by: Carlos Gonçalves <LRNcardozoWDF@users.noreply.github.com> Co-authored-by: Jason Ng <Jcory.ng@gmail.com>
This commit is contained in:
@@ -311,7 +311,6 @@ export default class MainBackground {
|
|||||||
this.memoryStorageService as BackgroundMemoryStorageService,
|
this.memoryStorageService as BackgroundMemoryStorageService,
|
||||||
this.storageService as BrowserLocalStorageService,
|
this.storageService as BrowserLocalStorageService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.encryptService = flagEnabled("multithreadDecryption")
|
this.encryptService = flagEnabled("multithreadDecryption")
|
||||||
? new MultithreadEncryptServiceImplementation(
|
? new MultithreadEncryptServiceImplementation(
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
@@ -374,13 +373,14 @@ export default class MainBackground {
|
|||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService);
|
this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService);
|
||||||
|
|
||||||
this.cryptoService = new BrowserCryptoService(
|
this.cryptoService = new BrowserCryptoService(
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
this.tokenService = new TokenService(this.stateService);
|
this.tokenService = new TokenService(this.stateService);
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.storageService);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { CryptoService as AbstractCryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as AbstractCryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AccountServiceInitOptions,
|
||||||
|
accountServiceFactory,
|
||||||
|
} from "../../../auth/background/service-factories/account-service.factory";
|
||||||
import {
|
import {
|
||||||
StateServiceInitOptions,
|
StateServiceInitOptions,
|
||||||
stateServiceFactory,
|
stateServiceFactory,
|
||||||
@@ -20,6 +24,7 @@ import {
|
|||||||
PlatformUtilsServiceInitOptions,
|
PlatformUtilsServiceInitOptions,
|
||||||
platformUtilsServiceFactory,
|
platformUtilsServiceFactory,
|
||||||
} from "./platform-utils-service.factory";
|
} from "./platform-utils-service.factory";
|
||||||
|
import { StateProviderInitOptions, stateProviderFactory } from "./state-provider.factory";
|
||||||
|
|
||||||
type CryptoServiceFactoryOptions = FactoryOptions;
|
type CryptoServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
@@ -28,7 +33,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
|
|||||||
EncryptServiceInitOptions &
|
EncryptServiceInitOptions &
|
||||||
PlatformUtilsServiceInitOptions &
|
PlatformUtilsServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
StateServiceInitOptions;
|
StateServiceInitOptions &
|
||||||
|
AccountServiceInitOptions &
|
||||||
|
StateProviderInitOptions;
|
||||||
|
|
||||||
export function cryptoServiceFactory(
|
export function cryptoServiceFactory(
|
||||||
cache: { cryptoService?: AbstractCryptoService } & CachedServices,
|
cache: { cryptoService?: AbstractCryptoService } & CachedServices,
|
||||||
@@ -45,6 +52,8 @@ export function cryptoServiceFactory(
|
|||||||
await platformUtilsServiceFactory(cache, opts),
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
|
await accountServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
UserKey,
|
UserKey,
|
||||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
export class BrowserCryptoService extends CryptoService {
|
export class BrowserCryptoService extends CryptoService {
|
||||||
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
return await this.stateService.getBiometricUnlock({ userId: userId });
|
return await this.stateService.getBiometricUnlock({ userId: userId });
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,7 @@ export class BrowserCryptoService extends CryptoService {
|
|||||||
*/
|
*/
|
||||||
protected override async getKeyFromStorage(
|
protected override async getKeyFromStorage(
|
||||||
keySuffix: KeySuffixOptions,
|
keySuffix: KeySuffixOptions,
|
||||||
userId?: string,
|
userId?: UserId,
|
||||||
): Promise<UserKey> {
|
): Promise<UserKey> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
await this.platformUtilService.authenticateBiometric();
|
await this.platformUtilService.authenticateBiometric();
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ export class Main {
|
|||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.storageService);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
|||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@@ -180,6 +181,8 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mock, mockReset } from "jest-mock-extended";
|
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
@@ -9,6 +10,12 @@ import {
|
|||||||
UserKey,
|
UserKey,
|
||||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FakeAccountService,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
} from "../../../../../libs/common/spec/fake-account-service";
|
||||||
|
|
||||||
import { ElectronCryptoService } from "./electron-crypto.service";
|
import { ElectronCryptoService } from "./electron-crypto.service";
|
||||||
import { ElectronStateService } from "./electron-state.service.abstraction";
|
import { ElectronStateService } from "./electron-state.service.abstraction";
|
||||||
@@ -21,15 +28,14 @@ describe("electronCryptoService", () => {
|
|||||||
const platformUtilService = mock<PlatformUtilsService>();
|
const platformUtilService = mock<PlatformUtilsService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const stateService = mock<ElectronStateService>();
|
const stateService = mock<ElectronStateService>();
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
|
||||||
const mockUserId = "mock user id";
|
const mockUserId = "mock user id" as UserId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(cryptoFunctionService);
|
accountService = mockAccountServiceWith("userId" as UserId);
|
||||||
mockReset(encryptService);
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
mockReset(platformUtilService);
|
|
||||||
mockReset(logService);
|
|
||||||
mockReset(stateService);
|
|
||||||
|
|
||||||
electronCryptoService = new ElectronCryptoService(
|
electronCryptoService = new ElectronCryptoService(
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
@@ -37,9 +43,15 @@ describe("electronCryptoService", () => {
|
|||||||
platformUtilService,
|
platformUtilService,
|
||||||
logService,
|
logService,
|
||||||
stateService,
|
stateService,
|
||||||
|
accountService,
|
||||||
|
stateProvider,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("instantiates", () => {
|
it("instantiates", () => {
|
||||||
expect(electronCryptoService).not.toBeFalsy();
|
expect(electronCryptoService).not.toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -11,7 +12,9 @@ import {
|
|||||||
UserKey,
|
UserKey,
|
||||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { CsprngString } from "@bitwarden/common/types/csprng";
|
import { CsprngString } from "@bitwarden/common/types/csprng";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { ElectronStateService } from "./electron-state.service.abstraction";
|
import { ElectronStateService } from "./electron-state.service.abstraction";
|
||||||
|
|
||||||
@@ -22,11 +25,21 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
protected override stateService: ElectronStateService,
|
protected override stateService: ElectronStateService,
|
||||||
|
accountService: AccountService,
|
||||||
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(cryptoFunctionService, encryptService, platformUtilsService, logService, stateService);
|
super(
|
||||||
|
cryptoFunctionService,
|
||||||
|
encryptService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
accountService,
|
||||||
|
stateProvider,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3474)
|
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3474)
|
||||||
const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId });
|
const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId });
|
||||||
@@ -35,7 +48,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
return super.hasUserKeyStored(keySuffix, userId);
|
return super.hasUserKeyStored(keySuffix, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void> {
|
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
this.stateService.setUserKeyBiometric(null, { userId: userId });
|
this.stateService.setUserKeyBiometric(null, { userId: userId });
|
||||||
this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
|
this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
|
||||||
@@ -44,7 +57,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
super.clearStoredUserKey(keySuffix, userId);
|
super.clearStoredUserKey(keySuffix, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async storeAdditionalKeys(key: UserKey, userId?: string) {
|
protected override async storeAdditionalKeys(key: UserKey, userId?: UserId) {
|
||||||
await super.storeAdditionalKeys(key, userId);
|
await super.storeAdditionalKeys(key, userId);
|
||||||
|
|
||||||
const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);
|
const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);
|
||||||
@@ -59,7 +72,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
|
|
||||||
protected override async getKeyFromStorage(
|
protected override async getKeyFromStorage(
|
||||||
keySuffix: KeySuffixOptions,
|
keySuffix: KeySuffixOptions,
|
||||||
userId?: string,
|
userId?: UserId,
|
||||||
): Promise<UserKey> {
|
): Promise<UserKey> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
await this.migrateBiometricKeyIfNeeded(userId);
|
await this.migrateBiometricKeyIfNeeded(userId);
|
||||||
@@ -69,7 +82,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
return await super.getKeyFromStorage(keySuffix, userId);
|
return await super.getKeyFromStorage(keySuffix, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async storeBiometricKey(key: UserKey, userId?: string): Promise<void> {
|
protected async storeBiometricKey(key: UserKey, userId?: UserId): Promise<void> {
|
||||||
let clientEncKeyHalf: CsprngString = null;
|
let clientEncKeyHalf: CsprngString = null;
|
||||||
if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) {
|
if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) {
|
||||||
clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
|
clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
|
||||||
@@ -80,7 +93,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId });
|
const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId });
|
||||||
return biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
return biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
||||||
@@ -88,12 +101,12 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
return await super.shouldStoreKey(keySuffix, userId);
|
return await super.shouldStoreKey(keySuffix, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async clearAllStoredUserKeys(userId?: string): Promise<void> {
|
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
||||||
super.clearAllStoredUserKeys(userId);
|
super.clearAllStoredUserKeys(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBiometricEncryptionClientKeyHalf(userId?: string): Promise<CsprngString | null> {
|
private async getBiometricEncryptionClientKeyHalf(userId?: UserId): Promise<CsprngString | null> {
|
||||||
try {
|
try {
|
||||||
let biometricKey = await this.stateService
|
let biometricKey = await this.stateService
|
||||||
.getBiometricEncryptionClientKeyHalf({ userId })
|
.getBiometricEncryptionClientKeyHalf({ userId })
|
||||||
@@ -118,7 +131,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
// These methods support migrating the old keys to the new ones.
|
// These methods support migrating the old keys to the new ones.
|
||||||
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
|
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
|
||||||
|
|
||||||
override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string) {
|
override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
|
||||||
if (keySuffix === KeySuffixOptions.Biometric) {
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
||||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||||
}
|
}
|
||||||
@@ -126,7 +139,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||||||
super.clearDeprecatedKeys(keySuffix, userId);
|
super.clearDeprecatedKeys(keySuffix, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async migrateBiometricKeyIfNeeded(userId?: string) {
|
private async migrateBiometricKeyIfNeeded(userId?: UserId) {
|
||||||
if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) {
|
if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) {
|
||||||
const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId });
|
const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId });
|
||||||
// decrypt
|
// decrypt
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
} from "@angular/router";
|
} from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
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";
|
||||||
@@ -19,6 +20,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
* Only allow access to this route if the vault is locked.
|
* Only allow access to this route if the vault is locked.
|
||||||
* If TDE is enabled then the user must also have had a user key at some point.
|
* If TDE is enabled then the user must also have had a user key at some point.
|
||||||
* Otherwise redirect to root.
|
* Otherwise redirect to root.
|
||||||
|
*
|
||||||
|
* TODO: This should return Observable<boolean | UrlTree> once we can remove all the promises
|
||||||
*/
|
*/
|
||||||
export function lockGuard(): CanActivateFn {
|
export function lockGuard(): CanActivateFn {
|
||||||
return async (
|
return async (
|
||||||
@@ -64,7 +67,7 @@ export function lockGuard(): CanActivateFn {
|
|||||||
|
|
||||||
// If authN user with TDE directly navigates to lock, kick them upwards so redirect guard can
|
// If authN user with TDE directly navigates to lock, kick them upwards so redirect guard can
|
||||||
// properly route them to the login decryption options component.
|
// properly route them to the login decryption options component.
|
||||||
const everHadUserKey = await cryptoService.getEverHadUserKey();
|
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
|
||||||
if (tdeEnabled && !everHadUserKey) {
|
if (tdeEnabled && !everHadUserKey) {
|
||||||
return router.createUrlTree(["/"]);
|
return router.createUrlTree(["/"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { inject } from "@angular/core";
|
import { inject } from "@angular/core";
|
||||||
import { CanActivateFn, Router } from "@angular/router";
|
import { CanActivateFn, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
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";
|
||||||
@@ -22,6 +23,8 @@ const defaultRoutes: RedirectRoutes = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Guard that consolidates all redirection logic, should be applied to root route.
|
* Guard that consolidates all redirection logic, should be applied to root route.
|
||||||
|
*
|
||||||
|
* TODO: This should return Observable<boolean | UrlTree> once we can get rid of all the promises
|
||||||
*/
|
*/
|
||||||
export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActivateFn {
|
export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActivateFn {
|
||||||
const routes = { ...defaultRoutes, ...overrides };
|
const routes = { ...defaultRoutes, ...overrides };
|
||||||
@@ -44,7 +47,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
|
|||||||
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
||||||
// login decryption options component.
|
// login decryption options component.
|
||||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||||
const everHadUserKey = await cryptoService.getEverHadUserKey();
|
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
|
||||||
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
|
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
|
||||||
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });
|
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
CanActivateFn,
|
CanActivateFn,
|
||||||
} from "@angular/router";
|
} from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
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";
|
||||||
@@ -14,6 +15,8 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
|||||||
/**
|
/**
|
||||||
* Only allow access to this route if the vault is locked and has never been decrypted.
|
* Only allow access to this route if the vault is locked and has never been decrypted.
|
||||||
* Otherwise redirect to root.
|
* Otherwise redirect to root.
|
||||||
|
*
|
||||||
|
* TODO: This should return Observable<boolean | UrlTree> once we can get rid of all the promises
|
||||||
*/
|
*/
|
||||||
export function tdeDecryptionRequiredGuard(): CanActivateFn {
|
export function tdeDecryptionRequiredGuard(): CanActivateFn {
|
||||||
return async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
return async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
@@ -24,7 +27,7 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn {
|
|||||||
|
|
||||||
const authStatus = await authService.getAuthStatus();
|
const authStatus = await authService.getAuthStatus();
|
||||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||||
const everHadUserKey = await cryptoService.getEverHadUserKey();
|
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
|
||||||
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
|
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
|
||||||
return router.createUrlTree(["/"]);
|
return router.createUrlTree(["/"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,8 +396,8 @@ import { ModalService } from "./modal.service";
|
|||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
DevicesApiServiceAbstraction,
|
StateProvider,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||||
@@ -29,14 +31,12 @@ export abstract class CryptoService {
|
|||||||
* kicking off a refresh of any additional keys
|
* kicking off a refresh of any additional keys
|
||||||
* (such as auto, biometrics, or pin)
|
* (such as auto, biometrics, or pin)
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* Check if the current sessions has ever had a user key, i.e. has ever been unlocked/decrypted.
|
|
||||||
* This is key for differentiating between TDE locked and standard locked states.
|
|
||||||
* @param userId The desired user
|
|
||||||
* @returns True if the current session has ever had a user key
|
|
||||||
*/
|
|
||||||
getEverHadUserKey: (userId?: string) => Promise<boolean>;
|
|
||||||
refreshAdditionalKeys: () => Promise<void>;
|
refreshAdditionalKeys: () => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Observable value that returns whether or not the currently active user has ever had auser key,
|
||||||
|
* i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states.
|
||||||
|
*/
|
||||||
|
everHadUserKey$: Observable<boolean>;
|
||||||
/**
|
/**
|
||||||
* Retrieves the user key
|
* Retrieves the user key
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
|
|||||||
@@ -393,8 +393,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||||
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
||||||
getEverHadUserKey: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setEverHadUserKey: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>;
|
getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>;
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ export class AccountProfile {
|
|||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
entityType?: string;
|
entityType?: string;
|
||||||
everHadUserKey?: boolean;
|
|
||||||
everBeenUnlocked?: boolean;
|
everBeenUnlocked?: boolean;
|
||||||
forceSetPasswordReason?: ForceSetPasswordReason;
|
forceSetPasswordReason?: ForceSetPasswordReason;
|
||||||
hasPremiumPersonally?: boolean;
|
hasPremiumPersonally?: boolean;
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { mock, mockReset } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
|
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||||
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "../abstractions/encrypt.service";
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
import { Utils } from "../misc/utils";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
import { EncString } from "../models/domain/enc-string";
|
||||||
import {
|
import {
|
||||||
MasterKey,
|
MasterKey,
|
||||||
@@ -13,7 +19,7 @@ import {
|
|||||||
SymmetricCryptoKey,
|
SymmetricCryptoKey,
|
||||||
UserKey,
|
UserKey,
|
||||||
} from "../models/domain/symmetric-crypto-key";
|
} from "../models/domain/symmetric-crypto-key";
|
||||||
import { CryptoService } from "../services/crypto.service";
|
import { CryptoService, USER_EVER_HAD_USER_KEY } from "../services/crypto.service";
|
||||||
|
|
||||||
describe("cryptoService", () => {
|
describe("cryptoService", () => {
|
||||||
let cryptoService: CryptoService;
|
let cryptoService: CryptoService;
|
||||||
@@ -23,15 +29,14 @@ describe("cryptoService", () => {
|
|||||||
const platformUtilService = mock<PlatformUtilsService>();
|
const platformUtilService = mock<PlatformUtilsService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const stateService = mock<StateService>();
|
const stateService = mock<StateService>();
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
|
||||||
const mockUserId = "mock user id";
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(cryptoFunctionService);
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
mockReset(encryptService);
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
mockReset(platformUtilService);
|
|
||||||
mockReset(logService);
|
|
||||||
mockReset(stateService);
|
|
||||||
|
|
||||||
cryptoService = new CryptoService(
|
cryptoService = new CryptoService(
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
@@ -39,9 +44,15 @@ describe("cryptoService", () => {
|
|||||||
platformUtilService,
|
platformUtilService,
|
||||||
logService,
|
logService,
|
||||||
stateService,
|
stateService,
|
||||||
|
accountService,
|
||||||
|
stateProvider,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("instantiates", () => {
|
it("instantiates", () => {
|
||||||
expect(cryptoService).not.toBeFalsy();
|
expect(cryptoService).not.toBeFalsy();
|
||||||
});
|
});
|
||||||
@@ -117,12 +128,49 @@ describe("cryptoService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("everHadUserKey$", () => {
|
||||||
|
let everHadUserKeyState: FakeActiveUserState<boolean>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
everHadUserKeyState = stateProvider.activeUser.getFake(USER_EVER_HAD_USER_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when stored value is true", async () => {
|
||||||
|
everHadUserKeyState.nextState(true);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when stored value is false", async () => {
|
||||||
|
everHadUserKeyState.nextState(false);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when stored value is null", async () => {
|
||||||
|
everHadUserKeyState.nextState(null);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("setUserKey", () => {
|
describe("setUserKey", () => {
|
||||||
let mockUserKey: UserKey;
|
let mockUserKey: UserKey;
|
||||||
|
let everHadUserKeyState: FakeSingleUserState<boolean>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||||
|
everHadUserKeyState = stateProvider.singleUser.getFake(mockUserId, USER_EVER_HAD_USER_KEY);
|
||||||
|
|
||||||
|
// Initialize storage
|
||||||
|
everHadUserKeyState.nextState(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set everHadUserKey if key is not null to true", async () => {
|
||||||
|
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(everHadUserKeyState.state$)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Auto Key refresh", () => {
|
describe("Auto Key refresh", () => {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import * as bigInt from "big-integer";
|
import * as bigInt from "big-integer";
|
||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||||
import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key";
|
import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key";
|
||||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||||
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||||
import { EncryptService } from "../abstractions/encrypt.service";
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
@@ -36,34 +39,48 @@ import {
|
|||||||
SymmetricCryptoKey,
|
SymmetricCryptoKey,
|
||||||
UserKey,
|
UserKey,
|
||||||
} from "../models/domain/symmetric-crypto-key";
|
} from "../models/domain/symmetric-crypto-key";
|
||||||
|
import { ActiveUserState, CRYPTO_DISK, KeyDefinition, StateProvider } from "../state";
|
||||||
|
|
||||||
|
export const USER_EVER_HAD_USER_KEY = new KeyDefinition<boolean>(CRYPTO_DISK, "everHadUserKey", {
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
});
|
||||||
|
|
||||||
export class CryptoService implements CryptoServiceAbstraction {
|
export class CryptoService implements CryptoServiceAbstraction {
|
||||||
|
private activeUserEverHadUserKey: ActiveUserState<boolean>;
|
||||||
|
|
||||||
|
readonly everHadUserKey$;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
protected encryptService: EncryptService,
|
protected encryptService: EncryptService,
|
||||||
protected platformUtilService: PlatformUtilsService,
|
protected platformUtilService: PlatformUtilsService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
) {}
|
protected accountService: AccountService,
|
||||||
|
protected stateProvider: StateProvider,
|
||||||
|
) {
|
||||||
|
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
|
||||||
|
|
||||||
async setUserKey(key: UserKey, userId?: string): Promise<void> {
|
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserKey(key: UserKey, userId?: UserId): Promise<void> {
|
||||||
|
// TODO: make this non-nullable in signature
|
||||||
|
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
await this.stateService.setEverHadUserKey(true, { userId: userId });
|
// Key should never be null anyway
|
||||||
|
this.stateProvider.getUser(userId, USER_EVER_HAD_USER_KEY).update(() => true);
|
||||||
}
|
}
|
||||||
await this.stateService.setUserKey(key, { userId: userId });
|
await this.stateService.setUserKey(key, { userId: userId });
|
||||||
await this.storeAdditionalKeys(key, userId);
|
await this.storeAdditionalKeys(key, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEverHadUserKey(userId?: string): Promise<boolean> {
|
|
||||||
return await this.stateService.getEverHadUserKey({ userId: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshAdditionalKeys(): Promise<void> {
|
async refreshAdditionalKeys(): Promise<void> {
|
||||||
const key = await this.getUserKey();
|
const key = await this.getUserKey();
|
||||||
await this.setUserKey(key);
|
await this.setUserKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserKey(userId?: string): Promise<UserKey> {
|
async getUserKey(userId?: UserId): Promise<UserKey> {
|
||||||
let userKey = await this.stateService.getUserKey({ userId: userId });
|
let userKey = await this.stateService.getUserKey({ userId: userId });
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
return userKey;
|
return userKey;
|
||||||
@@ -79,13 +96,13 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean> {
|
async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> {
|
||||||
return await this.validateUserKey(
|
return await this.validateUserKey(
|
||||||
(masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey,
|
(masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserKeyWithLegacySupport(userId?: string): Promise<UserKey> {
|
async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> {
|
||||||
const userKey = await this.getUserKey(userId);
|
const userKey = await this.getUserKey(userId);
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
return userKey;
|
return userKey;
|
||||||
@@ -96,7 +113,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return (await this.getMasterKey(userId)) as unknown as UserKey;
|
return (await this.getMasterKey(userId)) as unknown as UserKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey> {
|
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
|
||||||
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
if (!(await this.validateUserKey(userKey))) {
|
if (!(await this.validateUserKey(userKey))) {
|
||||||
@@ -113,11 +130,11 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasUserKeyInMemory(userId?: string): Promise<boolean> {
|
async hasUserKeyInMemory(userId?: UserId): Promise<boolean> {
|
||||||
return (await this.stateService.getUserKey({ userId: userId })) != null;
|
return (await this.stateService.getUserKey({ userId: userId })) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
|
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,14 +148,14 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return this.buildProtectedSymmetricKey(masterKey, newUserKey);
|
return this.buildProtectedSymmetricKey(masterKey, newUserKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearUserKey(clearStoredKeys = true, userId?: string): Promise<void> {
|
async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setUserKey(null, { userId: userId });
|
await this.stateService.setUserKey(null, { userId: userId });
|
||||||
if (clearStoredKeys) {
|
if (clearStoredKeys) {
|
||||||
await this.clearAllStoredUserKeys(userId);
|
await this.clearAllStoredUserKeys(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void> {
|
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||||
if (keySuffix === KeySuffixOptions.Auto) {
|
if (keySuffix === KeySuffixOptions.Auto) {
|
||||||
this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||||
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
|
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
|
||||||
@@ -149,15 +166,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: string): Promise<void> {
|
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId });
|
await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMasterKey(key: MasterKey, userId?: string): Promise<void> {
|
async setMasterKey(key: MasterKey, userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setMasterKey(key, { userId: userId });
|
await this.stateService.setMasterKey(key, { userId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMasterKey(userId?: string): Promise<MasterKey> {
|
async getMasterKey(userId?: UserId): Promise<MasterKey> {
|
||||||
let masterKey = await this.stateService.getMasterKey({ userId: userId });
|
let masterKey = await this.stateService.getMasterKey({ userId: userId });
|
||||||
if (!masterKey) {
|
if (!masterKey) {
|
||||||
masterKey = (await this.stateService.getCryptoMasterKey({ userId: userId })) as MasterKey;
|
masterKey = (await this.stateService.getCryptoMasterKey({ userId: userId })) as MasterKey;
|
||||||
@@ -170,7 +187,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return masterKey;
|
return masterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrDeriveMasterKey(password: string, userId?: string) {
|
async getOrDeriveMasterKey(password: string, userId?: UserId) {
|
||||||
let masterKey = await this.getMasterKey(userId);
|
let masterKey = await this.getMasterKey(userId);
|
||||||
return (masterKey ||= await this.makeMasterKey(
|
return (masterKey ||= await this.makeMasterKey(
|
||||||
password,
|
password,
|
||||||
@@ -195,7 +212,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return (await this.makeKey(password, email, kdf, KdfConfig)) as MasterKey;
|
return (await this.makeKey(password, email, kdf, KdfConfig)) as MasterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearMasterKey(userId?: string): Promise<void> {
|
async clearMasterKey(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setMasterKey(null, { userId: userId });
|
await this.stateService.setMasterKey(null, { userId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +227,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
async decryptUserKeyWithMasterKey(
|
async decryptUserKeyWithMasterKey(
|
||||||
masterKey: MasterKey,
|
masterKey: MasterKey,
|
||||||
userKey?: EncString,
|
userKey?: EncString,
|
||||||
userId?: string,
|
userId?: UserId,
|
||||||
): Promise<UserKey> {
|
): Promise<UserKey> {
|
||||||
masterKey ||= await this.getMasterKey(userId);
|
masterKey ||= await this.getMasterKey(userId);
|
||||||
if (masterKey == null) {
|
if (masterKey == null) {
|
||||||
@@ -275,7 +292,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return await this.stateService.getKeyHash();
|
return await this.stateService.getKeyHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearMasterKeyHash(userId?: string): Promise<void> {
|
async clearMasterKeyHash(userId?: UserId): Promise<void> {
|
||||||
return await this.stateService.setKeyHash(null, { userId: userId });
|
return await this.stateService.setKeyHash(null, { userId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +406,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return this.buildProtectedSymmetricKey(key, newSymKey);
|
return this.buildProtectedSymmetricKey(key, newSymKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId });
|
await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId });
|
||||||
if (!memoryOnly) {
|
if (!memoryOnly) {
|
||||||
await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId });
|
await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId });
|
||||||
@@ -452,7 +469,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return providerKeys;
|
return providerKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setDecryptedProviderKeys(null, { userId: userId });
|
await this.stateService.setDecryptedProviderKeys(null, { userId: userId });
|
||||||
if (!memoryOnly) {
|
if (!memoryOnly) {
|
||||||
await this.stateService.setEncryptedProviderKeys(null, { userId: userId });
|
await this.stateService.setEncryptedProviderKeys(null, { userId: userId });
|
||||||
@@ -537,7 +554,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return [publicB64, privateEnc];
|
return [publicB64, privateEnc];
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise<void[]> {
|
async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> {
|
||||||
const keysToClear: Promise<void>[] = [
|
const keysToClear: Promise<void>[] = [
|
||||||
this.stateService.setDecryptedPrivateKey(null, { userId: userId }),
|
this.stateService.setDecryptedPrivateKey(null, { userId: userId }),
|
||||||
this.stateService.setPublicKey(null, { userId: userId }),
|
this.stateService.setPublicKey(null, { userId: userId }),
|
||||||
@@ -553,7 +570,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return (await this.stretchKey(pinKey)) as PinKey;
|
return (await this.stretchKey(pinKey)) as PinKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearPinKeys(userId?: string): Promise<void> {
|
async clearPinKeys(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||||
await this.stateService.setProtectedPin(null, { userId: userId });
|
await this.stateService.setProtectedPin(null, { userId: userId });
|
||||||
@@ -613,7 +630,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return new SymmetricCryptoKey(randomBytes) as CipherKey;
|
return new SymmetricCryptoKey(randomBytes) as CipherKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearKeys(userId?: string): Promise<any> {
|
async clearKeys(userId?: UserId): Promise<any> {
|
||||||
await this.clearUserKey(true, userId);
|
await this.clearUserKey(true, userId);
|
||||||
await this.clearMasterKeyHash(userId);
|
await this.clearMasterKeyHash(userId);
|
||||||
await this.clearOrgKeys(false, userId);
|
await this.clearOrgKeys(false, userId);
|
||||||
@@ -776,7 +793,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
* @param key The user key
|
* @param key The user key
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
*/
|
*/
|
||||||
protected async storeAdditionalKeys(key: UserKey, userId?: string) {
|
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) {
|
||||||
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
|
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
|
||||||
if (storeAuto) {
|
if (storeAuto) {
|
||||||
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
|
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
|
||||||
@@ -802,7 +819,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
* ephemeral version.
|
* ephemeral version.
|
||||||
* @param key The user key
|
* @param key The user key
|
||||||
*/
|
*/
|
||||||
protected async storePinKey(key: UserKey, userId?: string) {
|
protected async storePinKey(key: UserKey, userId?: UserId) {
|
||||||
const pin = await this.encryptService.decryptToUtf8(
|
const pin = await this.encryptService.decryptToUtf8(
|
||||||
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
|
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
|
||||||
key,
|
key,
|
||||||
@@ -822,7 +839,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) {
|
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId) {
|
||||||
let shouldStoreKey = false;
|
let shouldStoreKey = false;
|
||||||
switch (keySuffix) {
|
switch (keySuffix) {
|
||||||
case KeySuffixOptions.Auto: {
|
case KeySuffixOptions.Auto: {
|
||||||
@@ -841,7 +858,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
|
|
||||||
protected async getKeyFromStorage(
|
protected async getKeyFromStorage(
|
||||||
keySuffix: KeySuffixOptions,
|
keySuffix: KeySuffixOptions,
|
||||||
userId?: string,
|
userId?: UserId,
|
||||||
): Promise<UserKey> {
|
): Promise<UserKey> {
|
||||||
if (keySuffix === KeySuffixOptions.Auto) {
|
if (keySuffix === KeySuffixOptions.Auto) {
|
||||||
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
|
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
|
||||||
@@ -889,7 +906,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async clearAllStoredUserKeys(userId?: string): Promise<void> {
|
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||||
}
|
}
|
||||||
@@ -984,7 +1001,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
// These methods support migrating the old keys to the new ones.
|
// These methods support migrating the old keys to the new ones.
|
||||||
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
|
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
|
||||||
|
|
||||||
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string) {
|
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
|
||||||
if (keySuffix === KeySuffixOptions.Auto) {
|
if (keySuffix === KeySuffixOptions.Auto) {
|
||||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||||
} else if (keySuffix === KeySuffixOptions.Pin) {
|
} else if (keySuffix === KeySuffixOptions.Pin) {
|
||||||
@@ -993,7 +1010,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrateAutoKeyIfNeeded(userId?: string) {
|
async migrateAutoKeyIfNeeded(userId?: UserId) {
|
||||||
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
|
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
|
||||||
if (!oldAutoKey) {
|
if (!oldAutoKey) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2093,24 +2093,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEverHadUserKey(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
|
||||||
?.profile?.everHadUserKey ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEverHadUserKey(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.profile.everHadUserKey = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
|
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
|
||||||
|
|||||||
@@ -18,3 +18,5 @@ import { StateDefinition } from "./state-definition";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||||
|
|
||||||
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AbstractStorageService } from "../platform/abstractions/storage.service
|
|||||||
|
|
||||||
import { MigrationBuilder } from "./migration-builder";
|
import { MigrationBuilder } from "./migration-builder";
|
||||||
import { MigrationHelper } from "./migration-helper";
|
import { MigrationHelper } from "./migration-helper";
|
||||||
|
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers";
|
||||||
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
||||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
@@ -15,7 +16,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 = 2;
|
export const MIN_VERSION = 2;
|
||||||
export const CURRENT_VERSION = 9;
|
export const CURRENT_VERSION = 10;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export async function migrate(
|
export async function migrate(
|
||||||
@@ -40,7 +41,8 @@ export async function migrate(
|
|||||||
.with(RemoveLegacyEtmKeyMigrator, 5, 6)
|
.with(RemoveLegacyEtmKeyMigrator, 5, 6)
|
||||||
.with(MoveBiometricAutoPromptToAccount, 6, 7)
|
.with(MoveBiometricAutoPromptToAccount, 6, 7)
|
||||||
.with(MoveStateVersionMigrator, 7, 8)
|
.with(MoveStateVersionMigrator, 7, 8)
|
||||||
.with(MoveBrowserSettingsToGlobal, 8, CURRENT_VERSION)
|
.with(MoveBrowserSettingsToGlobal, 8, 9)
|
||||||
|
.with(EverHadUserKeyMigrator, 9, CURRENT_VERSION)
|
||||||
.migrate(migrationHelper);
|
.migrate(migrationHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { MockProxy, any } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { EverHadUserKeyMigrator } from "./10-move-ever-had-user-key-to-state-providers";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: [
|
||||||
|
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||||
|
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||||
|
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||||
|
],
|
||||||
|
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||||
|
profile: {
|
||||||
|
everHadUserKey: false,
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||||
|
profile: {
|
||||||
|
everHadUserKey: true,
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
"user_c493ed01-4e08-4e88-abc7-332f380ca760_crypto_everHadUserKey": false,
|
||||||
|
"user_23e61a5f-2ece-4f5e-b499-f0bc489482a9_crypto_everHadUserKey": true,
|
||||||
|
"user_fd005ea6-a16a-45ef-ba4a-a194269bfd73_crypto_everHadUserKey": false,
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: [
|
||||||
|
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||||
|
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||||
|
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||||
|
],
|
||||||
|
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||||
|
profile: {
|
||||||
|
everHadUserKey: false,
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||||
|
profile: {
|
||||||
|
everHadUserKey: true,
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("EverHadUserKeyMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: EverHadUserKeyMigrator;
|
||||||
|
const keyDefinitionLike = {
|
||||||
|
key: "everHadUserKey",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "crypto",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 9);
|
||||||
|
sut = new EverHadUserKeyMigrator(9, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove everHadUserKey from all accounts", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||||
|
profile: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", {
|
||||||
|
profile: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set everHadUserKey provider value for each account", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||||
|
keyDefinitionLike,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||||
|
keyDefinitionLike,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||||
|
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||||
|
keyDefinitionLike,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 10);
|
||||||
|
sut = new EverHadUserKeyMigrator(9, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||||
|
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||||
|
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||||
|
])("should null out new values", async (userId) => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add explicit value back to accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||||
|
profile: {
|
||||||
|
everHadUserKey: false,
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", {
|
||||||
|
profile: {
|
||||||
|
everHadUserKey: true,
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not try to restore values to missing accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("fd005ea6-a16a-45ef-ba4a-a194269bfd73", any());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
profile?: {
|
||||||
|
everHadUserKey?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const USER_EVER_HAD_USER_KEY: KeyDefinitionLike = {
|
||||||
|
key: "everHadUserKey",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "crypto",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EverHadUserKeyMigrator extends Migrator<9, 10> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
const value = account?.profile?.everHadUserKey;
|
||||||
|
await helper.setToUser(userId, USER_EVER_HAD_USER_KEY, value ?? false);
|
||||||
|
if (value != null) {
|
||||||
|
delete account.profile.everHadUserKey;
|
||||||
|
}
|
||||||
|
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> {
|
||||||
|
const value = await helper.getFromUser(userId, USER_EVER_HAD_USER_KEY);
|
||||||
|
if (account) {
|
||||||
|
account.profile = Object.assign(account.profile ?? {}, {
|
||||||
|
everHadUserKey: value,
|
||||||
|
});
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
await helper.setToUser(userId, USER_EVER_HAD_USER_KEY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user