mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +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:
@@ -20,6 +20,11 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
@@ -33,7 +38,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import {
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PasswordStrengthService,
|
||||
@@ -41,7 +46,7 @@ import {
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
@@ -56,7 +61,7 @@ const masterPassword = "password";
|
||||
const deviceId = Utils.newGuid();
|
||||
const accessToken = "ACCESS_TOKEN";
|
||||
const refreshToken = "REFRESH_TOKEN";
|
||||
const userKey = "USER_KEY";
|
||||
const encryptedUserKey = makeEncString("USER_KEY");
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const kdf = 0;
|
||||
const kdfIterations = 10000;
|
||||
@@ -65,6 +70,14 @@ const masterPasswordHash = "MASTER_PASSWORD_HASH";
|
||||
const name = "NAME";
|
||||
const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: true,
|
||||
MasterPasswordUnlock: {
|
||||
Salt: email,
|
||||
Kdf: {
|
||||
KdfType: kdf,
|
||||
Iterations: kdfIterations,
|
||||
},
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
},
|
||||
};
|
||||
|
||||
const decodedToken = {
|
||||
@@ -86,7 +99,7 @@ export function identityTokenResponseFactory(
|
||||
ForcePasswordReset: false,
|
||||
Kdf: kdf,
|
||||
KdfIterations: kdfIterations,
|
||||
Key: userKey,
|
||||
Key: encryptedUserKey.encryptedString,
|
||||
PrivateKey: privateKey,
|
||||
ResetMasterPassword: false,
|
||||
access_token: accessToken,
|
||||
@@ -247,6 +260,14 @@ describe("LoginStrategy", () => {
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
UserDecryptionOptions.fromResponse(idTokenResponse),
|
||||
);
|
||||
expect(masterPasswordService.mock.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
new MasterPasswordUnlockData(
|
||||
email as MasterPasswordSalt,
|
||||
new PBKDF2KdfConfig(kdfIterations),
|
||||
encryptedUserKey as MasterKeyWrappedUserKey,
|
||||
),
|
||||
userId,
|
||||
);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||
});
|
||||
|
||||
|
||||
@@ -199,6 +199,15 @@ export abstract class LoginStrategy {
|
||||
UserDecryptionOptions.fromResponse(tokenResponse),
|
||||
);
|
||||
|
||||
if (tokenResponse.userDecryptionOptions?.masterPasswordUnlock != null) {
|
||||
const masterPasswordUnlockData =
|
||||
tokenResponse.userDecryptionOptions.masterPasswordUnlock.toMasterPasswordUnlockData();
|
||||
await this.masterPasswordService.setMasterPasswordUnlockData(
|
||||
masterPasswordUnlockData,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
const vaultTimeoutAction = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
|
||||
);
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response";
|
||||
import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response";
|
||||
|
||||
/**
|
||||
* Key Connector decryption options. Intended to be sent to the client for use after authentication.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { makeEncString } from "../../../../spec";
|
||||
|
||||
import { IdentityTokenResponse } from "./identity-token.response";
|
||||
|
||||
describe("IdentityTokenResponse", () => {
|
||||
const accessToken = "testAccessToken";
|
||||
const tokenType = "Bearer";
|
||||
const expiresIn = 3600;
|
||||
const refreshToken = "testRefreshToken";
|
||||
const encryptedUserKey = makeEncString("testUserKey");
|
||||
|
||||
it("should throw an error when access token is missing", () => {
|
||||
const response = {
|
||||
access_token: undefined as unknown,
|
||||
token_type: tokenType,
|
||||
};
|
||||
|
||||
expect(() => new IdentityTokenResponse(response)).toThrow(
|
||||
"Identity response does not contain a valid access token",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error when token type is missing", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: undefined as unknown,
|
||||
};
|
||||
|
||||
expect(() => new IdentityTokenResponse(response)).toThrow(
|
||||
"Identity response does not contain a valid token type",
|
||||
);
|
||||
});
|
||||
|
||||
it("should create response without optional fields", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.accessToken).toEqual(accessToken);
|
||||
expect(identityTokenResponse.tokenType).toEqual(tokenType);
|
||||
expect(identityTokenResponse.expiresIn).toBeUndefined();
|
||||
expect(identityTokenResponse.refreshToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create response with expires_in present", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
expires_in: expiresIn,
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.accessToken).toEqual(accessToken);
|
||||
expect(identityTokenResponse.tokenType).toEqual(tokenType);
|
||||
expect(identityTokenResponse.expiresIn).toEqual(expiresIn);
|
||||
expect(identityTokenResponse.refreshToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create response with refresh_token present", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
expires_in: expiresIn,
|
||||
refresh_token: refreshToken,
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.accessToken).toEqual(accessToken);
|
||||
expect(identityTokenResponse.tokenType).toEqual(tokenType);
|
||||
expect(identityTokenResponse.expiresIn).toEqual(expiresIn);
|
||||
expect(identityTokenResponse.refreshToken).toEqual(refreshToken);
|
||||
});
|
||||
|
||||
it("should create response with key is not present", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
Key: undefined as unknown,
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.key).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create response with key present", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
Key: encryptedUserKey.encryptedString,
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.key).toEqual(encryptedUserKey);
|
||||
});
|
||||
|
||||
it("should create response with user decryption options is not present", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
UserDecryptionOptions: undefined as unknown,
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.userDecryptionOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create response with user decryption options present", () => {
|
||||
const response = {
|
||||
access_token: accessToken,
|
||||
token_type: tokenType,
|
||||
UserDecryptionOptions: {},
|
||||
};
|
||||
|
||||
const identityTokenResponse = new IdentityTokenResponse(response);
|
||||
expect(identityTokenResponse.userDecryptionOptions).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -12,8 +12,8 @@ import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-de
|
||||
|
||||
export class IdentityTokenResponse extends BaseResponse {
|
||||
accessToken: string;
|
||||
expiresIn: number;
|
||||
refreshToken: string;
|
||||
expiresIn?: number;
|
||||
refreshToken?: string;
|
||||
tokenType: string;
|
||||
|
||||
resetMasterPassword: boolean;
|
||||
@@ -26,14 +26,30 @@ export class IdentityTokenResponse extends BaseResponse {
|
||||
apiUseKeyConnector: boolean;
|
||||
keyConnectorUrl: string;
|
||||
|
||||
userDecryptionOptions: UserDecryptionOptionsResponse;
|
||||
userDecryptionOptions?: UserDecryptionOptionsResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
constructor(response: unknown) {
|
||||
super(response);
|
||||
this.accessToken = response.access_token;
|
||||
this.expiresIn = response.expires_in;
|
||||
this.refreshToken = response.refresh_token;
|
||||
this.tokenType = response.token_type;
|
||||
|
||||
const accessToken = this.getResponseProperty("access_token");
|
||||
if (accessToken == null || typeof accessToken !== "string") {
|
||||
throw new Error("Identity response does not contain a valid access token");
|
||||
}
|
||||
const tokenType = this.getResponseProperty("token_type");
|
||||
if (tokenType == null || typeof tokenType !== "string") {
|
||||
throw new Error("Identity response does not contain a valid token type");
|
||||
}
|
||||
this.accessToken = accessToken;
|
||||
this.tokenType = tokenType;
|
||||
|
||||
const expiresIn = this.getResponseProperty("expires_in");
|
||||
if (expiresIn != null && typeof expiresIn === "number") {
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
const refreshToken = this.getResponseProperty("refresh_token");
|
||||
if (refreshToken != null && typeof refreshToken === "string") {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
this.resetMasterPassword = this.getResponseProperty("ResetMasterPassword");
|
||||
this.privateKey = this.getResponseProperty("PrivateKey");
|
||||
@@ -57,10 +73,9 @@ export class IdentityTokenResponse extends BaseResponse {
|
||||
this.getResponseProperty("MasterPasswordPolicy"),
|
||||
);
|
||||
|
||||
if (response.UserDecryptionOptions) {
|
||||
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
|
||||
this.getResponseProperty("UserDecryptionOptions"),
|
||||
);
|
||||
const userDecryptionOptions = this.getResponseProperty("UserDecryptionOptions");
|
||||
if (userDecryptionOptions != null && typeof userDecryptionOptions === "object") {
|
||||
this.userDecryptionOptions = new UserDecryptionOptionsResponse(userDecryptionOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import { makeEncString } from "../../../../../spec";
|
||||
|
||||
import { UserDecryptionOptionsResponse } from "./user-decryption-options.response";
|
||||
|
||||
describe("UserDecryptionOptionsResponse", () => {
|
||||
it("should create response when master password unlock is present", () => {
|
||||
const salt = "test@example.com";
|
||||
const encryptedUserKey = makeEncString("testUserKey");
|
||||
|
||||
const response = new UserDecryptionOptionsResponse({
|
||||
HasMasterPassword: true,
|
||||
MasterPasswordUnlock: {
|
||||
Salt: salt,
|
||||
Kdf: {
|
||||
KdfType: KdfType.PBKDF2_SHA256,
|
||||
Iterations: 600_000,
|
||||
},
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.hasMasterPassword).toBe(true);
|
||||
expect(response.masterPasswordUnlock).toBeDefined();
|
||||
expect(response.masterPasswordUnlock!.salt).toEqual(salt);
|
||||
expect(response.masterPasswordUnlock!.kdf.kdfType).toEqual(KdfType.PBKDF2_SHA256);
|
||||
expect(response.masterPasswordUnlock!.kdf.iterations).toEqual(600_000);
|
||||
expect(response.masterPasswordUnlock!.masterKeyWrappedUserKey).toEqual(encryptedUserKey);
|
||||
expect(response.trustedDeviceOption).toBeUndefined();
|
||||
expect(response.keyConnectorOption).toBeUndefined();
|
||||
expect(response.webAuthnPrfOption).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create response when master password unlock is not present", () => {
|
||||
const response = new UserDecryptionOptionsResponse({
|
||||
HasMasterPassword: false,
|
||||
});
|
||||
|
||||
expect(response.hasMasterPassword).toBe(false);
|
||||
expect(response.masterPasswordUnlock).toBeUndefined();
|
||||
expect(response.trustedDeviceOption).toBeUndefined();
|
||||
expect(response.keyConnectorOption).toBeUndefined();
|
||||
expect(response.webAuthnPrfOption).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MasterPasswordUnlockResponse } from "../../../../key-management/master-password/models/response/master-password-unlock.response";
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
|
||||
export interface IUserDecryptionOptionsServerResponse {
|
||||
HasMasterPassword: boolean;
|
||||
MasterPasswordUnlock?: unknown;
|
||||
TrustedDeviceOption?: ITrustedDeviceUserDecryptionOptionServerResponse;
|
||||
KeyConnectorOption?: IKeyConnectorUserDecryptionOptionServerResponse;
|
||||
WebAuthnPrfOption?: IWebAuthnPrfDecryptionOptionServerResponse;
|
||||
@@ -22,6 +24,7 @@ export interface IUserDecryptionOptionsServerResponse {
|
||||
|
||||
export class UserDecryptionOptionsResponse extends BaseResponse {
|
||||
hasMasterPassword: boolean;
|
||||
masterPasswordUnlock?: MasterPasswordUnlockResponse;
|
||||
trustedDeviceOption?: TrustedDeviceUserDecryptionOptionResponse;
|
||||
keyConnectorOption?: KeyConnectorUserDecryptionOptionResponse;
|
||||
webAuthnPrfOption?: WebAuthnPrfDecryptionOptionResponse;
|
||||
@@ -31,6 +34,11 @@ export class UserDecryptionOptionsResponse extends BaseResponse {
|
||||
|
||||
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
|
||||
|
||||
const masterPasswordUnlock = this.getResponseProperty("MasterPasswordUnlock");
|
||||
if (masterPasswordUnlock != null && typeof masterPasswordUnlock === "object") {
|
||||
this.masterPasswordUnlock = new MasterPasswordUnlockResponse(masterPasswordUnlock);
|
||||
}
|
||||
|
||||
if (response.TrustedDeviceOption) {
|
||||
this.trustedDeviceOption = new TrustedDeviceUserDecryptionOptionResponse(
|
||||
this.getResponseProperty("TrustedDeviceOption"),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { makeEncString } from "../../../spec";
|
||||
import { Matrix } from "../../../spec/matrix";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -29,6 +30,11 @@ import { DomainSettingsService } from "../../autofill/services/domain-settings.s
|
||||
import { BillingAccountProfileStateService } from "../../billing/abstractions";
|
||||
import { KeyConnectorService } from "../../key-management/key-connector/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../../key-management/master-password/types/master-password.types";
|
||||
import { SendApiService } from "../../tools/send/services/send-api.service.abstraction";
|
||||
import { InternalSendService } from "../../tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "../../types/guid";
|
||||
@@ -84,6 +90,7 @@ describe("DefaultSyncService", () => {
|
||||
sendService = mock();
|
||||
logService = mock();
|
||||
keyConnectorService = mock();
|
||||
keyConnectorService.convertAccountRequired$ = of(false);
|
||||
providerService = mock();
|
||||
folderApiService = mock();
|
||||
organizationService = mock();
|
||||
@@ -236,5 +243,55 @@ describe("DefaultSyncService", () => {
|
||||
expect(sut["inFlightApiCalls"].sync).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncUserDecryption", () => {
|
||||
const salt = "test@example.com";
|
||||
const kdf = new PBKDF2KdfConfig(600_000);
|
||||
const encryptedUserKey = makeEncString("testUserKey");
|
||||
|
||||
it("should set master password unlock when present in user decryption", async () => {
|
||||
const syncResponse = new SyncResponse({
|
||||
Profile: {
|
||||
Id: user1,
|
||||
},
|
||||
UserDecryption: {
|
||||
MasterPasswordUnlock: {
|
||||
Salt: salt,
|
||||
Kdf: {
|
||||
KdfType: kdf.kdfType,
|
||||
Iterations: kdf.iterations,
|
||||
},
|
||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
||||
},
|
||||
},
|
||||
});
|
||||
apiService.getSync.mockResolvedValue(syncResponse);
|
||||
|
||||
await sut.fullSync(true, true);
|
||||
|
||||
expect(masterPasswordAbstraction.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
new MasterPasswordUnlockData(
|
||||
salt as MasterPasswordSalt,
|
||||
kdf,
|
||||
encryptedUserKey as MasterKeyWrappedUserKey,
|
||||
),
|
||||
user1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not set master password unlock when not present in user decryption", async () => {
|
||||
const syncResponse = new SyncResponse({
|
||||
Profile: {
|
||||
Id: user1,
|
||||
},
|
||||
UserDecryption: {},
|
||||
});
|
||||
apiService.getSync.mockResolvedValue(syncResponse);
|
||||
|
||||
await sut.fullSync(true, true);
|
||||
|
||||
expect(masterPasswordAbstraction.setMasterPasswordUnlockData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DomainSettingsService } from "../../autofill/services/domain-settings.s
|
||||
import { BillingAccountProfileStateService } from "../../billing/abstractions";
|
||||
import { KeyConnectorService } from "../../key-management/key-connector/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { UserDecryptionResponse } from "../../key-management/models/response/user-decryption.response";
|
||||
import { DomainsResponse } from "../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
@@ -168,6 +169,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
|
||||
const response = await this.inFlightApiCalls.sync;
|
||||
|
||||
await this.syncUserDecryption(response.profile.id, response.userDecryption);
|
||||
await this.syncProfile(response.profile);
|
||||
await this.syncFolders(response.folders, response.profile.id);
|
||||
await this.syncCollections(response.collections, response.profile.id);
|
||||
@@ -390,4 +392,21 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
}
|
||||
return await this.policyService.replace(policies, userId);
|
||||
}
|
||||
|
||||
private async syncUserDecryption(
|
||||
userId: UserId,
|
||||
userDecryption: UserDecryptionResponse | undefined,
|
||||
) {
|
||||
if (userDecryption == null) {
|
||||
return;
|
||||
}
|
||||
if (userDecryption.masterPasswordUnlock != null) {
|
||||
const masterPasswordUnlockData =
|
||||
userDecryption.masterPasswordUnlock.toMasterPasswordUnlockData();
|
||||
await this.masterPasswordService.setMasterPasswordUnlockData(
|
||||
masterPasswordUnlockData,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
libs/common/src/platform/sync/sync.response.spec.ts
Normal file
18
libs/common/src/platform/sync/sync.response.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { SyncResponse } from "./sync.response";
|
||||
|
||||
describe("SyncResponse", () => {
|
||||
it("should create response when user decryption is not provided", () => {
|
||||
const response = new SyncResponse({
|
||||
UserDecryption: undefined,
|
||||
});
|
||||
expect(response.userDecryption).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create response when user decryption is provided", () => {
|
||||
const response = new SyncResponse({
|
||||
UserDecryption: {},
|
||||
});
|
||||
expect(response.userDecryption).toBeDefined();
|
||||
expect(response.userDecryption!.masterPasswordUnlock).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
import { CollectionDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
|
||||
import { PolicyResponse } from "../../admin-console/models/response/policy.response";
|
||||
import { UserDecryptionResponse } from "../../key-management/models/response/user-decryption.response";
|
||||
import { BaseResponse } from "../../models/response/base.response";
|
||||
import { DomainsResponse } from "../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
@@ -18,6 +19,7 @@ export class SyncResponse extends BaseResponse {
|
||||
domains?: DomainsResponse;
|
||||
policies?: PolicyResponse[] = [];
|
||||
sends: SendResponse[] = [];
|
||||
userDecryption?: UserDecryptionResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -56,5 +58,10 @@ export class SyncResponse extends BaseResponse {
|
||||
if (sends != null) {
|
||||
this.sends = sends.map((s: any) => new SendResponse(s));
|
||||
}
|
||||
|
||||
const userDecryption = this.getResponseProperty("UserDecryption");
|
||||
if (userDecryption != null && typeof userDecryption === "object") {
|
||||
this.userDecryption = new UserDecryptionResponse(userDecryption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +295,7 @@ describe("ApiService", () => {
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: `${expectedEffectiveUser}_new_access_token`,
|
||||
token_type: "Bearer",
|
||||
refresh_token: `${expectedEffectiveUser}_new_refresh_token`,
|
||||
}),
|
||||
} satisfies Partial<Response> as Response);
|
||||
|
||||
@@ -61,6 +61,7 @@ export const LOGIN_EMAIL_MEMORY = new StateDefinition("loginEmail", "memory");
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
||||
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
|
||||
export const MASTER_PASSWORD_UNLOCK_DISK = new StateDefinition("masterPasswordUnlock", "disk");
|
||||
export const PIN_DISK = new StateDefinition("pinUnlock", "disk");
|
||||
export const PIN_MEMORY = new StateDefinition("pinUnlock", "memory");
|
||||
export const ROUTER_DISK = new StateDefinition("router", "disk");
|
||||
|
||||
Reference in New Issue
Block a user