1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

[PM-27236] account registration v2 for key connector (#17951)

* account registration v2 for key connector

* explicit naming

* test coverage

* missing AccountCryptographicStateService and DI dependencies

* redundant SdkLoadService.Ready

* update sdk version
This commit is contained in:
Maciej Zieniuk
2025-12-31 11:04:54 +01:00
committed by jaasen-livefront
parent a3e5b94ba9
commit b0000476b1
9 changed files with 536 additions and 161 deletions

View File

@@ -126,6 +126,7 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
@@ -164,6 +165,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
@@ -463,6 +465,7 @@ export default class MainBackground {
themeStateService: DefaultThemeStateService;
autoSubmitLoginBackground: AutoSubmitLoginBackground;
sdkService: SdkService;
registerSdkService: RegisterSdkService;
sdkLoadService: SdkLoadService;
cipherAuthorizationService: CipherAuthorizationService;
endUserNotificationService: EndUserNotificationService;
@@ -797,18 +800,6 @@ export default class MainBackground {
this.apiService,
this.accountService,
);
this.keyConnectorService = new KeyConnectorService(
this.accountService,
this.masterPasswordService,
this.keyService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.keyGenerationService,
logoutCallback,
this.stateProvider,
);
this.authService = new AuthService(
this.accountService,
@@ -846,6 +837,37 @@ export default class MainBackground {
this.configService,
);
this.registerSdkService = new DefaultRegisterSdkService(
sdkClientFactory,
this.environmentService,
this.platformUtilsService,
this.accountService,
this.apiService,
this.stateProvider,
this.configService,
);
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
this.stateProvider,
);
this.keyConnectorService = new KeyConnectorService(
this.accountService,
this.masterPasswordService,
this.keyService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.keyGenerationService,
logoutCallback,
this.stateProvider,
this.configService,
this.registerSdkService,
this.securityStateService,
this.accountCryptographicStateService,
);
this.pinService = new PinService(
this.encryptService,
this.logService,
@@ -1013,9 +1035,7 @@ export default class MainBackground {
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
this.providerService = new ProviderService(this.stateProvider);
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
this.stateProvider,
);
this.syncService = new DefaultSyncService(
this.masterPasswordService,
this.accountService,

View File

@@ -104,6 +104,7 @@ import {
EnvironmentService,
RegionConfig,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { LogLevelType } from "@bitwarden/common/platform/enums";
@@ -124,6 +125,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { SyncService } from "@bitwarden/common/platform/sync";
@@ -323,6 +325,7 @@ export class ServiceContainer {
kdfConfigService: KdfConfigService;
taskSchedulerService: TaskSchedulerService;
sdkService: SdkService;
registerSdkService: RegisterSdkService;
sdkLoadService: SdkLoadService;
cipherAuthorizationService: CipherAuthorizationService;
ssoUrlService: SsoUrlService;
@@ -632,26 +635,10 @@ export class ServiceContainer {
this.accountService,
);
this.keyConnectorService = new KeyConnectorService(
this.accountService,
this.masterPasswordService,
this.keyService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.keyGenerationService,
logoutCallback,
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
this.stateProvider,
);
this.twoFactorService = new DefaultTwoFactorService(
this.i18nService,
this.platformUtilsService,
this.globalStateProvider,
this.twoFactorApiService,
);
const sdkClientFactory = flagEnabled("sdk")
? new DefaultSdkClientFactory()
: new NoopSdkClientFactory();
@@ -670,6 +657,41 @@ export class ServiceContainer {
customUserAgent,
);
this.registerSdkService = new DefaultRegisterSdkService(
sdkClientFactory,
this.environmentService,
this.platformUtilsService,
this.accountService,
this.apiService,
this.stateProvider,
this.configService,
customUserAgent,
);
this.keyConnectorService = new KeyConnectorService(
this.accountService,
this.masterPasswordService,
this.keyService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.keyGenerationService,
logoutCallback,
this.stateProvider,
this.configService,
this.registerSdkService,
this.securityStateService,
this.accountCryptographicStateService,
);
this.twoFactorService = new DefaultTwoFactorService(
this.i18nService,
this.platformUtilsService,
this.globalStateProvider,
this.twoFactorApiService,
);
this.passwordStrengthService = new PasswordStrengthService();
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
@@ -719,10 +741,6 @@ export class ServiceContainer {
this.accountService,
);
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
this.stateProvider,
);
this.loginStrategyService = new LoginStrategyService(
this.accountService,
this.masterPasswordService,

View File

@@ -1146,6 +1146,10 @@ const safeProviders: SafeProvider[] = [
KeyGenerationService,
LOGOUT_CALLBACK,
StateProvider,
ConfigService,
RegisterSdkService,
SecurityStateService,
AccountCryptographicStateService,
],
}),
safeProvider({

View File

@@ -45,6 +45,7 @@ export enum FeatureFlag {
DataRecoveryTool = "pm-28813-data-recovery-tool",
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit",
EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration",
/* Tools */
DesktopSendUIRefresh = "desktop-send-ui-refresh",
@@ -152,6 +153,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.DataRecoveryTool]: FALSE,
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
[FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE,
[FeatureFlag.EnableAccountEncryptionV2KeyConnectorRegistration]: FALSE,
/* Platform */
[FeatureFlag.IpcChannelFramework]: FALSE,

View File

@@ -5,5 +5,6 @@ import { KdfConfig } from "@bitwarden/key-management";
export interface NewSsoUserKeyConnectorConversion {
kdfConfig: KdfConfig;
keyConnectorUrl: string;
// SSO organization identifier, not UUID
organizationId: string;
}

View File

@@ -7,7 +7,8 @@ import { SetKeyConnectorKeyRequest } from "@bitwarden/common/key-management/key-
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
// 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 { Argon2KdfConfig, PBKDF2KdfConfig, KeyService, KdfType } from "@bitwarden/key-management";
import { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { ApiService } from "../../../abstractions/api.service";
@@ -16,21 +17,26 @@ import { Organization } from "../../../admin-console/models/domain/organization"
import { ProfileOrganizationResponse } from "../../../admin-console/models/response/profile-organization.response";
import { KeyConnectorUserKeyResponse } from "../../../auth/models/response/key-connector-user-key.response";
import { TokenService } from "../../../auth/services/token.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { RegisterSdkService } from "../../../platform/abstractions/sdk/register-sdk.service";
import { Rc } from "../../../platform/misc/reference-counting/rc";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { OrganizationId, UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { AccountCryptographicStateService } from "../../account-cryptography/account-cryptographic-state.service";
import { KeyGenerationService } from "../../crypto";
import { EncString } from "../../crypto/models/enc-string";
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
import { SecurityStateService } from "../../security-state/abstractions/security-state.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
import { NewSsoUserKeyConnectorConversion } from "../models/new-sso-user-key-connector-conversion";
import {
USES_KEY_CONNECTOR,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
KeyConnectorService,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
USES_KEY_CONNECTOR,
} from "./key-connector.service";
describe("KeyConnectorService", () => {
@@ -43,6 +49,10 @@ describe("KeyConnectorService", () => {
const organizationService = mock<OrganizationService>();
const keyGenerationService = mock<KeyGenerationService>();
const logoutCallback = jest.fn();
const configService = mock<ConfigService>();
const registerSdkService = mock<RegisterSdkService>();
const securityStateService = mock<SecurityStateService>();
const accountCryptographicStateService = mock<AccountCryptographicStateService>();
let stateProvider: FakeStateProvider;
@@ -50,6 +60,7 @@ describe("KeyConnectorService", () => {
let masterPasswordService: FakeMasterPasswordService;
const mockUserId = Utils.newGuid() as UserId;
const mockSsoOrgIdentifier = "test-sso-org-id";
const mockOrgId = Utils.newGuid() as OrganizationId;
const mockMasterKeyResponse: KeyConnectorUserKeyResponse = new KeyConnectorUserKeyResponse({
@@ -61,7 +72,7 @@ describe("KeyConnectorService", () => {
const conversion: NewSsoUserKeyConnectorConversion = {
kdfConfig: new PBKDF2KdfConfig(600_000),
keyConnectorUrl,
organizationId: mockOrgId,
organizationId: mockSsoOrgIdentifier,
};
beforeEach(() => {
@@ -82,6 +93,10 @@ describe("KeyConnectorService", () => {
keyGenerationService,
logoutCallback,
stateProvider,
configService,
registerSdkService,
securityStateService,
accountCryptographicStateService,
);
});
@@ -419,44 +434,52 @@ describe("KeyConnectorService", () => {
});
describe("convertNewSsoUserToKeyConnector", () => {
const passwordKey = new SymmetricCryptoKey(new Uint8Array(64));
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const mockEmail = "test@example.com";
const mockMasterKey = getMockMasterKey();
const mockKeyPair = ["mockPubKey", new EncString("mockEncryptedPrivKey")] as [
string,
EncString,
];
let mockMakeUserKeyResult: [UserKey, EncString];
describe("V2", () => {
const mockKeyConnectorKey = Utils.fromBufferToB64(new Uint8Array(64));
const mockUserKeyString = Utils.fromBufferToB64(new Uint8Array(64));
const mockPrivateKey = "mockPrivateKey789";
const mockKeyConnectorKeyWrappedUserKey = "2.mockWrappedUserKey";
const mockSigningKey = "mockSigningKey";
const mockSignedPublicKey = "mockSignedPublicKey";
const mockSecurityState = "mockSecurityState";
beforeEach(() => {
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const encString = new EncString("mockEncryptedString");
mockMakeUserKeyResult = [mockUserKey, encString] as [UserKey, EncString];
let mockSdkRef: any;
let mockSdk: any;
keyGenerationService.createKey.mockResolvedValue(passwordKey);
keyService.makeMasterKey.mockResolvedValue(mockMasterKey);
keyService.makeUserKey.mockResolvedValue(mockMakeUserKeyResult);
keyService.makeKeyPair.mockResolvedValue(mockKeyPair);
tokenService.getEmail.mockResolvedValue(mockEmail);
});
beforeEach(() => {
configService.getFeatureFlag$.mockReturnValue(of(true));
it.each([
[KdfType.PBKDF2_SHA256, 700_000, undefined, undefined],
[KdfType.Argon2id, 11, 65, 5],
])(
"sets up a new SSO user with key connector",
async (kdfType, kdfIterations, kdfMemory, kdfParallelism) => {
const expectedKdfConfig =
kdfType == KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(kdfIterations)
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const conversion: NewSsoUserKeyConnectorConversion = {
kdfConfig: expectedKdfConfig,
keyConnectorUrl: keyConnectorUrl,
organizationId: mockOrgId,
mockSdkRef = {
value: {
auth: jest.fn().mockReturnValue({
registration: jest.fn().mockReturnValue({
post_keys_for_key_connector_registration: jest.fn().mockResolvedValue({
key_connector_key: mockKeyConnectorKey,
user_key: mockUserKeyString,
key_connector_key_wrapped_user_key: mockKeyConnectorKeyWrappedUserKey,
account_cryptographic_state: {
V2: {
private_key: mockPrivateKey,
signing_key: mockSigningKey,
signed_public_key: mockSignedPublicKey,
security_state: mockSecurityState,
},
},
}),
}),
}),
},
[Symbol.dispose]: jest.fn(),
};
mockSdk = {
take: jest.fn().mockReturnValue(mockSdkRef),
};
registerSdkService.registerClient$.mockReturnValue(of(mockSdk));
});
it("should set up a new SSO user with key connector using V2", async () => {
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
@@ -465,11 +488,253 @@ describe("KeyConnectorService", () => {
await keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId);
expect(registerSdkService.registerClient$).toHaveBeenCalledWith(mockUserId);
expect(mockSdk.take).toHaveBeenCalled();
expect(mockSdkRef.value.auth).toHaveBeenCalled();
const mockRegistration = mockSdkRef.value
.auth()
.registration().post_keys_for_key_connector_registration;
expect(mockRegistration).toHaveBeenCalledWith(
keyConnectorUrl,
mockSsoOrgIdentifier,
mockUserId,
);
expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
expect.any(SymmetricCryptoKey),
mockUserId,
);
expect(keyService.setUserKey).toHaveBeenCalledWith(
expect.any(SymmetricCryptoKey),
mockUserId,
);
expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
expect.any(EncString),
mockUserId,
);
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
{
V2: {
private_key: mockPrivateKey,
signing_key: mockSigningKey,
signed_public_key: mockSignedPublicKey,
security_state: mockSecurityState,
},
},
mockUserId,
);
expect(keyService.setPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockUserId);
expect(keyService.setUserSigningKey).toHaveBeenCalledWith(mockSigningKey, mockUserId);
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
mockSecurityState,
mockUserId,
);
expect(keyService.setSignedPublicKey).toHaveBeenCalledWith(mockSignedPublicKey, mockUserId);
expect(await firstValueFrom(conversionState.state$)).toBeNull();
});
it("should throw error when SDK is not available", async () => {
registerSdkService.registerClient$.mockReturnValue(
of(null as unknown as Rc<BitwardenClient>),
);
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(conversion);
await expect(
keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId),
).rejects.toThrow("SDK not available");
expect(await firstValueFrom(conversionState.state$)).toEqual(conversion);
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
expect(keyService.setUserKey).not.toHaveBeenCalled();
expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).not.toHaveBeenCalled();
expect(
accountCryptographicStateService.setAccountCryptographicState,
).not.toHaveBeenCalled();
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
expect(keyService.setUserSigningKey).not.toHaveBeenCalled();
expect(securityStateService.setAccountSecurityState).not.toHaveBeenCalled();
expect(keyService.setSignedPublicKey).not.toHaveBeenCalled();
});
it("should throw error when account cryptographic state is not V2", async () => {
mockSdkRef.value
.auth()
.registration()
.post_keys_for_key_connector_registration.mockResolvedValue({
key_connector_key: mockKeyConnectorKey,
user_key: mockUserKeyString,
key_connector_key_wrapped_user_key: mockKeyConnectorKeyWrappedUserKey,
account_cryptographic_state: {
V1: {
private_key: mockPrivateKey,
},
},
});
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(conversion);
await expect(
keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId),
).rejects.toThrow("Unexpected account cryptographic state version");
expect(await firstValueFrom(conversionState.state$)).toEqual(conversion);
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
expect(keyService.setUserKey).not.toHaveBeenCalled();
expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).not.toHaveBeenCalled();
expect(
accountCryptographicStateService.setAccountCryptographicState,
).not.toHaveBeenCalled();
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
expect(keyService.setUserSigningKey).not.toHaveBeenCalled();
expect(securityStateService.setAccountSecurityState).not.toHaveBeenCalled();
expect(keyService.setSignedPublicKey).not.toHaveBeenCalled();
});
it("should throw error when post_keys_for_key_connector_registration fails", async () => {
const sdkError = new Error("Key Connector registration failed");
mockSdkRef.value
.auth()
.registration()
.post_keys_for_key_connector_registration.mockRejectedValue(sdkError);
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(conversion);
await expect(
keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId),
).rejects.toThrow("Key Connector registration failed");
expect(await firstValueFrom(conversionState.state$)).toEqual(conversion);
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
expect(keyService.setUserKey).not.toHaveBeenCalled();
expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).not.toHaveBeenCalled();
expect(
accountCryptographicStateService.setAccountCryptographicState,
).not.toHaveBeenCalled();
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
expect(keyService.setUserSigningKey).not.toHaveBeenCalled();
expect(securityStateService.setAccountSecurityState).not.toHaveBeenCalled();
expect(keyService.setSignedPublicKey).not.toHaveBeenCalled();
});
});
describe("V1", () => {
const passwordKey = new SymmetricCryptoKey(new Uint8Array(64));
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const mockEmail = "test@example.com";
const mockMasterKey = getMockMasterKey();
const mockKeyPair = ["mockPubKey", new EncString("mockEncryptedPrivKey")] as [
string,
EncString,
];
let mockMakeUserKeyResult: [UserKey, EncString];
beforeEach(() => {
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const encString = new EncString("mockEncryptedString");
mockMakeUserKeyResult = [mockUserKey, encString] as [UserKey, EncString];
keyGenerationService.createKey.mockResolvedValue(passwordKey);
keyService.makeMasterKey.mockResolvedValue(mockMasterKey);
keyService.makeUserKey.mockResolvedValue(mockMakeUserKeyResult);
keyService.makeKeyPair.mockResolvedValue(mockKeyPair);
tokenService.getEmail.mockResolvedValue(mockEmail);
configService.getFeatureFlag$.mockReturnValue(of(false));
});
it.each([
[KdfType.PBKDF2_SHA256, 700_000, undefined, undefined],
[KdfType.Argon2id, 11, 65, 5],
])(
"sets up a new SSO user with key connector",
async (kdfType, kdfIterations, kdfMemory, kdfParallelism) => {
const expectedKdfConfig =
kdfType == KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(kdfIterations)
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const conversion: NewSsoUserKeyConnectorConversion = {
kdfConfig: expectedKdfConfig,
keyConnectorUrl: keyConnectorUrl,
organizationId: mockSsoOrgIdentifier,
};
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(conversion);
await keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId);
expect(keyGenerationService.createKey).toHaveBeenCalledWith(512);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
passwordKey.keyB64,
mockEmail,
expectedKdfConfig,
);
expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
mockMasterKey,
mockUserId,
);
expect(keyService.makeUserKey).toHaveBeenCalledWith(mockMasterKey);
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, mockUserId);
expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
mockMakeUserKeyResult[1],
mockUserId,
);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(mockMakeUserKeyResult[0]);
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
keyConnectorUrl,
new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(mockMasterKey.inner().encryptionKey),
),
);
expect(apiService.postSetKeyConnectorKey).toHaveBeenCalledWith(
new SetKeyConnectorKeyRequest(
mockMakeUserKeyResult[1].encryptedString!,
expectedKdfConfig,
mockSsoOrgIdentifier,
new KeysRequest(mockKeyPair[0], mockKeyPair[1].encryptedString!),
),
);
// Verify that conversion data is cleared from conversionState
expect(await firstValueFrom(conversionState.state$)).toBeNull();
},
);
it("handles api error", async () => {
apiService.postUserKeyToKeyConnector.mockRejectedValue(new Error("API error"));
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(conversion);
await expect(
keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId),
).rejects.toThrow(new Error("Key Connector error"));
expect(keyGenerationService.createKey).toHaveBeenCalledWith(512);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
passwordKey.keyB64,
mockEmail,
expectedKdfConfig,
new PBKDF2KdfConfig(600_000),
);
expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
mockMasterKey,
@@ -488,76 +753,29 @@ describe("KeyConnectorService", () => {
Utils.fromBufferToB64(mockMasterKey.inner().encryptionKey),
),
);
expect(apiService.postSetKeyConnectorKey).toHaveBeenCalledWith(
new SetKeyConnectorKeyRequest(
mockMakeUserKeyResult[1].encryptedString!,
expectedKdfConfig,
mockOrgId,
new KeysRequest(mockKeyPair[0], mockKeyPair[1].encryptedString!),
),
expect(apiService.postSetKeyConnectorKey).not.toHaveBeenCalled();
expect(await firstValueFrom(conversionState.state$)).toEqual(conversion);
expect(logoutCallback).toHaveBeenCalledWith("keyConnectorError");
});
it("should throw error when conversion data is null", async () => {
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(null);
// Verify that conversion data is cleared from conversionState
expect(await firstValueFrom(conversionState.state$)).toBeNull();
},
);
await expect(
keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId),
).rejects.toThrow(new Error("Key Connector conversion not found"));
it("handles api error", async () => {
apiService.postUserKeyToKeyConnector.mockRejectedValue(new Error("API error"));
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(conversion);
await expect(keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId)).rejects.toThrow(
new Error("Key Connector error"),
);
expect(keyGenerationService.createKey).toHaveBeenCalledWith(512);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
passwordKey.keyB64,
mockEmail,
new PBKDF2KdfConfig(600_000),
);
expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
mockMasterKey,
mockUserId,
);
expect(keyService.makeUserKey).toHaveBeenCalledWith(mockMasterKey);
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, mockUserId);
expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
mockMakeUserKeyResult[1],
mockUserId,
);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(mockMakeUserKeyResult[0]);
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
keyConnectorUrl,
new KeyConnectorUserKeyRequest(Utils.fromBufferToB64(mockMasterKey.inner().encryptionKey)),
);
expect(apiService.postSetKeyConnectorKey).not.toHaveBeenCalled();
expect(await firstValueFrom(conversionState.state$)).toEqual(conversion);
expect(logoutCallback).toHaveBeenCalledWith("keyConnectorError");
});
it("should throw error when conversion data is null", async () => {
const conversionState = stateProvider.singleUser.getFake(
mockUserId,
NEW_SSO_USER_KEY_CONNECTOR_CONVERSION,
);
conversionState.nextState(null);
await expect(keyConnectorService.convertNewSsoUserToKeyConnector(mockUserId)).rejects.toThrow(
new Error("Key Connector conversion not found"),
);
// Verify that no key generation or API calls were made
expect(keyGenerationService.createKey).not.toHaveBeenCalled();
expect(keyService.makeMasterKey).not.toHaveBeenCalled();
expect(apiService.postUserKeyToKeyConnector).not.toHaveBeenCalled();
expect(apiService.postSetKeyConnectorKey).not.toHaveBeenCalled();
// Verify that no key generation or API calls were made
expect(keyGenerationService.createKey).not.toHaveBeenCalled();
expect(keyService.makeMasterKey).not.toHaveBeenCalled();
expect(apiService.postUserKeyToKeyConnector).not.toHaveBeenCalled();
expect(apiService.postSetKeyConnectorKey).not.toHaveBeenCalled();
});
});
});

