1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-8933] Require userId on setUserKey (#9675)

* Updated all sets of user key to pass in userId

* Added userId on auth request login.

* Fixed tests.

* Fixed tests to pass in UserId

* Added parameter to tests.

* Addressed PR feedback.

* Merged main
This commit is contained in:
Todd Martin
2024-08-13 08:07:36 -04:00
committed by GitHub
parent cdc82f13b0
commit 7b508b1ad7
15 changed files with 65 additions and 39 deletions

View File

@@ -1067,7 +1067,6 @@ export default class MainBackground {
this.messagingService, this.messagingService,
this.appIdService, this.appIdService,
this.platformUtilsService, this.platformUtilsService,
this.stateService,
this.logService, this.logService,
this.authService, this.authService,
this.biometricStateService, this.biometricStateService,

View File

@@ -9,7 +9,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
@@ -79,7 +78,6 @@ export class NativeMessagingBackground {
private messagingService: MessagingService, private messagingService: MessagingService,
private appIdService: AppIdService, private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private stateService: StateService,
private logService: LogService, private logService: LogService,
private authService: AuthService, private authService: AuthService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
@@ -214,7 +212,7 @@ export class NativeMessagingBackground {
await this.connect(); await this.connect();
} }
message.userId = await this.stateService.getUserId(); message.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
message.timestamp = Date.now(); message.timestamp = Date.now();
if (this.platformUtilsService.isSafari()) { if (this.platformUtilsService.isSafari()) {
@@ -367,13 +365,14 @@ export class NativeMessagingBackground {
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
this.publicKey = publicKey; this.publicKey = publicKey;
this.privateKey = privateKey; this.privateKey = privateKey;
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendUnencrypted({ this.sendUnencrypted({
command: "setupEncryption", command: "setupEncryption",
publicKey: Utils.fromBufferToB64(publicKey), publicKey: Utils.fromBufferToB64(publicKey),
userId: await this.stateService.getUserId(), userId: userId,
}); });
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
@@ -391,7 +390,7 @@ export class NativeMessagingBackground {
private async showFingerprintDialog() { private async showFingerprintDialog() {
const fingerprint = await this.cryptoService.getFingerprint( const fingerprint = await this.cryptoService.getFingerprint(
await this.stateService.getUserId(), (await firstValueFrom(this.accountService.activeAccount$))?.id,
this.publicKey, this.publicKey,
); );

View File

@@ -69,7 +69,7 @@ export class UnlockCommand {
} }
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey, userId);
if (await this.keyConnectorService.getConvertAccountRequired()) { if (await this.keyConnectorService.getConvertAccountRequired()) {
const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand( const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(

View File

@@ -69,7 +69,7 @@ export class ElectronCryptoService extends CryptoService {
await super.clearStoredUserKey(keySuffix, userId); await super.clearStoredUserKey(keySuffix, userId);
} }
protected override async storeAdditionalKeys(key: UserKey, userId?: UserId) { protected override async storeAdditionalKeys(key: UserKey, userId: UserId) {
await super.storeAdditionalKeys(key, userId); await super.storeAdditionalKeys(key, userId);
const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId); const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);

View File

@@ -134,10 +134,13 @@ export class LockComponent implements OnInit, OnDestroy {
} }
await this.biometricStateService.setUserPromptCancelled(); await this.biometricStateService.setUserPromptCancelled();
const userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric); const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
this.activeUserId,
);
if (userKey) { if (userKey) {
await this.setUserKeyAndContinue(userKey, false); await this.setUserKeyAndContinue(userKey, this.activeUserId, false);
} }
return !!userKey; return !!userKey;
@@ -174,7 +177,7 @@ export class LockComponent implements OnInit, OnDestroy {
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId); const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
if (userKey) { if (userKey) {
await this.setUserKeyAndContinue(userKey); await this.setUserKeyAndContinue(userKey, userId);
return; // successfully unlocked return; // successfully unlocked
} }
@@ -257,11 +260,15 @@ export class LockComponent implements OnInit, OnDestroy {
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
response.masterKey, response.masterKey,
); );
await this.setUserKeyAndContinue(userKey, true); await this.setUserKeyAndContinue(userKey, userId, true);
} }
private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) { private async setUserKeyAndContinue(
await this.cryptoService.setUserKey(key); key: UserKey,
userId: UserId,
evaluatePasswordAfterUnlock = false,
) {
await this.cryptoService.setUserKey(key, userId);
// Now that we have a decrypted user key in memory, we can check if we // Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device // need to establish trust on the current device

View File

@@ -386,6 +386,7 @@ export class LoginViaAuthRequestComponent
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse, adminAuthReqResponse,
privateKey, privateKey,
userId,
); );
} else { } else {
// Flow 3: masterPasswordHash is null // Flow 3: masterPasswordHash is null
@@ -393,6 +394,7 @@ export class LoginViaAuthRequestComponent
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse, adminAuthReqResponse,
privateKey, privateKey,
userId,
); );
} }

