1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-5533] Migrate Asymmetric User Keys to State Providers (#7665)

This commit is contained in:
Matt Gibson
2024-02-14 15:04:08 -05:00
committed by GitHub
parent 7a6d7b3a68
commit d8b74b78da
13 changed files with 554 additions and 127 deletions

View File

@@ -9,7 +9,16 @@ import { AccountService } from "../../auth/abstractions/account.service";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Utils } from "../../platform/misc/utils";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import { OrgKey, UserKey, MasterKey, ProviderKey, PinKey, CipherKey } from "../../types/key";
import {
OrgKey,
UserKey,
MasterKey,
ProviderKey,
PinKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
} from "../../types/key";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
@@ -38,7 +47,12 @@ import {
USER_ORGANIZATION_KEYS,
} from "./key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./key-state/provider-keys.state";
import { USER_EVER_HAD_USER_KEY } from "./key-state/user-key.state";
import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_PRIVATE_KEY,
USER_PUBLIC_KEY,
} from "./key-state/user-key.state";
export class CryptoService implements CryptoServiceAbstraction {
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
@@ -49,12 +63,16 @@ export class CryptoService implements CryptoServiceAbstraction {
private readonly activeUserEncryptedProviderKeysState: ActiveUserState<
Record<ProviderId, EncryptedString>
>;
private readonly activeUserProviderKeysState: DerivedState<Record<OrganizationId, ProviderKey>>;
private readonly activeUserProviderKeysState: DerivedState<Record<ProviderId, ProviderKey>>;
private readonly activeUserEncryptedPrivateKeyState: ActiveUserState<EncryptedString>;
private readonly activeUserPrivateKeyState: DerivedState<UserPrivateKey>;
private readonly activeUserPublicKeyState: DerivedState<UserPublicKey>;
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
readonly activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
readonly everHadUserKey$;
readonly activeUserPrivateKey$: Observable<UserPrivateKey>;
readonly activeUserPublicKey$: Observable<UserPublicKey>;
readonly everHadUserKey$: Observable<boolean>;
constructor(
protected cryptoFunctionService: CryptoFunctionService,
@@ -65,7 +83,31 @@ export class CryptoService implements CryptoServiceAbstraction {
protected accountService: AccountService,
protected stateProvider: StateProvider,
) {
// User Key
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
// User Asymmetric Key Pair
this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY);
this.activeUserPrivateKeyState = stateProvider.getDerived(
this.activeUserEncryptedPrivateKeyState.combinedState$,
USER_PRIVATE_KEY,
{
encryptService: this.encryptService,
cryptoService: this,
},
);
this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null
this.activeUserPublicKeyState = stateProvider.getDerived(
this.activeUserPrivateKey$,
USER_PUBLIC_KEY,
{
cryptoFunctionService: this.cryptoFunctionService,
},
);
this.activeUserPublicKey$ = this.activeUserPublicKeyState.state$; // may be null
// Organization keys
this.activeUserEncryptedOrgKeysState = stateProvider.getActive(
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
@@ -74,6 +116,9 @@ export class CryptoService implements CryptoServiceAbstraction {
USER_ORGANIZATION_KEYS,
{ cryptoService: this },
);
this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function
// Provider keys
this.activeUserEncryptedProviderKeysState = stateProvider.getActive(
USER_ENCRYPTED_PROVIDER_KEYS,
);
@@ -82,9 +127,6 @@ export class CryptoService implements CryptoServiceAbstraction {
USER_PROVIDER_KEYS,
{ encryptService: this.encryptService, cryptoService: this },
);
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function
this.activeUserProviderKeys$ = this.activeUserProviderKeysState.state$; // null handled by `derive` function
}
@@ -465,19 +507,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async getPublicKey(): Promise<Uint8Array> {
const inMemoryPublicKey = await this.stateService.getPublicKey();
if (inMemoryPublicKey != null) {
return inMemoryPublicKey;
}
const privateKey = await this.getPrivateKey();
if (privateKey == null) {
return null;
}
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
await this.stateService.setPublicKey(publicKey);
return publicKey;
return await firstValueFrom(this.activeUserPublicKey$);
}
async makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]> {
@@ -487,32 +517,16 @@ export class CryptoService implements CryptoServiceAbstraction {
return [encShareKey, new SymmetricCryptoKey(shareKey) as T];
}
async setPrivateKey(encPrivateKey: string): Promise<void> {
async setPrivateKey(encPrivateKey: EncryptedString): Promise<void> {
if (encPrivateKey == null) {
return;
}
await this.stateService.setDecryptedPrivateKey(null);
await this.stateService.setEncryptedPrivateKey(encPrivateKey);
await this.activeUserEncryptedPrivateKeyState.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array> {
const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey();
if (decryptedPrivateKey != null) {
return decryptedPrivateKey;
}
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
if (encPrivateKey == null) {
return null;
}
const privateKey = await this.encryptService.decryptToBytes(
new EncString(encPrivateKey),
await this.getUserKeyWithLegacySupport(),
);
await this.stateService.setDecryptedPrivateKey(privateKey);
return privateKey;
return await firstValueFrom(this.activeUserPrivateKey$);
}
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
@@ -543,14 +557,23 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> {
const keysToClear: Promise<void>[] = [
this.stateService.setDecryptedPrivateKey(null, { userId: userId }),
this.stateService.setPublicKey(null, { userId: userId }),
];
if (!memoryOnly) {
keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId }));
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const userIdIsActive = userId == null || userId === activeUserId;
if (memoryOnly && userIdIsActive) {
// key pair is only cached for active users
await this.activeUserPrivateKeyState.forceValue(null);
await this.activeUserPublicKeyState.forceValue(null);
return;
} else {
if (userId == null && activeUserId == null) {
// nothing to do
return;
}
// below updates decrypted private key and public keys if this is the active user as well since those are derived from the encrypted private key
await this.stateProvider
.getUser(userId ?? activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => null);
}
return Promise.all(keysToClear);
}
async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise<PinKey> {
@@ -735,16 +758,23 @@ export class CryptoService implements CryptoServiceAbstraction {
}
try {
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
const [userId, encPrivateKey] = await firstValueFrom(
this.activeUserEncryptedPrivateKeyState.combinedState$,
);
if (encPrivateKey == null) {
return false;
}
const privateKey = await this.encryptService.decryptToBytes(
new EncString(encPrivateKey),
key,
);
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
// Can decrypt private key
const privateKey = await USER_PRIVATE_KEY.derive([userId, encPrivateKey], {
encryptService: this.encryptService,
cryptoService: this,
});
// Can successfully derive public key
await USER_PUBLIC_KEY.derive(privateKey, {
cryptoFunctionService: this.cryptoFunctionService,
});
} catch (e) {
return false;
}
@@ -765,7 +795,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const userKey = new SymmetricCryptoKey(rawKey) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
await this.setUserKey(userKey);
await this.stateService.setEncryptedPrivateKey(privateKey.encryptedString);
await this.activeUserEncryptedPrivateKeyState.update(() => privateKey.encryptedString);
return {
userKey,