View File

@@ -9,22 +9,36 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { NewSsoUserKeyConnectorConversion } from "@bitwarden/common/key-management/key-connector/models/new-sso-user-key-connector-conversion";
// 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 { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
import {
Argon2KdfConfig,
KdfConfig,
KdfType,
KeyService,
PBKDF2KdfConfig,
} from "@bitwarden/key-management";
import { LogService } from "@bitwarden/logging";
import { ApiService } from "../../../abstractions/api.service";
import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "../../../admin-console/enums";
import { Organization } from "../../../admin-console/models/domain/organization";
import { TokenService } from "../../../auth/abstractions/token.service";
import { FeatureFlag } from "../../../enums/feature-flag.enum";
import { KeysRequest } from "../../../models/request/keys.request";
import { LogService } from "../../../platform/abstractions/log.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { RegisterSdkService } from "../../../platform/abstractions/sdk/register-sdk.service";
import { asUuid } from "../../../platform/abstractions/sdk/sdk.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { KEY_CONNECTOR_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { MasterKey, UserKey } from "../../../types/key";
import { AccountCryptographicStateService } from "../../account-cryptography/account-cryptographic-state.service";
import { KeyGenerationService } from "../../crypto";
import { EncString } from "../../crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
import { SecurityStateService } from "../../security-state/abstractions/security-state.service";
import { SignedPublicKey, SignedSecurityState, WrappedSigningKey } from "../../types";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { KeyConnectorDomainConfirmation } from "../models/key-connector-domain-confirmation";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
@@ -75,6 +89,10 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
private keyGenerationService: KeyGenerationService,
private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise<void>,
private stateProvider: StateProvider,
private configService: ConfigService,
private registerSdkService: RegisterSdkService,
private securityStateService: SecurityStateService,
private accountCryptographicStateService: AccountCryptographicStateService,
) {
this.convertAccountRequired$ = accountService.activeAccount$.pipe(
filter((account) => account != null),
@@ -152,8 +170,106 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
throw new Error("Key Connector conversion not found");
}
const { kdfConfig, keyConnectorUrl, organizationId } = conversion;
const { kdfConfig, keyConnectorUrl, organizationId: ssoOrganizationIdentifier } = conversion;
if (
await firstValueFrom(
this.configService.getFeatureFlag$(
FeatureFlag.EnableAccountEncryptionV2KeyConnectorRegistration,
),
)
) {
await this.convertNewSsoUserToKeyConnectorV2(
userId,
keyConnectorUrl,
ssoOrganizationIdentifier,
);
} else {
await this.convertNewSsoUserToKeyConnectorV1(
userId,
kdfConfig,
keyConnectorUrl,
ssoOrganizationIdentifier,
);
}
await this.stateProvider
.getUser(userId, NEW_SSO_USER_KEY_CONNECTOR_CONVERSION)
.update(() => null);
}
async convertNewSsoUserToKeyConnectorV2(
userId: UserId,
keyConnectorUrl: string,
ssoOrganizationIdentifier: string,
) {
const result = await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map((sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value
.auth()
.registration()
.post_keys_for_key_connector_registration(
keyConnectorUrl,
ssoOrganizationIdentifier,
asUuid(userId),
);
}),
),
);
if (!("V2" in result.account_cryptographic_state)) {
const version = Object.keys(result.account_cryptographic_state);
throw new Error(`Unexpected account cryptographic state version ${version}`);
}
await this.masterPasswordService.setMasterKey(
SymmetricCryptoKey.fromString(result.key_connector_key) as MasterKey,
userId,
);
await this.keyService.setUserKey(
SymmetricCryptoKey.fromString(result.user_key) as UserKey,
userId,
);
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(result.key_connector_key_wrapped_user_key),
userId,
);
await this.accountCryptographicStateService.setAccountCryptographicState(
result.account_cryptographic_state,
userId,
);
// Legacy states
await this.keyService.setPrivateKey(result.account_cryptographic_state.V2.private_key, userId);
await this.keyService.setUserSigningKey(
result.account_cryptographic_state.V2.signing_key as WrappedSigningKey,
userId,
);
await this.securityStateService.setAccountSecurityState(
result.account_cryptographic_state.V2.security_state as SignedSecurityState,
userId,
);
if (result.account_cryptographic_state.V2.signed_public_key != null) {
await this.keyService.setSignedPublicKey(
result.account_cryptographic_state.V2.signed_public_key as SignedPublicKey,
userId,
);
}
}
async convertNewSsoUserToKeyConnectorV1(
userId: UserId,
kdfConfig: KdfConfig,
keyConnectorUrl: string,
ssoOrganizationIdentifier: string,
) {
const password = await this.keyGenerationService.createKey(512);
const masterKey = await this.keyService.makeMasterKey(
@@ -182,14 +298,10 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const setPasswordRequest = new SetKeyConnectorKeyRequest(
userKey[1].encryptedString,
kdfConfig,
organizationId,
ssoOrganizationIdentifier,
keys,
);
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
await this.stateProvider
.getUser(userId, NEW_SSO_USER_KEY_CONNECTOR_CONVERSION)
.update(() => null);
}
async setNewSsoUserKeyConnectorConversionData(

16
package-lock.json generated
View File

@@ -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.439",
"@bitwarden/sdk-internal": "0.2.0-main.439",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.450",
"@bitwarden/sdk-internal": "0.2.0-main.450",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -4973,9 +4973,9 @@
"link": true
},
"node_modules/@bitwarden/commercial-sdk-internal": {
"version": "0.2.0-main.439",
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.439.tgz",
"integrity": "sha512-Wujtym00U7XMEsf9zJ3/0Ggw9WmMcIpE9hMtcLryloX182118vnzkEQbEldqtywpMHiDsD9VmP6RiZ725nnUIQ==",
"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==",
"license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT",
"dependencies": {
"type-fest": "^4.41.0"
@@ -5078,9 +5078,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
"version": "0.2.0-main.439",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.439.tgz",
"integrity": "sha512-uvIS8erGmzgWCZom7Kt78C4n4tbjfZuTCn7+y2+E8BTtLBqIZNtl4kC0tNh8c4GUWsmoIYlbQyz+HymWQ7J+QA==",
"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==",
"license": "GPL-3.0",
"dependencies": {
"type-fest": "^4.41.0"

View File

@@ -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.439",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.439",
"@bitwarden/sdk-internal": "0.2.0-main.450",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.450",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",