1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +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

@@ -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();
});
});
});
});

View File

@@ -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,
);
}
}
}

View 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();
});
});

View File

@@ -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);
}
}
}