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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1069,9 +1069,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: MasterPasswordService,
|
||||
deps: [
|
||||
StateProvider,
|
||||
StateServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
EncryptService,
|
||||
LogService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user