mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-5363] PinService State Providers (#8244)
* move pinKeyEncryptedUserKey * move pinKeyEncryptedUserKeyEphemeral * remove comments, move docs * cleanup * use UserKeyDefinition * refactor methods * add migration * fix browser dependency * add tests for migration * rename to pinService * move state to PinService * add PinService dep to CryptoService * move protectedPin to state provider * update service deps * renaming * move decryptUserKeyWithPin to pinService * update service injection * move more methods our of crypto service * remove CryptoService dep from PinService and update service injection * remove cryptoService reference * add method to FakeMasterPasswordService * fix circular dependency * fix desktop service injection * update browser dependencies * add protectedPin to migrations * move storePinKey to pinService * update and clarify documentation * more jsdoc updates * update import paths * refactor isPinLockSet method * update state definitions * initialize service before injecting into other services * initialize service before injecting into other services (bw.ts) * update clearOn and do additional cleanup * clarify docs and naming * assign abstract & private methods, add clarity to decryptAndMigrateOldPinKeyEncryptedMasterKey() method * derived state (attempt) * fix typos * use accountService to get active user email * use constant userId * add derived state * add get and clear for oldPinKeyEncryptedMasterKey * require userId * move pinProtected * add clear methods * remove pinProtected from account.ts and replace methods * add methods to create and store pinKeyEncryptedUserKey * add pinProtected/oldPinKeyEncrypterMasterKey to migration * update migration tests * update migration rollback tests * update to systemService and decryptAndMigrate... method * remove old test * increase length of state definition name to meet test requirements * rename 'TRANSIENT' to 'EPHEMERAL' for consistency * fix tests for login strategies, vault-export, and fake MP service * more updates to login-strategy tests * write new tests for core pinKeyEncrypterUserKey methods and isPinSet * write new tests for pinProtected and oldPinKeyEncryptedMasterKey methods * minor test reformatting * update test for decryptUserKeyWithPin() * fix bug with oldPinKeyEncryptedMasterKey * fix tests for vault-timeout-settings.service * fix bitwarden-password-protected-importer test * fix login strategy tests and auth-request.service test * update pinService tests * fix crypto service tests * add jsdoc * fix test file import * update jsdocs for decryptAndMigrateOldPinKeyEncryptedMasterKey() * update error messages and jsdocs * add null checks, move userId retrievals * update migration tests * update stateService calls to require userId * update test for decryptUserKeyWithPin() * update oldPinKeyEncryptedMasterKey migration tests * more test updates * fix factory import * update tests for isPinSet() and createProtectedPin() * add test for makePinKey() * add test for createPinKeyEncryptedUserKey() * add tests for getPinLockType() * consolidate userId verification tests * add tests for storePinKeyEncryptedUserKey() * fix service dep * get email based on userId * use MasterPasswordService instead of internal * rename protectedPin to userKeyEncryptedPin * rename to pinKeyEncryptedUserKeyPersistent * update method params * fix CryptoService tests * jsdoc update * use EncString for userKeyEncryptedPin * remove comment * use cryptoFunctionService.compareFast() * update tests * cleanup, remove comments * resolve merge conflict * fix DI of MasterPasswordService * more DI fixes
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as bigInt from "big-integer";
|
||||
import { Observable, filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
UserKey,
|
||||
MasterKey,
|
||||
ProviderKey,
|
||||
PinKey,
|
||||
CipherKey,
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
@@ -74,6 +74,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
readonly everHadUserKey$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected pinService: PinServiceAbstraction,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected keyGenerationService: KeyGenerationService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -254,7 +255,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
if (keySuffix === KeySuffixOptions.Pin) {
|
||||
// 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
|
||||
this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(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
|
||||
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||
@@ -303,46 +304,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
|
||||
}
|
||||
|
||||
// TODO: move to master password service
|
||||
async decryptUserKeyWithMasterKey(
|
||||
masterKey: MasterKey,
|
||||
userKey?: EncString,
|
||||
userId?: UserId,
|
||||
): Promise<UserKey> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
|
||||
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey == null) {
|
||||
throw new Error("No master key found.");
|
||||
}
|
||||
|
||||
// Try one more way to get the user key if it still wasn't found.
|
||||
if (userKey == null) {
|
||||
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
||||
userId: userId,
|
||||
});
|
||||
if (deprecatedKey == null) {
|
||||
throw new Error("No encrypted user key found.");
|
||||
}
|
||||
userKey = new EncString(deprecatedKey);
|
||||
}
|
||||
|
||||
let decUserKey: Uint8Array;
|
||||
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
|
||||
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const newKey = await this.stretchKey(masterKey);
|
||||
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type.");
|
||||
}
|
||||
if (decUserKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SymmetricCryptoKey(decUserKey) as UserKey;
|
||||
}
|
||||
|
||||
// TODO: move to MasterPasswordService
|
||||
async hashMasterKey(
|
||||
password: string,
|
||||
@@ -548,53 +509,19 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
|
||||
}
|
||||
|
||||
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
|
||||
return (await this.stretchKey(pinKey)) as PinKey;
|
||||
}
|
||||
|
||||
async clearPinKeys(userId?: UserId): Promise<void> {
|
||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||
await this.stateService.setProtectedPin(null, { userId: userId });
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
if (userId == null) {
|
||||
throw new Error("Cannot clear PIN keys, no user Id resolved.");
|
||||
}
|
||||
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
await this.pinService.clearUserKeyEncryptedPin(userId);
|
||||
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||
}
|
||||
|
||||
async decryptUserKeyWithPin(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdfConfig: KdfConfig,
|
||||
pinProtectedUserKey?: EncString,
|
||||
): Promise<UserKey> {
|
||||
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKey();
|
||||
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
||||
if (!pinProtectedUserKey) {
|
||||
throw new Error("No PIN protected key found.");
|
||||
}
|
||||
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
||||
const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey);
|
||||
return new SymmetricCryptoKey(userKey) as UserKey;
|
||||
}
|
||||
|
||||
// only for migration purposes
|
||||
async decryptMasterKeyWithPin(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdfConfig: KdfConfig,
|
||||
pinProtectedMasterKey?: EncString,
|
||||
): Promise<MasterKey> {
|
||||
if (!pinProtectedMasterKey) {
|
||||
const pinProtectedMasterKeyString = await this.stateService.getEncryptedPinProtected();
|
||||
if (pinProtectedMasterKeyString == null) {
|
||||
throw new Error("No PIN protected key found.");
|
||||
}
|
||||
pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString);
|
||||
}
|
||||
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
||||
const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey);
|
||||
return new SymmetricCryptoKey(masterKey) as MasterKey;
|
||||
}
|
||||
|
||||
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyGenerationService.deriveKeyFromMaterial(
|
||||
keyMaterial,
|
||||
@@ -798,6 +725,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
* @param userId The desired user
|
||||
*/
|
||||
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
if (userId == null) {
|
||||
throw new Error("Cannot store additional keys, no user Id resolved.");
|
||||
}
|
||||
|
||||
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
|
||||
if (storeAuto) {
|
||||
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
|
||||
@@ -808,37 +741,31 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
|
||||
if (storePin) {
|
||||
await this.storePinKey(key, userId);
|
||||
// Decrypt userKeyEncryptedPin with user key
|
||||
const pin = await this.encryptService.decryptToUtf8(
|
||||
await this.pinService.getUserKeyEncryptedPin(userId),
|
||||
key,
|
||||
);
|
||||
|
||||
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
|
||||
pin,
|
||||
key,
|
||||
userId,
|
||||
);
|
||||
const noPreExistingPersistentKey =
|
||||
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) == null;
|
||||
|
||||
await this.pinService.storePinKeyEncryptedUserKey(
|
||||
pinKeyEncryptedUserKey,
|
||||
noPreExistingPersistentKey,
|
||||
userId,
|
||||
);
|
||||
// We can't always clear deprecated keys because the pin is only
|
||||
// migrated once used to unlock
|
||||
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||
} else {
|
||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the pin key if needed. If MP on Reset is enabled, stores the
|
||||
* ephemeral version.
|
||||
* @param key The user key
|
||||
*/
|
||||
protected async storePinKey(key: UserKey, userId?: UserId) {
|
||||
const pin = await this.encryptService.decryptToUtf8(
|
||||
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
|
||||
key,
|
||||
);
|
||||
const pinKey = await this.makePinKey(
|
||||
pin,
|
||||
await this.stateService.getEmail({ userId: userId }),
|
||||
await this.kdfConfigService.getKdfConfig(),
|
||||
);
|
||||
const encPin = await this.encryptService.encrypt(key.key, pinKey);
|
||||
|
||||
if ((await this.stateService.getPinKeyEncryptedUserKey({ userId: userId })) != null) {
|
||||
await this.stateService.setPinKeyEncryptedUserKey(encPin, { userId: userId });
|
||||
} else {
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(encPin, { userId: userId });
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,8 +778,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
break;
|
||||
}
|
||||
case KeySuffixOptions.Pin: {
|
||||
const protectedPin = await this.stateService.getProtectedPin({ userId: userId });
|
||||
shouldStoreKey = !!protectedPin;
|
||||
const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
|
||||
shouldStoreKey = !!userKeyEncryptedPin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -874,16 +801,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||
}
|
||||
|
||||
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
const newKey = new Uint8Array(64);
|
||||
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
|
||||
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
|
||||
newKey.set(new Uint8Array(encKey));
|
||||
newKey.set(new Uint8Array(macKey), 32);
|
||||
return new SymmetricCryptoKey(newKey);
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
|
||||
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
|
||||
@@ -912,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
): Promise<[T, EncString]> {
|
||||
let protectedSymKey: EncString = null;
|
||||
if (encryptionKey.key.byteLength === 32) {
|
||||
const stretchedEncryptionKey = await this.stretchKey(encryptionKey);
|
||||
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
|
||||
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
|
||||
} else if (encryptionKey.key.byteLength === 64) {
|
||||
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
|
||||
@@ -931,42 +849,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
} else if (keySuffix === KeySuffixOptions.Pin) {
|
||||
await this.stateService.setEncryptedPinProtected(null, { userId: userId });
|
||||
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
|
||||
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
|
||||
}
|
||||
}
|
||||
|
||||
async decryptAndMigrateOldPinKey(
|
||||
masterPasswordOnRestart: boolean,
|
||||
pin: string,
|
||||
email: string,
|
||||
kdfConfig: KdfConfig,
|
||||
oldPinKey: EncString,
|
||||
): Promise<UserKey> {
|
||||
// Decrypt
|
||||
const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey);
|
||||
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
|
||||
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey));
|
||||
// Migrate
|
||||
const pinKey = await this.makePinKey(pin, email, kdfConfig);
|
||||
const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey);
|
||||
if (masterPasswordOnRestart) {
|
||||
await this.stateService.setDecryptedPinProtected(null);
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
|
||||
} else {
|
||||
await this.stateService.setEncryptedPinProtected(null);
|
||||
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
|
||||
// We previously only set the protected pin if MP on Restart was enabled
|
||||
// now we set it regardless
|
||||
const encPin = await this.encryptService.encrypt(pin, userKey);
|
||||
await this.stateService.setProtectedPin(encPin.encryptedString);
|
||||
}
|
||||
// This also clears the old Biometrics key since the new Biometrics key will
|
||||
// be created when the user key is set.
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null);
|
||||
return userKey;
|
||||
}
|
||||
|
||||
// --DEPRECATED METHODS--
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user