mirror of
https://github.com/bitwarden/clients.git
synced 2026-01-24 11:03:23 +08:00
[PM-27233] Support v2 encryption for JIT Password signups (#18222)
* Support v2 encryption for JIT Password signups * TDE set master password split * update sdk-internal dependency * moved encryption v2 to InitializeJitPasswordUserService * remove account cryptographic state legacy states from #18164 * legacy state comments * sdk update * unit test coverage * consolidate do SetInitialPasswordService * replace legacy master key with setLegacyMasterKeyFromUnlockData * typo * web and desktop overrides with unit tests * early return * compact validation * simplify super prototype
This commit is contained in:
@@ -89,6 +89,7 @@ import {
|
||||
PlatformUtilsService,
|
||||
PlatformUtilsService as PlatformUtilsServiceAbstraction,
|
||||
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -432,6 +433,7 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
AccountCryptographicStateService,
|
||||
RegisterSdkService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -19,12 +21,14 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -45,6 +49,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
let registerSdkService: MockProxy<RegisterSdkService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
@@ -59,6 +64,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
messagingService = mock<MessagingService>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
registerSdkService = mock<RegisterSdkService>();
|
||||
|
||||
sut = new DesktopSetInitialPasswordService(
|
||||
apiService,
|
||||
@@ -73,6 +79,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
userDecryptionOptionsService,
|
||||
messagingService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -179,4 +186,36 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initializePasswordJitPasswordUserV2Encryption(...)", () => {
|
||||
it("should send a 'redrawMenu' message", async () => {
|
||||
// Arrange
|
||||
const credentials: InitializeJitPasswordCredentials = {
|
||||
newPasswordHint: "newPasswordHint",
|
||||
orgSsoIdentifier: "orgSsoIdentifier",
|
||||
orgId: "orgId" as OrganizationId,
|
||||
resetPasswordAutoEnroll: false,
|
||||
newPassword: "newPassword123!",
|
||||
salt: "user@example.com" as MasterPasswordSalt,
|
||||
};
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const superSpy = jest
|
||||
.spyOn(
|
||||
DefaultSetInitialPasswordService.prototype,
|
||||
"initializePasswordJitPasswordUserV2Encryption",
|
||||
)
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
// Assert
|
||||
expect(superSpy).toHaveBeenCalledWith(credentials, userId);
|
||||
expect(messagingService.send).toHaveBeenCalledTimes(1);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
|
||||
superSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -14,6 +15,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -34,6 +36,7 @@ export class DesktopSetInitialPasswordService
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private messagingService: MessagingService,
|
||||
protected accountCryptographicStateService: AccountCryptographicStateService,
|
||||
protected registerSdkService: RegisterSdkService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -47,6 +50,7 @@ export class DesktopSetInitialPasswordService
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,4 +63,13 @@ export class DesktopSetInitialPasswordService
|
||||
|
||||
this.messagingService.send("redrawMenu");
|
||||
}
|
||||
|
||||
override async initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials: InitializeJitPasswordCredentials,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await super.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
this.messagingService.send("redrawMenu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -20,11 +21,13 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
@@ -47,6 +50,7 @@ describe("WebSetInitialPasswordService", () => {
|
||||
let organizationInviteService: MockProxy<OrganizationInviteService>;
|
||||
let routerService: MockProxy<RouterService>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
let registerSdkService: MockProxy<RegisterSdkService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
@@ -62,6 +66,7 @@ describe("WebSetInitialPasswordService", () => {
|
||||
organizationInviteService = mock<OrganizationInviteService>();
|
||||
routerService = mock<RouterService>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
registerSdkService = mock<RegisterSdkService>();
|
||||
|
||||
sut = new WebSetInitialPasswordService(
|
||||
apiService,
|
||||
@@ -77,6 +82,7 @@ describe("WebSetInitialPasswordService", () => {
|
||||
organizationInviteService,
|
||||
routerService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -208,4 +214,36 @@ describe("WebSetInitialPasswordService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initializePasswordJitPasswordUserV2Encryption(...)", () => {
|
||||
it("should call routerService.getAndClearLoginRedirectUrl() and organizationInviteService.clearOrganizationInvitation()", async () => {
|
||||
// Arrange
|
||||
const credentials: InitializeJitPasswordCredentials = {
|
||||
newPasswordHint: "newPasswordHint",
|
||||
orgSsoIdentifier: "orgSsoIdentifier",
|
||||
orgId: "orgId" as OrganizationId,
|
||||
resetPasswordAutoEnroll: false,
|
||||
newPassword: "newPassword123!",
|
||||
salt: "user@example.com" as MasterPasswordSalt,
|
||||
};
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const superSpy = jest
|
||||
.spyOn(
|
||||
Object.getPrototypeOf(Object.getPrototypeOf(sut)),
|
||||
"initializePasswordJitPasswordUserV2Encryption",
|
||||
)
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
// Assert
|
||||
expect(superSpy).toHaveBeenCalledWith(credentials, userId);
|
||||
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1);
|
||||
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1);
|
||||
|
||||
superSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -14,6 +15,7 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
@@ -36,6 +38,7 @@ export class WebSetInitialPasswordService
|
||||
private organizationInviteService: OrganizationInviteService,
|
||||
private routerService: RouterService,
|
||||
protected accountCryptographicStateService: AccountCryptographicStateService,
|
||||
protected registerSdkService: RegisterSdkService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -49,6 +52,7 @@ export class WebSetInitialPasswordService
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,4 +87,15 @@ export class WebSetInitialPasswordService
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
|
||||
override async initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials: InitializeJitPasswordCredentials,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await super.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
// TODO: Investigate refactoring the following logic in https://bitwarden.atlassian.net/browse/PM-22615
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Router } from "@angular/router";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
DefaultCollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
CollectionService,
|
||||
OrganizationUserService,
|
||||
DefaultCollectionAdminService,
|
||||
DefaultOrganizationUserService,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth/device-management/default-device-management-component.service";
|
||||
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
|
||||
@@ -27,17 +27,17 @@ import {
|
||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
SafeInjectionToken,
|
||||
SECURE_STORAGE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SafeInjectionToken,
|
||||
WINDOW,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import {
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
LoginComponentService,
|
||||
SsoComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SsoComponentService,
|
||||
TwoFactorAuthDuoComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
@@ -90,6 +90,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
@@ -120,9 +121,9 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
BiometricsService,
|
||||
KdfConfigService,
|
||||
KeyService as KeyServiceAbstraction,
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
LockComponentService,
|
||||
@@ -135,17 +136,17 @@ import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/va
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
import {
|
||||
POLICY_EDIT_REGISTER,
|
||||
ossPolicyEditRegister,
|
||||
POLICY_EDIT_REGISTER,
|
||||
} from "../admin-console/organizations/policies";
|
||||
import {
|
||||
LinkSsoService,
|
||||
WebChangePasswordService,
|
||||
WebRegistrationFinishService,
|
||||
WebLoginComponentService,
|
||||
WebLoginDecryptionOptionsService,
|
||||
WebTwoFactorAuthDuoComponentService,
|
||||
LinkSsoService,
|
||||
WebRegistrationFinishService,
|
||||
WebSetInitialPasswordService,
|
||||
WebTwoFactorAuthDuoComponentService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service";
|
||||
@@ -320,6 +321,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationInviteService,
|
||||
RouterService,
|
||||
AccountCryptographicStateService,
|
||||
RegisterSdkService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { concatMap, firstValueFrom } from "rxjs";
|
||||
|
||||
// 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
|
||||
@@ -19,19 +19,32 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import {
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
import {
|
||||
fromSdkKdfConfig,
|
||||
KdfConfig,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { OrganizationId as SdkOrganizationId, UserId as SdkUserId } from "@bitwarden/sdk-internal";
|
||||
|
||||
import {
|
||||
SetInitialPasswordService,
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordUserType,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordTdeOffboardingCredentials,
|
||||
SetInitialPasswordUserType,
|
||||
} from "./set-initial-password.service.abstraction";
|
||||
|
||||
export class DefaultSetInitialPasswordService implements SetInitialPasswordService {
|
||||
@@ -47,6 +60,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
protected accountCryptographicStateService: AccountCryptographicStateService,
|
||||
protected registerSdkService: RegisterSdkService,
|
||||
) {}
|
||||
|
||||
async setInitialPassword(
|
||||
@@ -199,6 +213,126 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
}
|
||||
}
|
||||
|
||||
async setInitialPasswordTdeOffboarding(
|
||||
credentials: SetInitialPasswordTdeOffboardingCredentials,
|
||||
userId: UserId,
|
||||
) {
|
||||
const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials;
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
if (value == null) {
|
||||
throw new Error(`${key} not found. Could not set password.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (userId == null) {
|
||||
throw new Error("userId not found. Could not set password.");
|
||||
}
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
if (userKey == null) {
|
||||
throw new Error("userKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
userKey,
|
||||
);
|
||||
|
||||
if (!newMasterKeyEncryptedUserKey[1].encryptedString) {
|
||||
throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
const request = new UpdateTdeOffboardingPasswordRequest();
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = newServerMasterKeyHash;
|
||||
request.masterPasswordHint = newPasswordHint;
|
||||
|
||||
await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
}
|
||||
|
||||
async initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials: InitializeJitPasswordCredentials,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
if (value == null) {
|
||||
throw new Error(`${key} is required.`);
|
||||
}
|
||||
}
|
||||
|
||||
const { newPasswordHint, orgSsoIdentifier, orgId, resetPasswordAutoEnroll, newPassword, salt } =
|
||||
credentials;
|
||||
|
||||
const organizationKeys = await this.organizationApiService.getKeys(orgId);
|
||||
if (organizationKeys == null) {
|
||||
throw new Error("Organization keys response is null.");
|
||||
}
|
||||
|
||||
const registerResult = await firstValueFrom(
|
||||
this.registerSdkService.registerClient$(userId).pipe(
|
||||
concatMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
return await ref.value
|
||||
.auth()
|
||||
.registration()
|
||||
.post_keys_for_jit_password_registration({
|
||||
org_id: asUuid<SdkOrganizationId>(orgId),
|
||||
org_public_key: organizationKeys.publicKey,
|
||||
master_password: newPassword,
|
||||
master_password_hint: newPasswordHint,
|
||||
salt: salt,
|
||||
organization_sso_identifier: orgSsoIdentifier,
|
||||
user_id: asUuid<SdkUserId>(userId),
|
||||
reset_password_enroll: resetPasswordAutoEnroll,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (!("V2" in registerResult.account_cryptographic_state)) {
|
||||
throw new Error("Unexpected V2 account cryptographic state");
|
||||
}
|
||||
|
||||
// Note: When SDK state management matures, these should be moved into post_keys_for_tde_registration
|
||||
// Set account cryptography state
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
registerResult.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
const masterPasswordUnlockData = MasterPasswordUnlockData.fromSdk(
|
||||
registerResult.master_password_unlock,
|
||||
);
|
||||
await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);
|
||||
|
||||
await this.keyService.setUserKey(
|
||||
SymmetricCryptoKey.fromString(registerResult.user_key) as UserKey,
|
||||
userId,
|
||||
);
|
||||
|
||||
await this.updateLegacyState(
|
||||
newPassword,
|
||||
fromSdkKdfConfig(registerResult.master_password_unlock.kdf),
|
||||
new EncString(registerResult.master_password_unlock.masterKeyWrappedUserKey),
|
||||
userId,
|
||||
masterPasswordUnlockData,
|
||||
);
|
||||
}
|
||||
|
||||
private async makeMasterKeyEncryptedUserKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
@@ -244,6 +378,37 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||
}
|
||||
|
||||
// Deprecated legacy support - to be removed in future
|
||||
private async updateLegacyState(
|
||||
newPassword: string,
|
||||
kdfConfig: KdfConfig,
|
||||
masterKeyWrappedUserKey: EncString,
|
||||
userId: UserId,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
) {
|
||||
// TODO Remove HasMasterPassword from UserDecryptionOptions https://bitwarden.atlassian.net/browse/PM-23475
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||
userId,
|
||||
userDecryptionOpts,
|
||||
);
|
||||
|
||||
// TODO Remove KDF state https://bitwarden.atlassian.net/browse/PM-30661
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
// TODO Remove master key memory state https://bitwarden.atlassian.net/browse/PM-23477
|
||||
await this.masterPasswordService.setMasterKeyEncryptedUserKey(masterKeyWrappedUserKey, userId);
|
||||
|
||||
// TODO Removed with https://bitwarden.atlassian.net/browse/PM-30676
|
||||
await this.masterPasswordService.setLegacyMasterKeyFromUnlockData(
|
||||
newPassword,
|
||||
masterPasswordUnlockData,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* As part of [PM-28494], adding this setting path to accommodate the changes that are
|
||||
* emerging with pm-23246-unlock-with-master-password-unlock-data.
|
||||
@@ -310,44 +475,4 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
enrollmentRequest,
|
||||
);
|
||||
}
|
||||
|
||||
async setInitialPasswordTdeOffboarding(
|
||||
credentials: SetInitialPasswordTdeOffboardingCredentials,
|
||||
userId: UserId,
|
||||
) {
|
||||
const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials;
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
if (value == null) {
|
||||
throw new Error(`${key} not found. Could not set password.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (userId == null) {
|
||||
throw new Error("userId not found. Could not set password.");
|
||||
}
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
if (userKey == null) {
|
||||
throw new Error("userKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
userKey,
|
||||
);
|
||||
|
||||
if (!newMasterKeyEncryptedUserKey[1].encryptedString) {
|
||||
throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
const request = new UpdateTdeOffboardingPasswordRequest();
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = newServerMasterKeyHash;
|
||||
request.masterPasswordHint = newPasswordHint;
|
||||
|
||||
await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
// Polyfill for Symbol.dispose required by the service's use of `using` keyword
|
||||
import "core-js/proposals/explicit-resource-management";
|
||||
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, Observable, of } from "rxjs";
|
||||
|
||||
// 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
|
||||
@@ -27,17 +30,35 @@ import {
|
||||
EncString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { Rc } from "@bitwarden/common/platform/misc/reference-counting/rc";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import {
|
||||
DEFAULT_KDF_CONFIG,
|
||||
fromSdkKdfConfig,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
AuthClient,
|
||||
BitwardenClient,
|
||||
WrappedAccountCryptographicState,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { DefaultSetInitialPasswordService } from "./default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordTdeOffboardingCredentials,
|
||||
@@ -58,6 +79,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
const registerSdkService = mock<RegisterSdkService>();
|
||||
|
||||
let userId: UserId;
|
||||
let userKey: UserKey;
|
||||
@@ -94,6 +116,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -834,4 +857,246 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initializePasswordJitPasswordUserV2Encryption()", () => {
|
||||
let mockSdkRef: {
|
||||
value: MockProxy<BitwardenClient>;
|
||||
[Symbol.dispose]: jest.Mock;
|
||||
};
|
||||
let mockSdk: {
|
||||
take: jest.Mock;
|
||||
};
|
||||
let mockRegistration: jest.Mock;
|
||||
|
||||
const userId = "d4e2e3a1-1b5e-4c3b-8d7a-9f8e7d6c5b4a" as UserId;
|
||||
const orgId = "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d" as OrganizationId;
|
||||
|
||||
const credentials: InitializeJitPasswordCredentials = {
|
||||
newPasswordHint: "test-hint",
|
||||
orgSsoIdentifier: "org-sso-id",
|
||||
orgId: orgId,
|
||||
resetPasswordAutoEnroll: false,
|
||||
newPassword: "Test@Password123!",
|
||||
salt: "user@example.com" as unknown as MasterPasswordSalt,
|
||||
};
|
||||
|
||||
const orgKeys: OrganizationKeysResponse = {
|
||||
publicKey: "org-public-key-base64",
|
||||
privateKey: "org-private-key-encrypted",
|
||||
} as OrganizationKeysResponse;
|
||||
|
||||
const sdkRegistrationResult = {
|
||||
account_cryptographic_state: {
|
||||
V2: {
|
||||
private_key: makeEncString().encryptedString!,
|
||||
signed_public_key: "test-signed-public-key",
|
||||
signing_key: makeEncString().encryptedString!,
|
||||
security_state: "test-security-state",
|
||||
},
|
||||
},
|
||||
master_password_unlock: {
|
||||
kdf: {
|
||||
pBKDF2: {
|
||||
iterations: 600000,
|
||||
},
|
||||
},
|
||||
masterKeyWrappedUserKey: makeEncString().encryptedString!,
|
||||
salt: "user@example.com" as unknown as MasterPasswordSalt,
|
||||
},
|
||||
user_key: makeSymmetricCryptoKey(64).keyB64,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockSdkRef = {
|
||||
value: mock<BitwardenClient>(),
|
||||
[Symbol.dispose]: jest.fn(),
|
||||
};
|
||||
|
||||
mockSdkRef.value.auth.mockReturnValue({
|
||||
registration: jest.fn().mockReturnValue({
|
||||
post_keys_for_jit_password_registration: jest.fn(),
|
||||
}),
|
||||
} as unknown as AuthClient);
|
||||
|
||||
mockSdk = {
|
||||
take: jest.fn().mockReturnValue(mockSdkRef),
|
||||
};
|
||||
|
||||
registerSdkService.registerClient$.mockReturnValue(
|
||||
of(mockSdk) as unknown as Observable<Rc<BitwardenClient>>,
|
||||
);
|
||||
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeys);
|
||||
|
||||
mockRegistration = mockSdkRef.value.auth().registration()
|
||||
.post_keys_for_jit_password_registration as unknown as jest.Mock;
|
||||
mockRegistration.mockResolvedValue(sdkRegistrationResult);
|
||||
|
||||
const mockUserDecryptionOpts = new UserDecryptionOptions({ hasMasterPassword: false });
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
of(mockUserDecryptionOpts),
|
||||
);
|
||||
});
|
||||
|
||||
it("should successfully initialize JIT password user", async () => {
|
||||
await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(credentials.orgId);
|
||||
|
||||
expect(registerSdkService.registerClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockRegistration).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
org_id: credentials.orgId,
|
||||
org_public_key: orgKeys.publicKey,
|
||||
master_password: credentials.newPassword,
|
||||
master_password_hint: credentials.newPasswordHint,
|
||||
salt: credentials.salt,
|
||||
organization_sso_identifier: credentials.orgSsoIdentifier,
|
||||
user_id: userId,
|
||||
reset_password_enroll: credentials.resetPasswordAutoEnroll,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
sdkRegistrationResult.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.None,
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
MasterPasswordUnlockData.fromSdk(sdkRegistrationResult.master_password_unlock),
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(
|
||||
SymmetricCryptoKey.fromString(sdkRegistrationResult.user_key) as UserKey,
|
||||
userId,
|
||||
);
|
||||
|
||||
// Verify legacy state updates below
|
||||
expect(userDecryptionOptionsService.userDecryptionOptionsById$).toHaveBeenCalledWith(userId);
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||
userId,
|
||||
expect.objectContaining({ hasMasterPassword: true }),
|
||||
);
|
||||
|
||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(
|
||||
userId,
|
||||
fromSdkKdfConfig(sdkRegistrationResult.master_password_unlock.kdf),
|
||||
);
|
||||
|
||||
expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
new EncString(sdkRegistrationResult.master_password_unlock.masterKeyWrappedUserKey),
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(masterPasswordService.setLegacyMasterKeyFromUnlockData).toHaveBeenCalledWith(
|
||||
credentials.newPassword,
|
||||
MasterPasswordUnlockData.fromSdk(sdkRegistrationResult.master_password_unlock),
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
it.each([
|
||||
"newPasswordHint",
|
||||
"orgSsoIdentifier",
|
||||
"orgId",
|
||||
"resetPasswordAutoEnroll",
|
||||
"newPassword",
|
||||
"salt",
|
||||
])("should throw error when %s is null", async (field) => {
|
||||
const invalidCredentials = {
|
||||
...credentials,
|
||||
[field]: null,
|
||||
} as unknown as InitializeJitPasswordCredentials;
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(
|
||||
invalidCredentials,
|
||||
userId,
|
||||
);
|
||||
|
||||
await expect(promise).rejects.toThrow(`${field} is required.`);
|
||||
|
||||
expect(organizationApiService.getKeys).not.toHaveBeenCalled();
|
||||
expect(registerSdkService.registerClient$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw error when userId is null", async () => {
|
||||
const nullUserId = null as unknown as UserId;
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, nullUserId);
|
||||
|
||||
await expect(promise).rejects.toThrow("User ID is required.");
|
||||
expect(organizationApiService.getKeys).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("organization API error handling", () => {
|
||||
it("should throw when organizationApiService.getKeys returns null", async () => {
|
||||
organizationApiService.getKeys.mockResolvedValue(
|
||||
null as unknown as OrganizationKeysResponse,
|
||||
);
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
await expect(promise).rejects.toThrow("Organization keys response is null.");
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(credentials.orgId);
|
||||
expect(registerSdkService.registerClient$).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw when organizationApiService.getKeys rejects", async () => {
|
||||
const apiError = new Error("API network error");
|
||||
organizationApiService.getKeys.mockRejectedValue(apiError);
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
await expect(promise).rejects.toThrow("API network error");
|
||||
expect(registerSdkService.registerClient$).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SDK error handling", () => {
|
||||
it("should throw when SDK is not available", async () => {
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeys);
|
||||
registerSdkService.registerClient$.mockReturnValue(
|
||||
of(null) as unknown as Observable<Rc<BitwardenClient>>,
|
||||
);
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
await expect(promise).rejects.toThrow("SDK not available");
|
||||
});
|
||||
|
||||
it("should throw when SDK registration fails", async () => {
|
||||
const sdkError = new Error("SDK crypto operation failed");
|
||||
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeys);
|
||||
mockRegistration.mockRejectedValue(sdkError);
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
await expect(promise).rejects.toThrow("SDK crypto operation failed");
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw when account_cryptographic_state is not V2", async () => {
|
||||
const invalidResult = {
|
||||
...sdkRegistrationResult,
|
||||
account_cryptographic_state: { V1: {} } as unknown as WrappedAccountCryptographicState,
|
||||
};
|
||||
|
||||
mockRegistration.mockResolvedValue(invalidResult);
|
||||
|
||||
const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
await expect(promise).rejects.toThrow("Unexpected V2 account cryptographic state");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,14 +21,16 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { assertTruthy, assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
ButtonModule,
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordTdeOffboardingCredentials,
|
||||
@@ -86,6 +89,7 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -101,6 +105,51 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
switch (this.userType) {
|
||||
case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER: {
|
||||
const accountEncryptionV2 = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.EnableAccountEncryptionV2JitPasswordRegistration,
|
||||
);
|
||||
|
||||
if (accountEncryptionV2) {
|
||||
await this.setInitialPasswordJitMPUserV2Encryption(passwordInputResult);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setInitialPassword(passwordInputResult);
|
||||
|
||||
break;
|
||||
}
|
||||
case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
|
||||
await this.setInitialPassword(passwordInputResult);
|
||||
break;
|
||||
case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER:
|
||||
await this.setInitialPasswordTdeOffboarding(passwordInputResult);
|
||||
break;
|
||||
default:
|
||||
this.logService.error(
|
||||
`Unexpected user type: ${this.userType}. Could not set initial password.`,
|
||||
);
|
||||
this.validationService.showError("Unexpected user type. Could not set initial password.");
|
||||
}
|
||||
}
|
||||
|
||||
protected async logout() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
private async establishUserType() {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found. Could not determine user type.");
|
||||
@@ -189,22 +238,39 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
private async setInitialPasswordJitMPUserV2Encryption(passwordInputResult: PasswordInputResult) {
|
||||
const ctx = "Could not set initial password for SSO JIT master password encryption user.";
|
||||
assertTruthy(passwordInputResult.newPassword, "newPassword", ctx);
|
||||
assertTruthy(passwordInputResult.salt, "salt", ctx);
|
||||
assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx);
|
||||
assertTruthy(this.orgId, "orgId", ctx);
|
||||
assertTruthy(this.userId, "userId", ctx);
|
||||
assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish
|
||||
assertNonNullish(this.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish
|
||||
|
||||
switch (this.userType) {
|
||||
case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER:
|
||||
case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
|
||||
await this.setInitialPassword(passwordInputResult);
|
||||
break;
|
||||
case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER:
|
||||
await this.setInitialPasswordTdeOffboarding(passwordInputResult);
|
||||
break;
|
||||
default:
|
||||
this.logService.error(
|
||||
`Unexpected user type: ${this.userType}. Could not set initial password.`,
|
||||
);
|
||||
this.validationService.showError("Unexpected user type. Could not set initial password.");
|
||||
try {
|
||||
const credentials: InitializeJitPasswordCredentials = {
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId as OrganizationId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
newPassword: passwordInputResult.newPassword,
|
||||
salt: passwordInputResult.salt,
|
||||
};
|
||||
|
||||
await this.setInitialPasswordService.initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials,
|
||||
this.userId,
|
||||
);
|
||||
|
||||
this.showSuccessToastByUserType();
|
||||
|
||||
this.submitting = false;
|
||||
await this.router.navigate(["vault"]);
|
||||
} catch (e) {
|
||||
this.logService.error("Error setting initial password", e);
|
||||
this.validationService.showError(e);
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,17 +373,4 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async logout() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
@@ -61,6 +61,24 @@ export interface SetInitialPasswordTdeOffboardingCredentials {
|
||||
newPasswordHint: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credentials required to initialize a just-in-time (JIT) provisioned user with a master password.
|
||||
*/
|
||||
export interface InitializeJitPasswordCredentials {
|
||||
/** Hint for the new master password */
|
||||
newPasswordHint: string;
|
||||
/** SSO identifier for the organization */
|
||||
orgSsoIdentifier: string;
|
||||
/** Organization ID */
|
||||
orgId: OrganizationId;
|
||||
/** Whether to auto-enroll the user in account recovery (reset password) */
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
/** The new master password */
|
||||
newPassword: string;
|
||||
/** Master password salt (typically the user's email) */
|
||||
salt: MasterPasswordSalt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setting an initial password for an existing authed user.
|
||||
*
|
||||
@@ -95,4 +113,14 @@ export abstract class SetInitialPasswordService {
|
||||
credentials: SetInitialPasswordTdeOffboardingCredentials,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Initializes a JIT-provisioned user's cryptographic state and enrolls them in master password unlock.
|
||||
* @param credentials The credentials needed to initialize the JIT password user
|
||||
* @param userId The account userId
|
||||
*/
|
||||
abstract initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials: InitializeJitPasswordCredentials,
|
||||
userId: UserId,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access";
|
||||
import { DefaultSendTokenService, SendTokenService } from "@bitwarden/common/auth/send-access";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
@@ -131,10 +131,10 @@ import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauth
|
||||
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import {
|
||||
TwoFactorApiService,
|
||||
DefaultTwoFactorApiService,
|
||||
TwoFactorService,
|
||||
DefaultTwoFactorService,
|
||||
TwoFactorApiService,
|
||||
TwoFactorService,
|
||||
} from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
@@ -208,8 +208,8 @@ import { PinService } from "@bitwarden/common/key-management/pin/pin.service.imp
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
|
||||
import {
|
||||
SendPasswordService,
|
||||
DefaultSendPasswordService,
|
||||
SendPasswordService,
|
||||
} from "@bitwarden/common/key-management/sends";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
@@ -387,12 +387,12 @@ import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
DefaultVaultExportApiService,
|
||||
IndividualVaultExportService,
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
DefaultVaultExportApiService,
|
||||
VaultExportApiService,
|
||||
OrganizationVaultExportService,
|
||||
OrganizationVaultExportServiceAbstraction,
|
||||
VaultExportApiService,
|
||||
VaultExportService,
|
||||
VaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
@@ -1583,6 +1583,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AccountCryptographicStateService,
|
||||
RegisterSdkService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -47,6 +47,7 @@ export enum FeatureFlag {
|
||||
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
|
||||
PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit",
|
||||
EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration",
|
||||
EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration",
|
||||
|
||||
/* Tools */
|
||||
UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators",
|
||||
@@ -156,6 +157,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
|
||||
[FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE,
|
||||
[FeatureFlag.EnableAccountEncryptionV2KeyConnectorRegistration]: FALSE,
|
||||
[FeatureFlag.EnableAccountEncryptionV2JitPasswordRegistration]: FALSE,
|
||||
|
||||
/* Platform */
|
||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -23,8 +23,8 @@
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.450",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.450",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.470",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.470",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "4.0.0",
|
||||
@@ -4982,9 +4982,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/commercial-sdk-internal": {
|
||||
"version": "0.2.0-main.450",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.450.tgz",
|
||||
"integrity": "sha512-WCihR6ykpIfaqJBHl4Wou4xDB8mp+5UPi94eEKYUdkx/9/19YyX33SX9H56zEriOuOMCD8l2fymhzAFjAAB++g==",
|
||||
"version": "0.2.0-main.470",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.470.tgz",
|
||||
"integrity": "sha512-QYhxv5eX6ouFJv94gMtBW7MjuK6t2KAN9FLz+/w1wnq8dScnA9Iky25phNPw+iHMgWwhq/dzZq45asKUFF//oA==",
|
||||
"license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT",
|
||||
"dependencies": {
|
||||
"type-fest": "^4.41.0"
|
||||
@@ -5087,9 +5087,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/sdk-internal": {
|
||||
"version": "0.2.0-main.450",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.450.tgz",
|
||||
"integrity": "sha512-XRhrBN0uoo66ONx7dYo9glhe9N451+VhwtC/oh3wo3j3qYxbPwf9yE98szlQ52u3iUExLisiYJY7sQNzhZrbZw==",
|
||||
"version": "0.2.0-main.470",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.470.tgz",
|
||||
"integrity": "sha512-XKvcUtoU6NnxeEzl3WK7bATiCh2RNxRmuX6JYNgcQHUtHUH+x3ckToR6II1qM3nha0VH0u1ijy3+07UdNQM+JQ==",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"type-fest": "^4.41.0"
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.450",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.450",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.470",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.470",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "4.0.0",
|
||||
|
||||
Reference in New Issue
Block a user