mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +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:
@@ -154,4 +154,16 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
reason: ForceSetPasswordReason,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the master password unlock data for the user.
|
||||
* This data is used to unlock the user key with the master password.
|
||||
* @param masterPasswordUnlockData The master password unlock data containing the KDF settings, salt, and encrypted user key.
|
||||
* @param userId The user ID.
|
||||
* @throws Error If the user ID or master password unlock data is missing.
|
||||
*/
|
||||
abstract setMasterPasswordUnlockData(
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
userId: UserId,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 testKdfResponse = { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000 };
|
||||
|
||||
it("should throw error when salt is not provided", () => {
|
||||
expect(() => {
|
||||
new MasterPasswordUnlockResponse({
|
||||
Salt: undefined,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
});
|
||||
}).toThrow("MasterPasswordUnlockResponse does not contain a valid salt");
|
||||
});
|
||||
|
||||
it("should throw error when master key encrypted user key is not provided", () => {
|
||||
expect(() => {
|
||||
new MasterPasswordUnlockResponse({
|
||||
Salt: salt,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: undefined,
|
||||
});
|
||||
}).toThrow(
|
||||
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
|
||||
);
|
||||
});
|
||||
|
||||
it("should create response", () => {
|
||||
const response = new MasterPasswordUnlockResponse({
|
||||
Salt: salt,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
});
|
||||
|
||||
expect(response.salt).toBe(salt);
|
||||
expect(response.kdf).toBeDefined();
|
||||
expect(response.kdf.toKdfConfig()).toEqual(new PBKDF2KdfConfig(600_000));
|
||||
expect(response.masterKeyWrappedUserKey).toEqual(encryptedUserKey);
|
||||
});
|
||||
|
||||
describe("toMasterPasswordUnlockData", () => {
|
||||
it("should return MasterPasswordUnlockData", () => {
|
||||
const response = new MasterPasswordUnlockResponse({
|
||||
Salt: salt,
|
||||
Kdf: testKdfResponse,
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
});
|
||||
|
||||
const unlockData = response.toMasterPasswordUnlockData();
|
||||
expect(unlockData).toBeDefined();
|
||||
expect(unlockData.salt).toBe(salt);
|
||||
expect(unlockData.kdf).toEqual(new PBKDF2KdfConfig(600_000));
|
||||
expect(unlockData.masterKeyWrappedUserKey).toEqual(encryptedUserKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
import { EncString } from "../../../crypto/models/enc-string";
|
||||
import { KdfConfigResponse } from "../../../models/response/kdf-config.response";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../../types/master-password.types";
|
||||
|
||||
export class MasterPasswordUnlockResponse extends BaseResponse {
|
||||
salt: MasterPasswordSalt;
|
||||
kdf: KdfConfigResponse;
|
||||
masterKeyWrappedUserKey: MasterKeyWrappedUserKey;
|
||||
|
||||
constructor(response: unknown) {
|
||||
super(response);
|
||||
|
||||
const salt = this.getResponseProperty("Salt");
|
||||
if (salt == null || typeof salt !== "string") {
|
||||
throw new Error("MasterPasswordUnlockResponse does not contain a valid salt");
|
||||
}
|
||||
this.salt = salt as MasterPasswordSalt;
|
||||
|
||||
this.kdf = new KdfConfigResponse(this.getResponseProperty("Kdf"));
|
||||
|
||||
const masterKeyEncryptedUserKey = this.getResponseProperty("MasterKeyEncryptedUserKey");
|
||||
if (masterKeyEncryptedUserKey == null || typeof masterKeyEncryptedUserKey !== "string") {
|
||||
throw new Error(
|
||||
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
|
||||
);
|
||||
}
|
||||
this.masterKeyWrappedUserKey = new EncString(
|
||||
masterKeyEncryptedUserKey,
|
||||
) as MasterKeyWrappedUserKey;
|
||||
}
|
||||
|
||||
toMasterPasswordUnlockData() {
|
||||
return new MasterPasswordUnlockData(
|
||||
this.salt,
|
||||
this.kdf.toKdfConfig(),
|
||||
this.masterKeyWrappedUserKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -108,4 +108,11 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
): Promise<UserKey> {
|
||||
return this.mock.unwrapUserKeyFromMasterPasswordUnlockData(password, masterPasswordUnlockData);
|
||||
}
|
||||
|
||||
setMasterPasswordUnlockData(
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
return this.mock.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import * as rxjs from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
makeEncString,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../spec";
|
||||
@@ -23,9 +25,13 @@ 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 { MasterPasswordSalt } from "../types/master-password.types";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
import { MasterPasswordService } from "./master-password.service";
|
||||
import { MASTER_PASSWORD_UNLOCK_KEY, MasterPasswordService } from "./master-password.service";
|
||||
|
||||
describe("MasterPasswordService", () => {
|
||||
let sut: MasterPasswordService;
|
||||
@@ -182,7 +188,7 @@ describe("MasterPasswordService", () => {
|
||||
expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(testMasterKey);
|
||||
});
|
||||
it("returns null if failed to decrypt", async () => {
|
||||
encryptService.unwrapSymmetricKey.mockResolvedValue(null);
|
||||
encryptService.unwrapSymmetricKey.mockRejectedValue(new Error("Decryption failed"));
|
||||
const result = await sut.decryptUserKeyWithMasterKey(
|
||||
testMasterKey,
|
||||
userId,
|
||||
@@ -321,4 +327,106 @@ describe("MasterPasswordService", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setMasterPasswordUnlockData", () => {
|
||||
const kdfPBKDF2: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3);
|
||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||
const userKey = makeSymmetricCryptoKey(64, 2) as UserKey;
|
||||
|
||||
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,
|
||||
salt,
|
||||
userKey,
|
||||
);
|
||||
|
||||
await sut.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);
|
||||
|
||||
expect(stateProvider.getUser).toHaveBeenCalledWith(userId, MASTER_PASSWORD_UNLOCK_KEY);
|
||||
expect(mockUserState.update).toHaveBeenCalled();
|
||||
|
||||
const updateFn = mockUserState.update.mock.calls[0][0];
|
||||
expect(updateFn(null)).toEqual(masterPasswordUnlockData.toJSON());
|
||||
},
|
||||
);
|
||||
|
||||
it("throws if masterPasswordUnlockData is null", async () => {
|
||||
await expect(
|
||||
sut.setMasterPasswordUnlockData(null as unknown as MasterPasswordUnlockData, userId),
|
||||
).rejects.toThrow("masterPasswordUnlockData is null or undefined.");
|
||||
});
|
||||
|
||||
it("throws if userId is null", async () => {
|
||||
const masterPasswordUnlockData = await sut.makeMasterPasswordUnlockData(
|
||||
"test-password",
|
||||
kdfPBKDF2,
|
||||
salt,
|
||||
userKey,
|
||||
);
|
||||
|
||||
await expect(
|
||||
sut.setMasterPasswordUnlockData(masterPasswordUnlockData, null as unknown as UserId),
|
||||
).rejects.toThrow("userId is null or undefined.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MASTER_PASSWORD_UNLOCK_KEY", () => {
|
||||
it("has the correct configuration", () => {
|
||||
expect(MASTER_PASSWORD_UNLOCK_KEY.stateDefinition).toBeDefined();
|
||||
expect(MASTER_PASSWORD_UNLOCK_KEY.key).toBe("masterPasswordUnlockKey");
|
||||
expect(MASTER_PASSWORD_UNLOCK_KEY.clearOn).toEqual(["logout"]);
|
||||
});
|
||||
|
||||
describe("deserializer", () => {
|
||||
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;
|
||||
|
||||
it("returns null when value is null", () => {
|
||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(
|
||||
null as unknown as Jsonify<MasterPasswordUnlockData>,
|
||||
);
|
||||
expect(deserialized).toBeNull();
|
||||
});
|
||||
|
||||
it("returns master password unlock data when value is present and kdf type is pbkdf2", () => {
|
||||
const data: Jsonify<MasterPasswordUnlockData> = {
|
||||
salt: salt,
|
||||
kdf: {
|
||||
kdfType: KdfType.PBKDF2_SHA256,
|
||||
iterations: kdfPBKDF2.iterations,
|
||||
},
|
||||
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
|
||||
};
|
||||
|
||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
||||
expect(deserialized).toEqual(
|
||||
new MasterPasswordUnlockData(salt, kdfPBKDF2, encryptedUserKey),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns master password unlock data when value is present and kdf type is argon2", () => {
|
||||
const data: Jsonify<MasterPasswordUnlockData> = {
|
||||
salt: salt,
|
||||
kdf: {
|
||||
kdfType: KdfType.Argon2id,
|
||||
iterations: kdfArgon2.iterations,
|
||||
memory: kdfArgon2.memory,
|
||||
parallelism: kdfArgon2.parallelism,
|
||||
},
|
||||
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
|
||||
};
|
||||
|
||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
||||
expect(deserialized).toEqual(
|
||||
new MasterPasswordUnlockData(salt, kdfArgon2, encryptedUserKey),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
||||
import {
|
||||
MASTER_PASSWORD_DISK,
|
||||
MASTER_PASSWORD_MEMORY,
|
||||
MASTER_PASSWORD_UNLOCK_DISK,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../../platform/state";
|
||||
@@ -68,6 +69,16 @@ const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>(
|
||||
},
|
||||
);
|
||||
|
||||
/** Disk to persist through lock */
|
||||
export const MASTER_PASSWORD_UNLOCK_KEY = new UserKeyDefinition<MasterPasswordUnlockData>(
|
||||
MASTER_PASSWORD_UNLOCK_DISK,
|
||||
"masterPasswordUnlockKey",
|
||||
{
|
||||
deserializer: (obj) => MasterPasswordUnlockData.fromJSON(obj),
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
@@ -296,11 +307,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
kdf.toSdkConfig(),
|
||||
),
|
||||
) as MasterKeyWrappedUserKey;
|
||||
return {
|
||||
salt,
|
||||
kdf,
|
||||
masterKeyWrappedUserKey,
|
||||
};
|
||||
return new MasterPasswordUnlockData(salt, kdf, masterKeyWrappedUserKey);
|
||||
}
|
||||
|
||||
async unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
@@ -321,4 +328,16 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
);
|
||||
return userKey as UserKey;
|
||||
}
|
||||
|
||||
async setMasterPasswordUnlockData(
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData");
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
await this.stateProvider
|
||||
.getUser(userId, MASTER_PASSWORD_UNLOCK_KEY)
|
||||
.update(() => masterPasswordUnlockData.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Opaque } from "type-fest";
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
|
||||
@@ -18,11 +18,35 @@ export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterPasswordSalt">;
|
||||
/**
|
||||
* The data required to unlock with the master password.
|
||||
*/
|
||||
export type MasterPasswordUnlockData = {
|
||||
salt: MasterPasswordSalt;
|
||||
kdf: KdfConfig;
|
||||
masterKeyWrappedUserKey: MasterKeyWrappedUserKey;
|
||||
};
|
||||
export class MasterPasswordUnlockData {
|
||||
constructor(
|
||||
readonly salt: MasterPasswordSalt,
|
||||
readonly kdf: KdfConfig,
|
||||
readonly masterKeyWrappedUserKey: MasterKeyWrappedUserKey,
|
||||
) {}
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
salt: this.salt,
|
||||
kdf: this.kdf,
|
||||
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey.toJSON(),
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<MasterPasswordUnlockData>): MasterPasswordUnlockData | null {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MasterPasswordUnlockData(
|
||||
obj.salt,
|
||||
obj.kdf.kdfType === KdfType.PBKDF2_SHA256
|
||||
? PBKDF2KdfConfig.fromJSON(obj.kdf)
|
||||
: Argon2KdfConfig.fromJSON(obj.kdf),
|
||||
EncString.fromJSON(obj.masterKeyWrappedUserKey) as MasterKeyWrappedUserKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The data required to authenticate with the master password.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user