1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-23619] Remove getPrivateKey from the key service and update consumers (#15784)

* remove getPrivateKey from keyService

* Update consumer code

* Increase unit test coverage
This commit is contained in:
Thomas Avery
2025-08-05 09:25:50 -05:00
committed by GitHub
parent 7145092889
commit 2a3e1ae1f5
7 changed files with 358 additions and 143 deletions

View File

@@ -294,16 +294,6 @@ export abstract class KeyService {
* @param encPrivateKey An encrypted private key
*/
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
* @returns The user's private key
*
* @throws Error when no active user
*
* @deprecated Use {@link userPrivateKey$} instead.
*/
abstract getPrivateKey(): Promise<Uint8Array | null>;
/**
* Gets an observable stream of the given users decrypted private key, will emit null if the user
@@ -311,6 +301,8 @@ export abstract class KeyService {
* encrypted private key at all.
*
* @param userId The user id of the user to get the data for.
* @returns An observable stream of the decrypted private key or null.
* @throws Error when decryption of the encrypted private key fails.
*/
abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null>;

View File

@@ -494,77 +494,79 @@ describe("keyService", () => {
});
describe("userPrivateKey$", () => {
type SetupKeysParams = {
makeMasterKey: boolean;
makeUserKey: boolean;
};
let mockUserKey: UserKey;
let mockUserPrivateKey: Uint8Array;
let mockEncryptedPrivateKey: EncryptedString;
function setupKeys({
makeMasterKey,
makeUserKey,
}: SetupKeysParams): [UserKey | null, MasterKey | null] {
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];
}
beforeEach(() => {
mockUserKey = makeSymmetricCryptoKey<UserKey>(64);
mockEncryptedPrivateKey = makeEncString("encryptedPrivateKey").encryptedString!;
mockUserPrivateKey = makeStaticByteArray(10, 1);
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
stateProvider.singleUser
.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY)
.nextState(mockEncryptedPrivateKey);
encryptService.unwrapDecapsulationKey.mockResolvedValue(mockUserPrivateKey);
});
it("will return users decrypted private key when user has a user key and encrypted private key set", async () => {
const [userKey] = setupKeys({
makeMasterKey: false,
makeUserKey: true,
it("returns the unwrapped user private key when user key and encrypted private key are set", async () => {
const result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(result).toEqual(mockUserPrivateKey);
expect(encryptService.unwrapDecapsulationKey).toHaveBeenCalledWith(
new EncString(mockEncryptedPrivateKey),
mockUserKey,
);
});
it("throws an error if unwrapping encrypted private key fails", async () => {
encryptService.unwrapDecapsulationKey.mockImplementationOnce(() => {
throw new Error("Unwrapping failed");
});
const userEncryptedPrivateKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
await expect(firstValueFrom(keyService.userPrivateKey$(mockUserId))).rejects.toThrow(
"Unwrapping failed",
);
const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString!);
// Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
encryptService.unwrapDecapsulationKey.mockResolvedValue(fakeDecryptedUserPrivateKey);
const fakeUserPublicKey = makeStaticByteArray(10, 2);
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(fakeUserPublicKey);
const userPrivateKey = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(encryptService.unwrapDecapsulationKey).toHaveBeenCalledWith(
fakeEncryptedUserPrivateKey,
userKey,
);
expect(userPrivateKey).toBe(fakeDecryptedUserPrivateKey);
});
it("returns null user private key when no user key is found", async () => {
setupKeys({ makeMasterKey: false, makeUserKey: false });
it("returns null if user key is not set", async () => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null);
const userPrivateKey = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
const result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(result).toBeNull();
expect(encryptService.unwrapDecapsulationKey).not.toHaveBeenCalled();
expect(userPrivateKey).toBeFalsy();
});
it("returns null when user does not have a private key set", async () => {
setupKeys({ makeUserKey: true, makeMasterKey: false });
it("returns null if encrypted private key is not set", async () => {
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextState(null);
const encryptedUserPrivateKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
encryptedUserPrivateKeyState.nextState(null);
const result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
const userPrivateKey = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(userPrivateKey).toBeFalsy();
expect(result).toBeNull();
expect(encryptService.unwrapDecapsulationKey).not.toHaveBeenCalled();
});
it("reacts to changes in user key or encrypted private key", async () => {
// Initial state: both set
let result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(result).toEqual(mockUserPrivateKey);
// Change user key to null
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null);
result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(result).toBeNull();
// Restore user key, remove encrypted private key
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextState(null);
result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(result).toBeNull();
});
});
@@ -1063,7 +1065,7 @@ describe("keyService", () => {
});
});
describe("userPrivateKey$", () => {
describe("userEncryptionKeyPair$", () => {
type SetupKeysParams = {
makeMasterKey: boolean;
makeUserKey: boolean;

View File

@@ -501,16 +501,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array | null> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("User must be active while attempting to retrieve private key.");
}
return await firstValueFrom(this.userPrivateKey$(activeUserId));
}
// TODO: Make public key required
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
if (publicKey == null) {