diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c4c24799da1..b402d01e209 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -132,6 +132,7 @@ "@yao-pkg/pkg", "anyhow", "arboard", + "ashpd", "babel-loader", "base64-loader", "base64", @@ -142,6 +143,7 @@ "core-foundation", "copy-webpack-plugin", "css-loader", + "ctor", "dirs", "electron", "electron-builder", @@ -179,6 +181,7 @@ "sass", "sass-loader", "scopeguard", + "secmem-proc", "security-framework", "security-framework-sys", "semver", @@ -187,6 +190,7 @@ "simplelog", "style-loader", "sysinfo", + "thiserror", "tokio", "tokio-util", "tracing", @@ -210,6 +214,7 @@ "windows-registry", "zbus", "zbus_polkit", + "zeroizing-alloc", ], description: "Platform owned dependencies", commitMessagePrefix: "[deps] Platform:", @@ -285,6 +290,7 @@ "@types/jsdom", "@types/papaparse", "@types/zxcvbn", + "aes-gcm", "async-trait", "clap", "jsdom", @@ -337,6 +343,7 @@ "aes", "big-integer", "cbc", + "chacha20poly1305", "linux-keyutils", "memsec", "node-forge", @@ -445,6 +452,7 @@ matchPackageNames: [ "anyhow", "arboard", + "ashpd", "babel-loader", "base64-loader", "base64", @@ -454,6 +462,7 @@ "core-foundation", "copy-webpack-plugin", "css-loader", + "ctor", "dirs", "electron-builder", "electron-log", @@ -488,6 +497,7 @@ "sass", "sass-loader", "scopeguard", + "secmem-proc", "security-framework", "security-framework-sys", "semver", @@ -496,6 +506,7 @@ "simplelog", "style-loader", "sysinfo", + "thiserror", "tokio", "tokio-util", "tracing", @@ -517,6 +528,7 @@ "windows-registry", "zbus", "zbus_polkit", + "zeroizing-alloc", ], matchUpdateTypes: ["minor", "patch"], dependencyDashboardApproval: true, diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 45297a110a0..8e43127770c 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -255,7 +255,7 @@ jobs: # Note: It is important that we use the release build because some compute heavy # operations such as key derivation for oo7 on linux are too slow in debug mode run: | - node build.js --release + node build.js --target=x86_64-unknown-linux-gnu --release - name: Build application run: npm run dist:lin @@ -418,7 +418,7 @@ jobs: # Note: It is important that we use the release build because some compute heavy # operations such as key derivation for oo7 on linux are too slow in debug mode run: | - node build.js --release + node build.js --target=aarch64-unknown-linux-gnu --release - name: Check index.d.ts generated if: github.event_name == 'pull_request' && steps.cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml index 908664209d8..000f4020961 100644 --- a/.github/workflows/review-code.yml +++ b/.github/workflows/review-code.yml @@ -2,7 +2,7 @@ name: Code Review on: pull_request: - types: [opened, labeled] + types: [opened, synchronize, reopened] permissions: {} diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index 0f799fe7d4d..ebabbadf71c 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -1,5 +1,5 @@ import { Component } from "@angular/core"; -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; @@ -37,7 +37,12 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { newGuid } from "@bitwarden/guid"; -import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management"; +import { + BiometricStateService, + BiometricsService, + BiometricsStatus, + KeyService, +} from "@bitwarden/key-management"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; @@ -64,6 +69,7 @@ describe("AccountSecurityComponent", () => { const apiService = mock(); const billingService = mock(); const biometricStateService = mock(); + const biometricsService = mock(); const configService = mock(); const dialogService = mock(); const keyService = mock(); @@ -75,6 +81,7 @@ describe("AccountSecurityComponent", () => { const validationService = mock(); const vaultNudgesService = mock(); const vaultTimeoutSettingsService = mock(); + const mockI18nService = mock(); // Mock subjects to control the phishing detection observables let phishingAvailableSubject: BehaviorSubject; @@ -91,14 +98,14 @@ describe("AccountSecurityComponent", () => { provide: BillingAccountProfileStateService, useValue: billingService, }, - { provide: BiometricsService, useValue: mock() }, + { provide: BiometricsService, useValue: biometricsService }, { provide: BiometricStateService, useValue: biometricStateService }, { provide: CipherService, useValue: mock() }, { provide: CollectionService, useValue: mock() }, { provide: ConfigService, useValue: configService }, { provide: DialogService, useValue: dialogService }, { provide: EnvironmentService, useValue: mock() }, - { provide: I18nService, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, { provide: KeyService, useValue: keyService }, { provide: LockService, useValue: lockService }, { provide: LogService, useValue: mock() }, @@ -153,6 +160,7 @@ describe("AccountSecurityComponent", () => { pinServiceAbstraction.isPinSet.mockResolvedValue(false); configService.getFeatureFlag$.mockReturnValue(of(false)); billingService.hasPremiumPersonally$.mockReturnValue(of(true)); + mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`); policyService.policiesByType$.mockReturnValue(of([null])); @@ -459,4 +467,118 @@ describe("AccountSecurityComponent", () => { }); }); }); + + describe("biometrics polling timer", () => { + let browserApiSpy: jest.SpyInstance; + + beforeEach(() => { + browserApiSpy = jest.spyOn(BrowserApi, "permissionsGranted"); + }); + + afterEach(() => { + component.ngOnDestroy(); + }); + + it("disables biometric control when canEnableBiometricUnlock is false", fakeAsync(async () => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(false); + + await component.ngOnInit(); + tick(); + + expect(component.form.controls.biometric.disabled).toBe(true); + })); + + it("enables biometric control when canEnableBiometricUnlock is true", fakeAsync(async () => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(true); + + await component.ngOnInit(); + tick(); + + expect(component.form.controls.biometric.disabled).toBe(false); + })); + + it("skips status check when nativeMessaging permission is not granted and not Safari", fakeAsync(async () => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(true); + browserApiSpy.mockResolvedValue(false); + platformUtilsService.isSafari.mockReturnValue(false); + + await component.ngOnInit(); + tick(); + + expect(biometricsService.getBiometricsStatusForUser).not.toHaveBeenCalled(); + expect(component.biometricUnavailabilityReason).toBeUndefined(); + })); + + it("checks biometrics status when nativeMessaging permission is granted", fakeAsync(async () => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(true); + browserApiSpy.mockResolvedValue(true); + platformUtilsService.isSafari.mockReturnValue(false); + biometricsService.getBiometricsStatusForUser.mockResolvedValue( + BiometricsStatus.DesktopDisconnected, + ); + + await component.ngOnInit(); + tick(); + + expect(biometricsService.getBiometricsStatusForUser).toHaveBeenCalledWith(mockUserId); + })); + + it("should check status on Safari", fakeAsync(async () => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(true); + browserApiSpy.mockResolvedValue(false); + platformUtilsService.isSafari.mockReturnValue(true); + biometricsService.getBiometricsStatusForUser.mockResolvedValue( + BiometricsStatus.DesktopDisconnected, + ); + + await component.ngOnInit(); + tick(); + + expect(biometricsService.getBiometricsStatusForUser).toHaveBeenCalledWith(mockUserId); + })); + + test.each([ + [ + BiometricsStatus.DesktopDisconnected, + "biometricsStatusHelptextDesktopDisconnected-used-i18n", + ], + [ + BiometricsStatus.NotEnabledInConnectedDesktopApp, + "biometricsStatusHelptextNotEnabledInDesktop-used-i18n", + ], + [ + BiometricsStatus.HardwareUnavailable, + "biometricsStatusHelptextHardwareUnavailable-used-i18n", + ], + ])( + "sets expected unavailability reason for %s status when biometric not available", + fakeAsync(async (biometricStatus: BiometricsStatus, expected: string) => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(false); + browserApiSpy.mockResolvedValue(true); + platformUtilsService.isSafari.mockReturnValue(false); + biometricsService.getBiometricsStatusForUser.mockResolvedValue(biometricStatus); + + await component.ngOnInit(); + tick(); + + expect(component.biometricUnavailabilityReason).toBe(expected); + }), + ); + + it("should not set unavailability reason for error statuses when biometric is available", fakeAsync(async () => { + biometricsService.canEnableBiometricUnlock.mockResolvedValue(true); + browserApiSpy.mockResolvedValue(true); + platformUtilsService.isSafari.mockReturnValue(false); + biometricsService.getBiometricsStatusForUser.mockResolvedValue( + BiometricsStatus.DesktopDisconnected, + ); + + await component.ngOnInit(); + tick(); + + // Status is DesktopDisconnected but biometric IS available, so don't show error + expect(component.biometricUnavailabilityReason).toBe(""); + component.ngOnDestroy(); + })); + }); }); diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 7c36754c894..6a3378670bf 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -149,6 +149,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { protected refreshTimeoutSettings$ = new BehaviorSubject(undefined); private destroy$ = new Subject(); + private readonly BIOMETRICS_POLLING_INTERVAL = 2000; constructor( private accountService: AccountService, @@ -264,10 +265,9 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }; this.form.patchValue(initialValues, { emitEvent: false }); - timer(0, 1000) + timer(0, this.BIOMETRICS_POLLING_INTERVAL) .pipe( switchMap(async () => { - const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id); const biometricSettingAvailable = await this.biometricsService.canEnableBiometricUnlock(); if (!biometricSettingAvailable) { this.form.controls.biometric.disable({ emitEvent: false }); @@ -275,6 +275,15 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { this.form.controls.biometric.enable({ emitEvent: false }); } + // Biometrics status shouldn't be checked if permissions are needed. + const needsPermissionPrompt = + !(await BrowserApi.permissionsGranted(["nativeMessaging"])) && + !this.platformUtilsService.isSafari(); + if (needsPermissionPrompt) { + return; + } + + const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id); if (status === BiometricsStatus.DesktopDisconnected && !biometricSettingAvailable) { this.biometricUnavailabilityReason = this.i18nService.t( "biometricsStatusHelptextDesktopDisconnected", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 64a8f3b842b..b9b41943b04 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -125,6 +125,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"; @@ -163,6 +164,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; @@ -578,7 +581,7 @@ export default class MainBackground { "ephemeral", "bitwarden-ephemeral", ); - await sessionStorage.save("session-key", derivedKey); + await sessionStorage.save("session-key", derivedKey.toJSON()); return derivedKey; }); @@ -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, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 2f1e92d14fc..d98b5f0a861 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -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, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0d2dc163295..5eaac4033eb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1150,6 +1150,10 @@ const safeProviders: SafeProvider[] = [ KeyGenerationService, LOGOUT_CALLBACK, StateProvider, + ConfigService, + RegisterSdkService, + SecurityStateService, + AccountCryptographicStateService, ], }), safeProvider({ diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5a6eeebd001..e5c29636585 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -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, diff --git a/libs/common/src/key-management/key-connector/models/new-sso-user-key-connector-conversion.ts b/libs/common/src/key-management/key-connector/models/new-sso-user-key-connector-conversion.ts index 12996747c96..3d686697a4e 100644 --- a/libs/common/src/key-management/key-connector/models/new-sso-user-key-connector-conversion.ts +++ b/libs/common/src/key-management/key-connector/models/new-sso-user-key-connector-conversion.ts @@ -5,5 +5,6 @@ import { KdfConfig } from "@bitwarden/key-management"; export interface NewSsoUserKeyConnectorConversion { kdfConfig: KdfConfig; keyConnectorUrl: string; + // SSO organization identifier, not UUID organizationId: string; } diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts index 45b4f5e4ac6..b8ee5d7df64 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts @@ -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(); const keyGenerationService = mock(); const logoutCallback = jest.fn(); + const configService = mock(); + const registerSdkService = mock(); + const securityStateService = mock(); + const accountCryptographicStateService = mock(); 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), + ); + + 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(); + }); }); }); diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index 8a75034cae1..751f1ec8594 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -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, 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( diff --git a/package-lock.json b/package-lock.json index c40b5361cc8..e2556015113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 29ee9683464..be6658964a0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/dep-ownership.ts b/scripts/dep-ownership.ts index f0bcb1f7dd8..ae1a19bb170 100644 --- a/scripts/dep-ownership.ts +++ b/scripts/dep-ownership.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -/// Ensure that all dependencies in package.json have an owner in the renovate.json file. +/// Ensure that all dependencies in package.json and Cargo.toml have an owner in the renovate.json5 file. import fs from "fs"; import path from "path"; @@ -11,22 +11,67 @@ const renovateConfig = JSON5.parse( fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json5"), "utf8"), ); +// Extract all packages with owners from renovate config const packagesWithOwners = renovateConfig.packageRules .flatMap((rule: any) => rule.matchPackageNames) .filter((packageName: string) => packageName != null); +function hasOwner(packageName: string): boolean { + return packagesWithOwners.includes(packageName); +} + +// Collect npm dependencies const packageJson = JSON.parse( fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8"), ); -const dependencies = Object.keys(packageJson.dependencies).concat( - Object.keys(packageJson.devDependencies), +const npmDependencies = [ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.devDependencies || {}), +]; + +// Collect Cargo dependencies from workspace Cargo.toml +const cargoTomlPath = path.join( + __dirname, + "..", + "..", + "apps", + "desktop", + "desktop_native", + "Cargo.toml", ); +const cargoTomlContent = fs.existsSync(cargoTomlPath) ? fs.readFileSync(cargoTomlPath, "utf8") : ""; -const missingOwners = dependencies.filter((dep) => !packagesWithOwners.includes(dep)); +const cargoDependencies = new Set(); -if (missingOwners.length > 0) { +// Extract dependency names from [workspace.dependencies] section by +// extracting everything between [workspace.dependencies] and the next section start +// (indicated by a "\n["). +const workspaceSection = + cargoTomlContent.split("[workspace.dependencies]")[1]?.split(/\n\[/)[0] ?? ""; + +// Process each line to extract dependency names +workspaceSection + .split("\n") // Process each line + .map((line) => line.match(/^([a-zA-Z0-9_-]+)\s*=/)?.[1]) // Find the dependency name + .filter((depName): depName is string => depName != null && !depName.startsWith("bitwarden")) // Make sure it's not an empty line or a Bitwarden dependency + .forEach((depName) => cargoDependencies.add(depName)); + +// Check for missing owners +const missingNpmOwners = npmDependencies.filter((dep) => !hasOwner(dep)); +const missingCargoOwners = Array.from(cargoDependencies).filter((dep) => !hasOwner(dep)); + +const allMissing = [...missingNpmOwners, ...missingCargoOwners]; + +if (allMissing.length > 0) { console.error("Missing owners for the following dependencies:"); - console.error(missingOwners.join("\n")); + if (missingNpmOwners.length > 0) { + console.error("\nNPM dependencies:"); + console.error(missingNpmOwners.join("\n")); + } + if (missingCargoOwners.length > 0) { + console.error("\nCargo dependencies:"); + console.error(missingCargoOwners.join("\n")); + } process.exit(1); }