View File

@@ -49,19 +49,23 @@ export abstract class AuthRequestServiceAbstraction {
* Sets the `UserKey` from an auth request. Auth request must have a `UserKey`. * Sets the `UserKey` from an auth request. Auth request must have a `UserKey`.
* @param authReqResponse The auth request. * @param authReqResponse The auth request.
* @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request.
* @param userId The ID of the user for whose account we will set the key.
*/ */
abstract setUserKeyAfterDecryptingSharedUserKey: ( abstract setUserKeyAfterDecryptingSharedUserKey: (
authReqResponse: AuthRequestResponse, authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer, authReqPrivateKey: ArrayBuffer,
userId: UserId,
) => Promise<void>; ) => Promise<void>;
/** /**
* Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`. * Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`.
* @param authReqResponse The auth request. * @param authReqResponse The auth request.
* @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request.
* @param userId The ID of the user for whose account we will set the keys.
*/ */
abstract setKeysAfterDecryptingSharedMasterKeyAndHash: ( abstract setKeysAfterDecryptingSharedMasterKeyAndHash: (
authReqResponse: AuthRequestResponse, authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer, authReqPrivateKey: ArrayBuffer,
userId: UserId,
) => Promise<void>; ) => Promise<void>;
/** /**
* Decrypts a `UserKey` from a public key encrypted `UserKey`. * Decrypts a `UserKey` from a public key encrypted `UserKey`.

View File

@@ -159,7 +159,7 @@ describe("AuthRequestLoginStrategy", () => {
mockUserId, mockUserId,
); );
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId);
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
}); });
@@ -184,7 +184,7 @@ describe("AuthRequestLoginStrategy", () => {
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
// trustDeviceIfRequired should be called // trustDeviceIfRequired should be called

View File

@@ -102,7 +102,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (authRequestCredentials.decryptedUserKey) { if (authRequestCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey); await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey, userId);
} else { } else {
await this.trySetUserKeyWithMasterKey(userId); await this.trySetUserKeyWithMasterKey(userId);
@@ -115,7 +115,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) { if (masterKey) {
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey, userId);
} }
} }

View File

@@ -252,7 +252,7 @@ describe("SsoLoginStrategy", () => {
expect(deviceTrustService.getDeviceKey).toHaveBeenCalledTimes(1); expect(deviceTrustService.getDeviceKey).toHaveBeenCalledTimes(1);
expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1);
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey, userId);
}); });
it("does not set the user key when deviceKey is missing", async () => { it("does not set the user key when deviceKey is missing", async () => {
@@ -498,7 +498,7 @@ describe("SsoLoginStrategy", () => {
undefined, undefined,
undefined, undefined,
); );
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
}); });
}); });
@@ -554,7 +554,7 @@ describe("SsoLoginStrategy", () => {
undefined, undefined,
undefined, undefined,
); );
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
}); });
}); });
}); });

View File

@@ -255,6 +255,7 @@ export class SsoLoginStrategy extends LoginStrategy {
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse, adminAuthReqResponse,
adminAuthReqStorable.privateKey, adminAuthReqStorable.privateKey,
userId,
); );
} else { } else {
// if masterPasswordHash is null, we will always receive authReqResponse.key // if masterPasswordHash is null, we will always receive authReqResponse.key
@@ -262,6 +263,7 @@ export class SsoLoginStrategy extends LoginStrategy {
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse, adminAuthReqResponse,
adminAuthReqStorable.privateKey, adminAuthReqStorable.privateKey,
userId,
); );
} }
@@ -321,7 +323,7 @@ export class SsoLoginStrategy extends LoginStrategy {
); );
if (userKey) { if (userKey) {
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey, userId);
} }
} }
@@ -337,7 +339,7 @@ export class SsoLoginStrategy extends LoginStrategy {
} }
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey, userId);
} }
protected override async setPrivateKey( protected override async setPrivateKey(

View File

@@ -133,14 +133,18 @@ describe("AuthRequestService", () => {
cryptoService.setUserKey.mockResolvedValueOnce(undefined); cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act // Act
await sut.setUserKeyAfterDecryptingSharedUserKey(mockAuthReqResponse, mockPrivateKey); await sut.setUserKeyAfterDecryptingSharedUserKey(
mockAuthReqResponse,
mockPrivateKey,
mockUserId,
);
// Assert // Assert
expect(sut.decryptPubKeyEncryptedUserKey).toBeCalledWith( expect(sut.decryptPubKeyEncryptedUserKey).toBeCalledWith(
mockAuthReqResponse.key, mockAuthReqResponse.key,
mockPrivateKey, mockPrivateKey,
); );
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey); expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey, mockUserId);
}); });
}); });
@@ -169,7 +173,11 @@ describe("AuthRequestService", () => {
cryptoService.setUserKey.mockResolvedValueOnce(undefined); cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act // Act
await sut.setKeysAfterDecryptingSharedMasterKeyAndHash(mockAuthReqResponse, mockPrivateKey); await sut.setKeysAfterDecryptingSharedMasterKeyAndHash(
mockAuthReqResponse,
mockPrivateKey,
mockUserId,
);
// Assert // Assert
expect(sut.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith( expect(sut.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith(
@@ -190,7 +198,7 @@ describe("AuthRequestService", () => {
undefined, undefined,
undefined, undefined,
); );
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey, mockUserId);
}); });
}); });

View File

@@ -126,17 +126,19 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
async setUserKeyAfterDecryptingSharedUserKey( async setUserKeyAfterDecryptingSharedUserKey(
authReqResponse: AuthRequestResponse, authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array, authReqPrivateKey: Uint8Array,
userId: UserId,
) { ) {
const userKey = await this.decryptPubKeyEncryptedUserKey( const userKey = await this.decryptPubKeyEncryptedUserKey(
authReqResponse.key, authReqResponse.key,
authReqPrivateKey, authReqPrivateKey,
); );
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey, userId);
} }
async setKeysAfterDecryptingSharedMasterKeyAndHash( async setKeysAfterDecryptingSharedMasterKeyAndHash(
authReqResponse: AuthRequestResponse, authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array, authReqPrivateKey: Uint8Array,
userId: UserId,
) { ) {
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash( const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
authReqResponse.key, authReqResponse.key,
@@ -148,11 +150,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails) // Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
await this.masterPasswordService.setMasterKey(masterKey, userId); await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey, userId);
} }
// Decryption helpers // Decryption helpers

View File

@@ -263,6 +263,12 @@ describe("cryptoService", () => {
await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
}); });
it("throws if userId is null", async () => {
await expect(cryptoService.setUserKey(mockUserKey, null)).rejects.toThrow(
"No userId provided.",
);
});
describe("Pin Key refresh", () => { describe("Pin Key refresh", () => {
const mockPinKeyEncryptedUserKey = new EncString( const mockPinKeyEncryptedUserKey = new EncString(
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=", "2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",

View File

@@ -89,12 +89,16 @@ export class CryptoService implements CryptoServiceAbstraction {
); );
} }
async setUserKey(key: UserKey, userId?: UserId): Promise<void> { async setUserKey(key: UserKey, userId: UserId): Promise<void> {
if (key == null) { if (key == null) {
throw new Error("No key provided. Lock the user to clear the key"); throw new Error("No key provided. Lock the user to clear the key");
} }
if (userId == null) {
throw new Error("No userId provided.");
}
// Set userId to ensure we have one for the account status update // Set userId to ensure we have one for the account status update
[userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId); await this.stateProvider.setUserState(USER_KEY, key, userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
await this.storeAdditionalKeys(key, userId); await this.storeAdditionalKeys(key, userId);
@@ -701,13 +705,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* @param key The user key * @param key The user key
* @param userId The desired user * @param userId The desired user
*/ */
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) { 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); const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) { if (storeAuto) {
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });