mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
[PM-14445] TS strict for Key Management, Keys and Lock component (#13121)
* PM-14445: TS strict for Key Management Biometrics * formatting * callbacks not null expectations * state nullability expectations updates * unit tests fix * secure channel naming, explicit null check on messageId * KM-14445: TS strict for Key Management, Keys and Lock component * conflicts resolution, new strict check failures * null simplifications * migrate legacy encryption when no active user throw error instead of hiding it * throw instead of return
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import * as bigInt from "big-integer";
|
||||
import {
|
||||
NEVER,
|
||||
@@ -88,7 +86,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
|
||||
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
|
||||
);
|
||||
) as Observable<Record<OrganizationId, OrgKey>>;
|
||||
}
|
||||
|
||||
async setUserKey(key: UserKey, userId: UserId): Promise<void> {
|
||||
@@ -152,14 +150,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
|
||||
async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (userId == null) {
|
||||
throw new Error("No active user id found.");
|
||||
}
|
||||
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
|
||||
return await this.validateUserKey(masterKey as unknown as UserKey, userId);
|
||||
return await this.validateUserKey(masterKey, userId);
|
||||
}
|
||||
|
||||
// TODO: legacy support for user key is no longer needed since we require users to migrate on login
|
||||
async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (userId == null) {
|
||||
throw new Error("No active user id found.");
|
||||
}
|
||||
|
||||
const userKey = await this.getUserKey(userId);
|
||||
if (userKey) {
|
||||
@@ -172,16 +176,25 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return masterKey as unknown as UserKey;
|
||||
}
|
||||
|
||||
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
|
||||
async getUserKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: UserId,
|
||||
): Promise<UserKey | null> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||
if (userKey) {
|
||||
if (!(await this.validateUserKey(userKey, userId))) {
|
||||
this.logService.warning("Invalid key, throwing away stored keys");
|
||||
await this.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
return userKey;
|
||||
if (userId == null) {
|
||||
throw new Error("No active user id found.");
|
||||
}
|
||||
|
||||
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||
if (userKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(await this.validateUserKey(userKey, userId))) {
|
||||
this.logService.warning("Invalid key, throwing away stored keys");
|
||||
await this.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
return userKey;
|
||||
}
|
||||
|
||||
async hasUserKey(userId?: UserId): Promise<boolean> {
|
||||
@@ -205,9 +218,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
|
||||
}
|
||||
|
||||
async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> {
|
||||
if (!masterKey) {
|
||||
async makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]> {
|
||||
if (masterKey == null) {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (userId == null) {
|
||||
throw new Error("No active user id found.");
|
||||
}
|
||||
|
||||
masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
}
|
||||
if (masterKey == null) {
|
||||
@@ -241,7 +258,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
|
||||
}
|
||||
if (keySuffix === KeySuffixOptions.Pin) {
|
||||
if (keySuffix === KeySuffixOptions.Pin && userId != null) {
|
||||
// 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.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
@@ -251,8 +268,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> {
|
||||
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (userId == null) {
|
||||
throw new Error("No active user id found.");
|
||||
}
|
||||
|
||||
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
|
||||
new EncString(userKeyMasterKey),
|
||||
userId,
|
||||
@@ -265,16 +286,18 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
|
||||
map(([activeAccount, accounts]) => {
|
||||
userId ??= activeAccount?.id;
|
||||
return [userId, accounts[userId]?.email];
|
||||
if (userId == null || accounts[userId] == null) {
|
||||
throw new Error("No user found");
|
||||
}
|
||||
return [userId, accounts[userId].email];
|
||||
}),
|
||||
),
|
||||
);
|
||||
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
|
||||
return (masterKey ||= await this.makeMasterKey(
|
||||
password,
|
||||
email,
|
||||
await this.kdfConfigService.getKdfConfig(),
|
||||
));
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
|
||||
return (
|
||||
masterKey ||
|
||||
(await this.makeMasterKey(password, email, await this.kdfConfigService.getKdfConfig()))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,11 +326,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
// TODO: move to MasterPasswordService
|
||||
async hashMasterKey(
|
||||
password: string,
|
||||
key: MasterKey,
|
||||
key: MasterKey | null,
|
||||
hashPurpose?: HashPurpose,
|
||||
): Promise<string> {
|
||||
if (!key) {
|
||||
if (key == null) {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (userId == null) {
|
||||
throw new Error("No active user found.");
|
||||
}
|
||||
|
||||
key = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
}
|
||||
|
||||
@@ -322,7 +349,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
|
||||
// TODO: move to MasterPasswordService
|
||||
async compareKeyHash(
|
||||
masterPassword: string,
|
||||
masterPassword: string | null,
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
): Promise<boolean> {
|
||||
@@ -386,13 +413,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
async getOrgKey(orgId: OrganizationId): Promise<OrgKey> {
|
||||
async getOrgKey(orgId: OrganizationId): Promise<OrgKey | null> {
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (activeUserId == null) {
|
||||
throw new Error("A user must be active to retrieve an org key");
|
||||
}
|
||||
const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId));
|
||||
return orgKeys[orgId];
|
||||
return orgKeys?.[orgId] ?? null;
|
||||
}
|
||||
|
||||
async makeDataEncKey<T extends OrgKey | UserKey>(
|
||||
@@ -427,15 +454,19 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
// TODO: Deprecate in favor of observable
|
||||
async getProviderKey(providerId: ProviderId): Promise<ProviderKey> {
|
||||
async getProviderKey(providerId: ProviderId): Promise<ProviderKey | null> {
|
||||
if (providerId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (activeUserId == null) {
|
||||
throw new Error("No active user found.");
|
||||
}
|
||||
|
||||
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
|
||||
|
||||
return providerKeys[providerId] ?? null;
|
||||
return providerKeys?.[providerId] ?? null;
|
||||
}
|
||||
|
||||
private async clearProviderKeys(userId: UserId): Promise<void> {
|
||||
@@ -450,7 +481,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> {
|
||||
const shareKey = await this.keyGenerationService.createKey(512);
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (userId == null) {
|
||||
throw new Error("No active user found.");
|
||||
}
|
||||
|
||||
const publicKey = await firstValueFrom(this.userPublicKey$(userId));
|
||||
if (publicKey == null) {
|
||||
throw new Error("No public key found.");
|
||||
}
|
||||
|
||||
const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey);
|
||||
return [encShareKey, shareKey as T];
|
||||
}
|
||||
@@ -465,7 +504,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
.update(() => encPrivateKey);
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<Uint8Array> {
|
||||
async getPrivateKey(): Promise<Uint8Array | null> {
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
if (activeUserId == null) {
|
||||
@@ -479,7 +518,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
|
||||
if (publicKey == null) {
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
publicKey = await firstValueFrom(this.userPublicKey$(activeUserId));
|
||||
if (activeUserId == null) {
|
||||
throw new Error("No active user found.");
|
||||
}
|
||||
publicKey = (await firstValueFrom(this.userPublicKey$(activeUserId))) as Uint8Array;
|
||||
}
|
||||
|
||||
if (publicKey === null) {
|
||||
@@ -510,12 +552,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
* Clears the user's key pair
|
||||
* @param userId The desired user
|
||||
*/
|
||||
private async clearKeyPair(userId: UserId): Promise<void[]> {
|
||||
if (userId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
private async clearKeyPair(userId: UserId): Promise<void> {
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
|
||||
}
|
||||
|
||||
@@ -596,8 +633,8 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
// ---HELPERS---
|
||||
async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> {
|
||||
if (!key) {
|
||||
async validateUserKey(key: UserKey | MasterKey | null, userId: UserId): Promise<boolean> {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -659,10 +696,14 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
|
||||
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
|
||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||
if (privateKey.encryptedString == null) {
|
||||
throw new Error("Failed to create valid private key.");
|
||||
}
|
||||
|
||||
await this.setUserKey(userKey, activeUserId);
|
||||
await this.stateProvider
|
||||
.getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.update(() => privateKey.encryptedString);
|
||||
.update(() => privateKey.encryptedString!);
|
||||
|
||||
return {
|
||||
userKey,
|
||||
@@ -692,7 +733,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
if (storePin) {
|
||||
// Decrypt userKeyEncryptedPin with user key
|
||||
const pin = await this.encryptService.decryptToUtf8(
|
||||
await this.pinService.getUserKeyEncryptedPin(userId),
|
||||
(await this.pinService.getUserKeyEncryptedPin(userId))!,
|
||||
key,
|
||||
);
|
||||
|
||||
@@ -718,7 +759,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId) {
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId) {
|
||||
let shouldStoreKey = false;
|
||||
switch (keySuffix) {
|
||||
case KeySuffixOptions.Auto: {
|
||||
@@ -744,7 +785,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
protected async getKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: UserId,
|
||||
): Promise<UserKey> {
|
||||
): Promise<UserKey | null> {
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
|
||||
if (userKey) {
|
||||
@@ -754,7 +795,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||
protected async clearAllStoredUserKeys(userId: UserId): Promise<void> {
|
||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
@@ -783,7 +824,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
encryptionKey: SymmetricCryptoKey,
|
||||
newSymKey: Uint8Array,
|
||||
): Promise<[T, EncString]> {
|
||||
let protectedSymKey: EncString = null;
|
||||
let protectedSymKey: EncString;
|
||||
if (encryptionKey.key.byteLength === 32) {
|
||||
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
|
||||
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
|
||||
@@ -803,12 +844,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
} else if (keySuffix === KeySuffixOptions.Pin) {
|
||||
} else if (keySuffix === KeySuffixOptions.Pin && userId != null) {
|
||||
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
|
||||
}
|
||||
}
|
||||
|
||||
userKey$(userId: UserId): Observable<UserKey> {
|
||||
userKey$(userId: UserId): Observable<UserKey | null> {
|
||||
return this.stateProvider.getUser(userId, USER_KEY).state$;
|
||||
}
|
||||
|
||||
@@ -822,7 +863,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
// Legacy path
|
||||
return this.masterPasswordService.masterKey$(userId).pipe(
|
||||
switchMap(async (masterKey) => {
|
||||
if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) {
|
||||
if (!(await this.validateUserKey(masterKey, userId))) {
|
||||
// We don't have a UserKey or a valid MasterKey
|
||||
return null;
|
||||
}
|
||||
@@ -841,7 +882,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
private async derivePublicKey(privateKey: UserPrivateKey) {
|
||||
private async derivePublicKey(privateKey: UserPrivateKey | null) {
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -849,16 +890,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
||||
}
|
||||
|
||||
userPrivateKey$(userId: UserId): Observable<UserPrivateKey> {
|
||||
return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey));
|
||||
userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null> {
|
||||
return this.userPrivateKeyHelper$(userId, false).pipe(
|
||||
map((keys) => keys?.userPrivateKey ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString> {
|
||||
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null> {
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
|
||||
}
|
||||
|
||||
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey> {
|
||||
return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey));
|
||||
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null> {
|
||||
return this.userPrivateKeyHelper$(userId, true).pipe(
|
||||
map((keys) => keys?.userPrivateKey ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) {
|
||||
@@ -884,7 +929,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
private async decryptPrivateKey(encryptedPrivateKey: EncryptedString, key: SymmetricCryptoKey) {
|
||||
private async decryptPrivateKey(
|
||||
encryptedPrivateKey: EncryptedString | null,
|
||||
key: SymmetricCryptoKey,
|
||||
) {
|
||||
if (encryptedPrivateKey == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -916,7 +964,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
private providerKeysHelper$(
|
||||
userId: UserId,
|
||||
userPrivateKey: UserPrivateKey,
|
||||
): Observable<Record<ProviderId, ProviderKey>> {
|
||||
): Observable<Record<ProviderId, ProviderKey> | null> {
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe(
|
||||
// Convert each value in the record to it's own decryption observable
|
||||
convertValues(async (_, value) => {
|
||||
@@ -943,12 +991,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> {
|
||||
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys));
|
||||
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys ?? null));
|
||||
}
|
||||
|
||||
encryptedOrgKeys$(
|
||||
userId: UserId,
|
||||
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>> {
|
||||
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData> | null> {
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$;
|
||||
}
|
||||
|
||||
@@ -956,7 +1004,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
userId: UserId,
|
||||
legacySupport: boolean = false,
|
||||
): Observable<CipherDecryptionKeys | null> {
|
||||
return this.userPrivateKeyHelper$(userId, legacySupport).pipe(
|
||||
return this.userPrivateKeyHelper$(userId, legacySupport)?.pipe(
|
||||
switchMap((userKeys) => {
|
||||
if (userKeys == null) {
|
||||
return of(null);
|
||||
@@ -975,15 +1023,22 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
]).pipe(
|
||||
switchMap(async ([encryptedOrgKeys, providerKeys]) => {
|
||||
const result: Record<OrganizationId, OrgKey> = {};
|
||||
for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) {
|
||||
encryptedOrgKeys = encryptedOrgKeys ?? {};
|
||||
for (const orgId of Object.keys(encryptedOrgKeys) as OrganizationId[]) {
|
||||
if (result[orgId] != null) {
|
||||
continue;
|
||||
}
|
||||
const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]);
|
||||
if (encrypted == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let decrypted: OrgKey;
|
||||
|
||||
if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) {
|
||||
if (providerKeys == null) {
|
||||
throw new Error("No provider keys found.");
|
||||
}
|
||||
decrypted = await encrypted.decrypt(this.encryptService, providerKeys);
|
||||
} else {
|
||||
decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey);
|
||||
|
||||
Reference in New Issue
Block a user