mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 09:13:33 +00:00
[PM-16603] Implement userkey rotation v2 (#12646)
* Implement key rotation v2 * Pass through masterpassword hint * Properly split old and new code * Mark legacy rotation as deprecated * Throw when data is null * Cleanup * Add tests * Fix build * Update libs/key-management/src/key.service.spec.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update apps/web/src/app/auth/settings/change-password.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add documentation * Centralize loading logic * Fix build * Remove sharedlib from legacymigration component --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
@@ -326,6 +326,17 @@ export abstract class KeyService {
|
||||
*/
|
||||
abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null>;
|
||||
|
||||
/**
|
||||
* Gets an observable stream of the given users decrypted private key and public key, guaranteed to be consistent.
|
||||
* Will emit null if the user doesn't have a userkey to decrypt the encrypted private key, or null if the user doesn't have a private key
|
||||
* at all.
|
||||
*
|
||||
* @param userId The user id of the user to get the data for.
|
||||
*/
|
||||
abstract userEncryptionKeyPair$(
|
||||
userId: UserId,
|
||||
): Observable<{ privateKey: UserPrivateKey; publicKey: UserPublicKey } | null>;
|
||||
|
||||
/**
|
||||
* Generates a fingerprint phrase for the user based on their public key
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
|
||||
import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
|
||||
@@ -802,4 +802,51 @@ describe("keyService", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("userPrivateKey$", () => {
|
||||
type SetupKeysParams = {
|
||||
makeMasterKey: boolean;
|
||||
makeUserKey: boolean;
|
||||
};
|
||||
|
||||
function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] {
|
||||
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
|
||||
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
|
||||
masterPasswordService.masterKeySubject.next(fakeMasterKey);
|
||||
userKeyState.nextState(null);
|
||||
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
|
||||
userKeyState.nextState(fakeUserKey);
|
||||
return [fakeUserKey, fakeMasterKey];
|
||||
}
|
||||
|
||||
it("returns null when private key is null", async () => {
|
||||
setupKeys({ makeMasterKey: false, makeUserKey: false });
|
||||
|
||||
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
|
||||
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
|
||||
expect(key).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns null when private key is undefined", async () => {
|
||||
setupKeys({ makeUserKey: true, makeMasterKey: false });
|
||||
|
||||
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject(undefined));
|
||||
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
|
||||
expect(key).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns keys when private key is defined", async () => {
|
||||
setupKeys({ makeUserKey: false, makeMasterKey: true });
|
||||
|
||||
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject("private key"));
|
||||
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
|
||||
Utils.fromUtf8ToArray("public key"),
|
||||
);
|
||||
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
|
||||
expect(key).toEqual({
|
||||
privateKey: "private key",
|
||||
publicKey: Utils.fromUtf8ToArray("public key"),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -895,6 +895,21 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
userEncryptionKeyPair$(
|
||||
userId: UserId,
|
||||
): Observable<{ privateKey: UserPrivateKey; publicKey: UserPublicKey } | null> {
|
||||
return this.userPrivateKey$(userId).pipe(
|
||||
switchMap(async (privateKey) => {
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publicKey = (await this.derivePublicKey(privateKey))!;
|
||||
return { privateKey, publicKey };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null> {
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user