mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 19:23:52 +00:00
[PM-23230] Implement KDF Change Service (#15748)
* Add new mp service api * Fix tests * Add test coverage * Add newline * Fix type * Rename to "unwrapUserKeyFromMasterPasswordUnlockData" * Fix build * Fix build on cli * Fix linting * Re-sort spec * Add tests * Fix test and build issues * Fix build * Clean up * Remove introduced function * Clean up comments * Fix abstract class types * Fix comments * Cleanup * Cleanup * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/services/master-password.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add comments * Fix build * Add arg null check * Cleanup * Fix build * Fix build on browser * Implement KDF change service * Deprecate encryptUserKeyWithMasterKey * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for null params * Fix builds * Cleanup and deprecate more functions * Fix formatting * Prettier * Clean up * Update libs/key-management/src/abstractions/key.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Make emailToSalt private and expose abstract saltForUser * Add tests * Add docs * Fix build * Fix tests * Fix tests * Address feedback and fix primitive obsession * Consolidate active account checks in change kdf confirmation component * Update libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add defensive parameter checks * Add tests * Add comment for follow-up epic * Move change kdf service, remove abstraction and add api service * Fix test * Drop redundant null check * Address feedback * Add throw on empty password * Fix tests * Mark change kdf service as internal * Add abstract classes * Switch to abstraction * use sdk EncString in MasterPasswordUnlockData * fix remaining tests --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
@@ -27,6 +27,11 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
* @throws If the user ID is provided, but the user is not found.
|
||||
*/
|
||||
abstract saltForUser$: (userId: UserId) => Observable<MasterPasswordSalt>;
|
||||
/**
|
||||
* Converts an email to a master password salt. This is a canonical encoding of the
|
||||
* email, no matter how the email is capitalized.
|
||||
*/
|
||||
abstract emailToSalt(email: string): MasterPasswordSalt;
|
||||
/**
|
||||
* An observable that emits the master key for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { makeEncString } from "../../../../../spec";
|
||||
|
||||
import { MasterPasswordUnlockResponse } from "./master-password-unlock.response";
|
||||
|
||||
describe("MasterPasswordUnlockResponse", () => {
|
||||
const salt = "test@example.com";
|
||||
const encryptedUserKey = makeEncString("testUserKey");
|
||||
const encryptedUserKey = "testUserKey";
|
||||
const testKdfResponse = { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000 };
|
||||
|
||||
it("should throw error when salt is not provided", () => {
|
||||
@@ -15,7 +13,7 @@ describe("MasterPasswordUnlockResponse", () => {
|
||||
new MasterPasswordUnlockResponse({
|
||||
Salt: undefined,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||
});
|
||||
}).toThrow("MasterPasswordUnlockResponse does not contain a valid salt");
|
||||
});
|
||||
@@ -36,7 +34,7 @@ describe("MasterPasswordUnlockResponse", () => {
|
||||
const response = new MasterPasswordUnlockResponse({
|
||||
Salt: salt,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||
});
|
||||
|
||||
expect(response.salt).toBe(salt);
|
||||
@@ -50,7 +48,7 @@ describe("MasterPasswordUnlockResponse", () => {
|
||||
const response = new MasterPasswordUnlockResponse({
|
||||
Salt: salt,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||
});
|
||||
|
||||
const unlockData = response.toMasterPasswordUnlockData();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
import { EncString } from "../../../crypto/models/enc-string";
|
||||
import { KdfConfigResponse } from "../../../models/response/kdf-config.response";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
@@ -29,9 +28,7 @@ export class MasterPasswordUnlockResponse extends BaseResponse {
|
||||
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
|
||||
);
|
||||
}
|
||||
this.masterKeyWrappedUserKey = new EncString(
|
||||
masterKeyEncryptedUserKey,
|
||||
) as MasterKeyWrappedUserKey;
|
||||
this.masterKeyWrappedUserKey = masterKeyEncryptedUserKey as MasterKeyWrappedUserKey;
|
||||
}
|
||||
|
||||
toMasterPasswordUnlockData() {
|
||||
|
||||
@@ -33,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
this.masterKeyHashSubject.next(initialMasterKeyHash);
|
||||
}
|
||||
|
||||
emailToSalt(email: string): MasterPasswordSalt {
|
||||
return this.mock.emailToSalt(email);
|
||||
}
|
||||
|
||||
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
||||
return this.mock.saltForUser$(userId);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
makeEncString,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../spec";
|
||||
@@ -385,7 +384,7 @@ describe("MasterPasswordService", () => {
|
||||
const kdfPBKDF2: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3);
|
||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||
const encryptedUserKey = makeEncString("testUserKet") as MasterKeyWrappedUserKey;
|
||||
const encryptedUserKey = "testUserKet" as MasterKeyWrappedUserKey;
|
||||
|
||||
it("returns null when value is null", () => {
|
||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(
|
||||
@@ -401,7 +400,7 @@ describe("MasterPasswordService", () => {
|
||||
kdfType: KdfType.PBKDF2_SHA256,
|
||||
iterations: kdfPBKDF2.iterations,
|
||||
},
|
||||
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
|
||||
masterKeyWrappedUserKey: encryptedUserKey as string,
|
||||
};
|
||||
|
||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
||||
@@ -419,7 +418,7 @@ describe("MasterPasswordService", () => {
|
||||
memory: kdfArgon2.memory,
|
||||
parallelism: kdfArgon2.parallelism,
|
||||
},
|
||||
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
|
||||
masterKeyWrappedUserKey: encryptedUserKey as string,
|
||||
};
|
||||
|
||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
||||
|
||||
@@ -132,7 +132,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
return EncString.fromJSON(key);
|
||||
}
|
||||
|
||||
private emailToSalt(email: string): MasterPasswordSalt {
|
||||
emailToSalt(email: string): MasterPasswordSalt {
|
||||
return email.toLowerCase().trim() as MasterPasswordSalt;
|
||||
}
|
||||
|
||||
@@ -256,6 +256,9 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(kdf, "kdf");
|
||||
assertNonNullish(salt, "salt");
|
||||
if (password === "") {
|
||||
throw new Error("Master password cannot be empty.");
|
||||
}
|
||||
|
||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||
@@ -294,18 +297,19 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
assertNonNullish(kdf, "kdf");
|
||||
assertNonNullish(salt, "salt");
|
||||
assertNonNullish(userKey, "userKey");
|
||||
if (password === "") {
|
||||
throw new Error("Master password cannot be empty.");
|
||||
}
|
||||
|
||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
const masterKeyWrappedUserKey = new EncString(
|
||||
PureCrypto.encrypt_user_key_with_master_password(
|
||||
userKey.toEncoded(),
|
||||
password,
|
||||
salt,
|
||||
kdf.toSdkConfig(),
|
||||
),
|
||||
const masterKeyWrappedUserKey = PureCrypto.encrypt_user_key_with_master_password(
|
||||
userKey.toEncoded(),
|
||||
password,
|
||||
salt,
|
||||
kdf.toSdkConfig(),
|
||||
) as MasterKeyWrappedUserKey;
|
||||
return new MasterPasswordUnlockData(salt, kdf, masterKeyWrappedUserKey);
|
||||
}
|
||||
@@ -320,7 +324,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
await SdkLoadService.Ready;
|
||||
const userKey = new SymmetricCryptoKey(
|
||||
PureCrypto.decrypt_user_key_with_master_password(
|
||||
masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString,
|
||||
masterPasswordUnlockData.masterKeyWrappedUserKey,
|
||||
password,
|
||||
masterPasswordUnlockData.salt,
|
||||
masterPasswordUnlockData.kdf.toSdkConfig(),
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import { EncString } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* The Base64-encoded master password authentication hash, that is sent to the server for authentication.
|
||||
@@ -13,7 +12,7 @@ export type MasterPasswordAuthenticationHash = Opaque<string, "MasterPasswordAut
|
||||
* You MUST obtain this through the emailToSalt function in MasterPasswordService
|
||||
*/
|
||||
export type MasterPasswordSalt = Opaque<string, "MasterPasswordSalt">;
|
||||
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterPasswordSalt">;
|
||||
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterKeyWrappedUserKey">;
|
||||
|
||||
/**
|
||||
* The data required to unlock with the master password.
|
||||
@@ -29,7 +28,7 @@ export class MasterPasswordUnlockData {
|
||||
return {
|
||||
salt: this.salt,
|
||||
kdf: this.kdf,
|
||||
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey.toJSON(),
|
||||
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ export class MasterPasswordUnlockData {
|
||||
obj.kdf.kdfType === KdfType.PBKDF2_SHA256
|
||||
? PBKDF2KdfConfig.fromJSON(obj.kdf)
|
||||
: Argon2KdfConfig.fromJSON(obj.kdf),
|
||||
EncString.fromJSON(obj.masterKeyWrappedUserKey) as MasterKeyWrappedUserKey,
|
||||
obj.masterKeyWrappedUserKey as MasterKeyWrappedUserKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user