1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 19:23:52 +00:00

[PM-23243] In sync response and identity success response add MasterPasswordUnlockDataResponse in decryption options response model. (#15916)

* added master password unlock and decryption option fields into identity token connect response

* incorrect master password unlock response parsing

* use sdk

* use sdk

* better type checking on response parsing

* not using sdk

* revert of bad merge conflicts

* revert of bad merge conflicts

* master password unlock setter in state

* unit test coverage for responses processing

* master password unlock in identity user decryption options

* unit test coverage

* unit test coverage

* unit test coverage

* unit test coverage

* lint error

* set master password unlock data in state on identity response and sync response

* revert change in auth's user decryption options

* remove unnecessary cast

* better docs

* change to relative imports

* MasterPasswordUnlockData serialization issue

* explicit undefined type for `syncUserDecryption`

* incorrect identity token response tests
This commit is contained in:
Maciej Zieniuk
2025-09-05 16:13:56 +02:00
committed by GitHub
parent 6c5e15eb28
commit 203a24723b
24 changed files with 852 additions and 37 deletions

View File

@@ -0,0 +1,111 @@
// eslint-disable-next-line no-restricted-imports
import { Argon2KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { KdfConfigResponse } from "./kdf-config.response";
describe("KdfConfigResponse", () => {
it("should throw error when kdf type not provided", () => {
expect(() => {
new KdfConfigResponse({
KdfType: undefined,
Iterations: 1,
});
}).toThrow("KDF config response does not contain a valid KDF type");
});
it("should throw error when kdf type is PBKDF2 and iterations not provided", () => {
expect(() => {
new KdfConfigResponse({
KdfType: KdfType.PBKDF2_SHA256,
Iterations: undefined,
});
}).toThrow("KDF config response does not contain a valid number of iterations");
});
it("should throw error when kdf type is Argon2Id and iterations not provided", () => {
expect(() => {
new KdfConfigResponse({
KdfType: KdfType.Argon2id,
Iterations: undefined,
});
}).toThrow("KDF config response does not contain a valid number of iterations");
});
it("should throw error when kdf type is Argon2Id and memory not provided", () => {
expect(() => {
new KdfConfigResponse({
KdfType: KdfType.Argon2id,
Iterations: 3,
Memory: undefined,
Parallelism: 4,
});
}).toThrow("KDF config response does not contain a valid memory size for Argon2id");
});
it("should throw error when kdf type is Argon2Id and parallelism not provided", () => {
expect(() => {
new KdfConfigResponse({
KdfType: KdfType.Argon2id,
Iterations: 3,
Memory: 64,
Parallelism: undefined,
});
}).toThrow("KDF config response does not contain a valid parallelism for Argon2id");
});
it("should create response when kdf type is PBKDF2", () => {
const response = new KdfConfigResponse({
KdfType: KdfType.PBKDF2_SHA256,
Iterations: 600_000,
});
expect(response.kdfType).toBe(KdfType.PBKDF2_SHA256);
expect(response.iterations).toBe(600_000);
expect(response.memory).toBeUndefined();
expect(response.parallelism).toBeUndefined();
});
it("should create response when kdf type is Argon2Id", () => {
const response = new KdfConfigResponse({
KdfType: KdfType.Argon2id,
Iterations: 3,
Memory: 64,
Parallelism: 4,
});
expect(response.kdfType).toBe(KdfType.Argon2id);
expect(response.iterations).toBe(3);
expect(response.memory).toBe(64);
expect(response.parallelism).toBe(4);
});
describe("toKdfConfig", () => {
it("should convert to PBKDF2KdfConfig", () => {
const response = new KdfConfigResponse({
KdfType: KdfType.PBKDF2_SHA256,
Iterations: 600_000,
});
const kdfConfig = response.toKdfConfig();
expect(kdfConfig).toBeInstanceOf(PBKDF2KdfConfig);
const pbkdf2Config = kdfConfig as PBKDF2KdfConfig;
expect(pbkdf2Config.iterations).toBe(600_000);
});
it("should convert to Argon2KdfConfig", () => {
const response = new KdfConfigResponse({
KdfType: KdfType.Argon2id,
Iterations: 3,
Memory: 64,
Parallelism: 4,
});
const kdfConfig = response.toKdfConfig();
expect(kdfConfig).toBeInstanceOf(Argon2KdfConfig);
const argon2Config = kdfConfig as Argon2KdfConfig;
expect(argon2Config.iterations).toBe(3);
expect(argon2Config.memory).toBe(64);
expect(argon2Config.parallelism).toBe(4);
});
});
});

View File

@@ -0,0 +1,49 @@
// eslint-disable-next-line no-restricted-imports
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { BaseResponse } from "../../../models/response/base.response";
export class KdfConfigResponse extends BaseResponse {
kdfType: KdfType;
iterations: number;
memory?: number;
parallelism?: number;
constructor(response: unknown) {
super(response);
const kdfType = this.getResponseProperty("KdfType");
if (kdfType == null || typeof kdfType !== "number") {
throw new Error("KDF config response does not contain a valid KDF type");
}
this.kdfType = kdfType as KdfType;
const iterations = this.getResponseProperty("Iterations");
if (iterations == null || typeof iterations !== "number") {
throw new Error("KDF config response does not contain a valid number of iterations");
}
this.iterations = iterations;
if (this.kdfType === KdfType.Argon2id) {
const memory = this.getResponseProperty("Memory");
if (memory == null || typeof memory !== "number") {
throw new Error("KDF config response does not contain a valid memory size for Argon2id");
}
const parallelism = this.getResponseProperty("Parallelism");
if (parallelism == null || typeof parallelism !== "number") {
throw new Error("KDF config response does not contain a valid parallelism for Argon2id");
}
this.memory = memory;
this.parallelism = parallelism;
}
}
toKdfConfig(): KdfConfig {
switch (this.kdfType) {
case KdfType.Argon2id:
return new Argon2KdfConfig(this.iterations, this.memory!, this.parallelism!);
case KdfType.PBKDF2_SHA256:
return new PBKDF2KdfConfig(this.iterations);
}
}
}

View File

@@ -0,0 +1,43 @@
// eslint-disable-next-line no-restricted-imports
import { KdfType } from "@bitwarden/key-management";
import { makeEncString } from "../../../../spec";
import { UserDecryptionResponse } from "./user-decryption.response";
describe("UserDecryptionResponse", () => {
it("should create response when masterPasswordUnlock provided", () => {
const salt = "test@example.com";
const encryptedUserKey = makeEncString("testUserKey");
const kdfIterations = 600_000;
const response = {
MasterPasswordUnlock: {
Salt: salt,
Kdf: {
KdfType: KdfType.PBKDF2_SHA256 as number,
Iterations: kdfIterations,
},
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
},
};
const userDecryptionResponse = new UserDecryptionResponse(response);
expect(userDecryptionResponse.masterPasswordUnlock).toBeDefined();
expect(userDecryptionResponse.masterPasswordUnlock!.salt).toEqual(salt);
expect(userDecryptionResponse.masterPasswordUnlock!.kdf).toBeDefined();
expect(userDecryptionResponse.masterPasswordUnlock!.kdf!.kdfType).toEqual(
KdfType.PBKDF2_SHA256,
);
expect(userDecryptionResponse.masterPasswordUnlock!.kdf!.iterations).toEqual(kdfIterations);
expect(userDecryptionResponse.masterPasswordUnlock!.masterKeyWrappedUserKey).toEqual(
encryptedUserKey,
);
});
it("should create response when masterPasswordUnlock is not provided", () => {
const userDecryptionResponse = new UserDecryptionResponse({});
expect(userDecryptionResponse.masterPasswordUnlock).toBeUndefined();
});
});

View File

@@ -0,0 +1,15 @@
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordUnlockResponse } from "../../master-password/models/response/master-password-unlock.response";
export class UserDecryptionResponse extends BaseResponse {
masterPasswordUnlock?: MasterPasswordUnlockResponse;
constructor(response: unknown) {
super(response);
const masterPasswordUnlock = this.getResponseProperty("MasterPasswordUnlock");
if (masterPasswordUnlock != null || typeof masterPasswordUnlock === "object") {
this.masterPasswordUnlock = new MasterPasswordUnlockResponse(masterPasswordUnlock);
}
}
}