mirror of
https://github.com/bitwarden/browser
synced 2026-02-24 00:23:17 +00:00
* Do not initialize symmetric crypto keys with null * Require new message on invalid native message encryption Handling of this error is to require the user to retry, so the promise needs to resolve.
195 lines
8.6 KiB
TypeScript
195 lines
8.6 KiB
TypeScript
import { firstValueFrom } from "rxjs";
|
|
|
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
|
import { CsprngString } from "@bitwarden/common/types/csprng";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
|
|
|
export class ElectronCryptoService extends CryptoService {
|
|
constructor(
|
|
pinService: PinServiceAbstraction,
|
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
|
keyGenerationService: KeyGenerationService,
|
|
cryptoFunctionService: CryptoFunctionService,
|
|
encryptService: EncryptService,
|
|
platformUtilsService: PlatformUtilsService,
|
|
logService: LogService,
|
|
stateService: StateService,
|
|
accountService: AccountService,
|
|
stateProvider: StateProvider,
|
|
private biometricStateService: BiometricStateService,
|
|
kdfConfigService: KdfConfigService,
|
|
) {
|
|
super(
|
|
pinService,
|
|
masterPasswordService,
|
|
keyGenerationService,
|
|
cryptoFunctionService,
|
|
encryptService,
|
|
platformUtilsService,
|
|
logService,
|
|
stateService,
|
|
accountService,
|
|
stateProvider,
|
|
kdfConfigService,
|
|
);
|
|
}
|
|
|
|
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
|
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3474)
|
|
const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId });
|
|
return oldKey || (await this.stateService.hasUserKeyBiometric({ userId: userId }));
|
|
}
|
|
return super.hasUserKeyStored(keySuffix, userId);
|
|
}
|
|
|
|
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
|
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
|
await this.biometricStateService.removeEncryptedClientKeyHalf(userId);
|
|
await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
|
|
return;
|
|
}
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
await super.clearStoredUserKey(keySuffix, userId);
|
|
}
|
|
|
|
protected override async storeAdditionalKeys(key: UserKey, userId?: UserId) {
|
|
await super.storeAdditionalKeys(key, userId);
|
|
|
|
const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);
|
|
|
|
if (storeBiometricKey) {
|
|
await this.storeBiometricKey(key, userId);
|
|
} else {
|
|
await this.stateService.setUserKeyBiometric(null, { userId: userId });
|
|
}
|
|
await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
|
|
}
|
|
|
|
protected override async getKeyFromStorage(
|
|
keySuffix: KeySuffixOptions,
|
|
userId?: UserId,
|
|
): Promise<UserKey> {
|
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
|
await this.migrateBiometricKeyIfNeeded(userId);
|
|
const userKey = await this.stateService.getUserKeyBiometric({ userId: userId });
|
|
return userKey == null
|
|
? null
|
|
: (new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey);
|
|
}
|
|
return await super.getKeyFromStorage(keySuffix, userId);
|
|
}
|
|
|
|
protected async storeBiometricKey(key: UserKey, userId?: UserId): Promise<void> {
|
|
// May resolve to null, in which case no client key have is required
|
|
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(key, userId);
|
|
await this.stateService.setUserKeyBiometric(
|
|
{ key: key.keyB64, clientEncKeyHalf },
|
|
{ userId: userId },
|
|
);
|
|
}
|
|
|
|
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
|
const biometricUnlockPromise =
|
|
userId == null
|
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
|
: this.biometricStateService.getBiometricUnlockEnabled(userId);
|
|
const biometricUnlock = await biometricUnlockPromise;
|
|
return biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
|
}
|
|
return await super.shouldStoreKey(keySuffix, userId);
|
|
}
|
|
|
|
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
|
await this.clearStoredUserKey(KeySuffixOptions.Biometric, userId);
|
|
await super.clearAllStoredUserKeys(userId);
|
|
}
|
|
|
|
private async getBiometricEncryptionClientKeyHalf(
|
|
userKey: UserKey,
|
|
userId: UserId,
|
|
): Promise<CsprngString | null> {
|
|
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
|
if (!requireClientKeyHalf) {
|
|
return null;
|
|
}
|
|
|
|
// Retrieve existing key half if it exists
|
|
let biometricKey = await this.biometricStateService
|
|
.getEncryptedClientKeyHalf(userId)
|
|
.then((result) => result?.decrypt(null /* user encrypted */, userKey))
|
|
.then((result) => result as CsprngString);
|
|
if (biometricKey == null && userKey != null) {
|
|
// Set a key half if it doesn't exist
|
|
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
|
biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
|
|
const encKey = await this.encryptService.encrypt(biometricKey, userKey);
|
|
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
|
}
|
|
|
|
return biometricKey;
|
|
}
|
|
|
|
// --LEGACY METHODS--
|
|
// We previously used the master key for additional keys, but now we use the user key.
|
|
// These methods support migrating the old keys to the new ones.
|
|
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
|
|
|
|
override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
|
|
if (keySuffix === KeySuffixOptions.Biometric) {
|
|
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
|
}
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
super.clearDeprecatedKeys(keySuffix, userId);
|
|
}
|
|
|
|
private async migrateBiometricKeyIfNeeded(userId?: UserId) {
|
|
if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) {
|
|
const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId });
|
|
// decrypt
|
|
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey;
|
|
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey({
|
|
userId: userId,
|
|
});
|
|
const encUserKey =
|
|
encUserKeyPrim != null
|
|
? new EncString(encUserKeyPrim)
|
|
: await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
|
|
if (!encUserKey) {
|
|
throw new Error("No user key found during biometric migration");
|
|
}
|
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
|
masterKey,
|
|
encUserKey,
|
|
userId,
|
|
);
|
|
// migrate
|
|
await this.storeBiometricKey(userKey, userId);
|
|
await this.stateService.setCryptoMasterKeyBiometric(null, { userId });
|
|
}
|
|
}
|
|
}
|