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

[PM-24377] Use PureCrypto for decryptUserKeyWithMasterKey on the master password service (#16522)

* use PureCrypto in master password service decryptUserKeyWithMasterKey

* test for legacy AES256-CBC

* update SDK version to include the `PureCrypto.decrypt_user_key_with_master_key`

* change from integration to unit tests, use fake state provider
This commit is contained in:
Maciej Zieniuk
2025-10-17 19:28:38 +02:00
committed by GitHub
parent d65824e624
commit 8f0d509682
5 changed files with 68 additions and 83 deletions

View File

@@ -698,9 +698,7 @@ export default class MainBackground {
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
this.logService,
this.cryptoFunctionService,
this.accountService,

View File

@@ -455,9 +455,7 @@ export class ServiceContainer {
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
this.logService,
this.cryptoFunctionService,
this.accountService,

View File

@@ -1069,9 +1069,7 @@ const safeProviders: SafeProvider[] = [
useClass: MasterPasswordService,
deps: [
StateProvider,
StateServiceAbstraction,
KeyGenerationService,
EncryptService,
LogService,
CryptoFunctionServiceAbstraction,
AccountServiceAbstraction,

View File

@@ -6,22 +6,22 @@ import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-
import { Utils } from "@bitwarden/common/platform/misc/utils";
// eslint-disable-next-line no-restricted-imports
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { PureCrypto } from "@bitwarden/sdk-internal";
import {
FakeAccountService,
FakeStateProvider,
makeEncString,
makeSymmetricCryptoKey,
mockAccountServiceWith,
} from "../../../../spec";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";
import {
MasterKeyWrappedUserKey,
@@ -31,6 +31,7 @@ import {
import {
FORCE_SET_PASSWORD_REASON,
MASTER_KEY,
MASTER_KEY_ENCRYPTED_USER_KEY,
MASTER_PASSWORD_UNLOCK_KEY,
MasterPasswordService,
@@ -39,9 +40,7 @@ import {
describe("MasterPasswordService", () => {
let sut: MasterPasswordService;
let stateService: MockProxy<StateService>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let encryptService: MockProxy<EncryptService>;
let logService: MockProxy<LogService>;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let accountService: FakeAccountService;
@@ -53,18 +52,12 @@ describe("MasterPasswordService", () => {
const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3);
const salt = "test@bitwarden.com" as MasterPasswordSalt;
const userKey = makeSymmetricCryptoKey(64, 2) as UserKey;
const testUserKey: SymmetricCryptoKey = makeSymmetricCryptoKey(64, 1);
const testMasterKey: MasterKey = makeSymmetricCryptoKey(32, 2);
const testStretchedMasterKey: SymmetricCryptoKey = makeSymmetricCryptoKey(64, 3);
const testMasterKeyEncryptedKey =
"0.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc=";
const testStretchedMasterKeyEncryptedKey =
"2.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc=|DeUFkhIwgkGdZA08bDnDqMMNmZk21D+H5g8IostPKAY=";
const sdkLoadServiceReady = jest.fn();
beforeEach(() => {
stateService = mock<StateService>();
keyGenerationService = mock<KeyGenerationService>();
encryptService = mock<EncryptService>();
logService = mock<LogService>();
cryptoFunctionService = mock<CryptoFunctionService>();
accountService = mockAccountServiceWith(userId);
@@ -72,18 +65,18 @@ describe("MasterPasswordService", () => {
sut = new MasterPasswordService(
stateProvider,
stateService,
keyGenerationService,
encryptService,
logService,
cryptoFunctionService,
accountService,
);
encryptService.unwrapSymmetricKey.mockResolvedValue(makeSymmetricCryptoKey(64, 1));
keyGenerationService.stretchKey.mockResolvedValue(makeSymmetricCryptoKey(64, 3));
Object.defineProperty(SdkLoadService, "Ready", {
value: Promise.resolve(),
value: new Promise((resolve) => {
sdkLoadServiceReady();
resolve(undefined);
}),
configurable: true,
});
});
@@ -159,41 +152,62 @@ describe("MasterPasswordService", () => {
expect(state).toEqual(ForceSetPasswordReason.None);
});
});
describe("decryptUserKeyWithMasterKey", () => {
it("decrypts a userkey wrapped in AES256-CBC", async () => {
encryptService.unwrapSymmetricKey.mockResolvedValue(testUserKey);
await sut.decryptUserKeyWithMasterKey(
testMasterKey,
const masterKey = makeSymmetricCryptoKey(64, 0) as MasterKey;
const userKey = makeSymmetricCryptoKey(64, 1) as UserKey;
const masterKeyEncryptedUserKey = makeEncString("test-encrypted-user-key");
const decryptUserKeyWithMasterKeyMock = jest.spyOn(
PureCrypto,
"decrypt_user_key_with_master_key",
);
beforeEach(() => {
decryptUserKeyWithMasterKeyMock.mockReturnValue(userKey.toEncoded());
});
it("successfully decrypts", async () => {
const decryptedUserKey = await sut.decryptUserKeyWithMasterKey(
masterKey,
userId,
new EncString(testMasterKeyEncryptedKey),
masterKeyEncryptedUserKey,
);
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(
new EncString(testMasterKeyEncryptedKey),
testMasterKey,
expect(decryptedUserKey).toEqual(new SymmetricCryptoKey(userKey.toEncoded()));
expect(sdkLoadServiceReady).toHaveBeenCalled();
expect(PureCrypto.decrypt_user_key_with_master_key).toHaveBeenCalledWith(
masterKeyEncryptedUserKey.toSdk(),
masterKey.toEncoded(),
);
expect(sdkLoadServiceReady.mock.invocationCallOrder[0]).toBeLessThan(
decryptUserKeyWithMasterKeyMock.mock.invocationCallOrder[0],
);
});
it("decrypts a userkey wrapped in AES256-CBC-HMAC", async () => {
encryptService.unwrapSymmetricKey.mockResolvedValue(testUserKey);
keyGenerationService.stretchKey.mockResolvedValue(testStretchedMasterKey);
await sut.decryptUserKeyWithMasterKey(
testMasterKey,
it("returns null when failed to decrypt", async () => {
decryptUserKeyWithMasterKeyMock.mockImplementation(() => {
throw new Error("Decryption failed");
});
const decryptedUserKey = await sut.decryptUserKeyWithMasterKey(
masterKey,
userId,
new EncString(testStretchedMasterKeyEncryptedKey),
masterKeyEncryptedUserKey,
);
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(
new EncString(testStretchedMasterKeyEncryptedKey),
testStretchedMasterKey,
);
expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(testMasterKey);
expect(decryptedUserKey).toBeNull();
});
it("returns null if failed to decrypt", async () => {
encryptService.unwrapSymmetricKey.mockRejectedValue(new Error("Decryption failed"));
const result = await sut.decryptUserKeyWithMasterKey(
testMasterKey,
userId,
new EncString(testStretchedMasterKeyEncryptedKey),
);
expect(result).toBeNull();
it("returns error when master key is null", async () => {
stateProvider.singleUser.getFake(userId, MASTER_KEY).nextState(null);
await expect(
sut.decryptUserKeyWithMasterKey(
null as unknown as MasterKey,
userId,
masterKeyEncryptedUserKey,
),
).rejects.toThrow("No master key found.");
});
});
@@ -331,11 +345,11 @@ describe("MasterPasswordService", () => {
it.each([kdfPBKDF2, kdfArgon2])(
"sets the master password unlock data kdf %o in the state",
async (kdfConfig) => {
const masterPasswordUnlockData = await sut.makeMasterPasswordUnlockData(
"test-password",
kdfConfig,
const masterKeyWrappedUserKey = makeEncString().toSdk() as MasterKeyWrappedUserKey;
const masterPasswordUnlockData = new MasterPasswordUnlockData(
salt,
userKey,
kdfConfig,
masterKeyWrappedUserKey,
);
await sut.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);

View File

@@ -12,8 +12,6 @@ import { PureCrypto } from "@bitwarden/sdk-internal";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncryptionType } from "../../../platform/enums";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import {
MASTER_PASSWORD_DISK,
@@ -26,7 +24,6 @@ import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../../crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
import {
@@ -38,7 +35,7 @@ import {
} from "../types/master-password.types";
/** Memory since master key shouldn't be available on lock */
const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMORY, "masterKey", {
export const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMORY, "masterKey", {
deserializer: (masterKey) => SymmetricCryptoKey.fromJSON(masterKey) as MasterKey,
clearOn: ["lock", "logout"],
});
@@ -82,9 +79,7 @@ export const MASTER_PASSWORD_UNLOCK_KEY = new UserKeyDefinition<MasterPasswordUn
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
constructor(
private stateProvider: StateProvider,
private stateService: StateService,
private keyGenerationService: KeyGenerationService,
private encryptService: EncryptService,
private logService: LogService,
private cryptoFunctionService: CryptoFunctionService,
private accountService: AccountService,
@@ -219,33 +214,15 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
throw new Error("No master key found.");
}
let decUserKey: SymmetricCryptoKey;
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
try {
decUserKey = await this.encryptService.unwrapSymmetricKey(userKey, masterKey);
} catch {
this.logService.warning("Failed to decrypt user key with master key.");
return null;
}
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
try {
const newKey = await this.keyGenerationService.stretchKey(masterKey);
decUserKey = await this.encryptService.unwrapSymmetricKey(userKey, newKey);
} catch {
this.logService.warning("Failed to decrypt user key with stretched master key.");
return null;
}
} else {
throw new Error("Unsupported encryption type.");
}
if (decUserKey == null) {
this.logService.warning("Failed to decrypt user key with master key, user key is null.");
await SdkLoadService.Ready;
try {
return new SymmetricCryptoKey(
PureCrypto.decrypt_user_key_with_master_key(userKey.toSdk(), masterKey.toEncoded()),
) as UserKey;
} catch {
this.logService.warning("Failed to decrypt user key with master key.");
return null;
}
return decUserKey as UserKey;
}
async makeMasterPasswordAuthenticationData(