diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index 44754995f72..53a1c7dbd4c 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -127,8 +127,7 @@ describe("DesktopSetInitialPasswordService", () => { credentials.newPasswordHint, credentials.orgSsoIdentifier, keysRequest, - credentials.kdfConfig.kdfType, - credentials.kdfConfig.iterations, + credentials.kdfConfig, ); }); diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts index 2993a3774d3..70f7686a2cd 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts @@ -131,8 +131,7 @@ describe("WebSetInitialPasswordService", () => { credentials.newPasswordHint, credentials.orgSsoIdentifier, keysRequest, - credentials.kdfConfig.kdfType, - credentials.kdfConfig.iterations, + credentials.kdfConfig, ); }); diff --git a/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.ts index 5aa8eeb907c..3e09f710062 100644 --- a/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.ts @@ -4,13 +4,13 @@ import { Component, Inject } from "@angular/core"; import { FormGroup, FormControl, Validators } from "@angular/forms"; import { firstValueFrom } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DIALOG_DATA, ToastService } from "@bitwarden/components"; -import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management"; +import { KdfConfig, KdfType } from "@bitwarden/key-management"; @Component({ selector: "app-change-kdf-confirmation", @@ -28,13 +28,12 @@ export class ChangeKdfConfirmationComponent { loading = false; constructor( - private apiService: ApiService, private i18nService: I18nService, - private keyService: KeyService, private messagingService: MessagingService, @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, private accountService: AccountService, private toastService: ToastService, + private changeKdfService: ChangeKdfService, ) { this.kdfConfig = params.kdfConfig; this.masterPassword = null; @@ -56,37 +55,17 @@ export class ChangeKdfConfirmationComponent { }; private async makeKeyAndSaveAsync() { - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - if (activeAccount == null) { - throw new Error("No active account found."); - } const masterPassword = this.form.value.masterPassword; // Ensure the KDF config is valid. this.kdfConfig.validateKdfConfigForSetting(); - const request = new KdfRequest(); - request.kdf = this.kdfConfig.kdfType; - request.kdfIterations = this.kdfConfig.iterations; - if (this.kdfConfig.kdfType === KdfType.Argon2id) { - request.kdfMemory = this.kdfConfig.memory; - request.kdfParallelism = this.kdfConfig.parallelism; - } - const masterKey = await this.keyService.getOrDeriveMasterKey(masterPassword, activeAccount.id); - request.masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey); + const activeAccountId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - const newMasterKey = await this.keyService.makeMasterKey( + await this.changeKdfService.updateUserKdfParams( masterPassword, - activeAccount.email, this.kdfConfig, + activeAccountId, ); - request.newMasterPasswordHash = await this.keyService.hashMasterKey( - masterPassword, - newMasterKey, - ); - const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey); - request.key = newUserKey[1].encryptedString; - - await this.apiService.postAccountKdf(request); } } diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 9a67506c400..f7f611b75ee 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@angular/core"; -import { firstValueFrom, Observable } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -7,6 +7,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; +import { firstValueFromOrThrow } from "@bitwarden/common/key-management/utils"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -103,18 +104,18 @@ export class UserKeyRotationService { } // Read current cryptographic state / settings - const masterKeyKdfConfig: KdfConfig = (await this.firstValueFromOrThrow( + const masterKeyKdfConfig: KdfConfig = (await firstValueFromOrThrow( this.kdfConfigService.getKdfConfig$(user.id), "KDF config", ))!; // The masterkey salt used for deriving the masterkey always needs to be trimmed and lowercased. const masterKeySalt = user.email.trim().toLowerCase(); - const currentUserKey: UserKey = (await this.firstValueFromOrThrow( + const currentUserKey: UserKey = (await firstValueFromOrThrow( this.keyService.userKey$(user.id), "User key", ))!; const currentUserKeyWrappedPrivateKey = new EncString( - (await this.firstValueFromOrThrow( + (await firstValueFromOrThrow( this.keyService.userEncryptedPrivateKey$(user.id), "User encrypted private key", ))!, @@ -515,12 +516,4 @@ export class UserKeyRotationService { HashPurpose.ServerAuthorization, ); } - - async firstValueFromOrThrow(value: Observable, name: string): Promise { - const result = await firstValueFrom(value); - if (result == null) { - throw new Error(`Failed to get ${name}`); - } - return result; - } } diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-reset-password.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-reset-password.request.ts index 7d060e3390e..bd87f6ca5d8 100644 --- a/libs/admin-console/src/common/organization-user/models/requests/organization-user-reset-password.request.ts +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-reset-password.request.ts @@ -1,6 +1,25 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore + +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; + export class OrganizationUserResetPasswordRequest { newMasterPasswordHash: string; key: string; + + // This will eventually be changed to be an actual constructor, once all callers are updated. + // The body of this request will be changed to carry the authentication data and unlock data. + // https://bitwarden.atlassian.net/browse/PM-23234 + static newConstructor( + authenticationData: MasterPasswordAuthenticationData, + unlockData: MasterPasswordUnlockData, + ): OrganizationUserResetPasswordRequest { + const request = new OrganizationUserResetPasswordRequest(); + request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash; + request.key = unlockData.masterKeyWrappedUserKey; + return request; + } } diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts index fbab228d6e0..01e87cae0ed 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -137,8 +137,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi newPasswordHint, orgSsoIdentifier, keysRequest, - kdfConfig.kdfType, - kdfConfig.iterations, + kdfConfig, ); await this.masterPasswordApiService.setPassword(request); diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index d96b4c86fb5..7a1dfc91e67 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -157,8 +157,7 @@ describe("DefaultSetInitialPasswordService", () => { credentials.newPasswordHint, credentials.orgSsoIdentifier, keysRequest, - credentials.kdfConfig.kdfType, - credentials.kdfConfig.iterations, + credentials.kdfConfig, ); enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index fa6c1071e21..ff704394bc3 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -163,6 +163,10 @@ import { EncryptServiceImplementation } from "@bitwarden/common/key-management/c import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation"; +import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service"; +import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction"; +import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service"; +import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service"; import { @@ -1242,6 +1246,16 @@ const safeProviders: SafeProvider[] = [ ConfigService, ], }), + safeProvider({ + provide: ChangeKdfApiService, + useClass: DefaultChangeKdfApiService, + deps: [ApiServiceAbstraction], + }), + safeProvider({ + provide: ChangeKdfService, + useClass: DefaultChangeKdfService, + deps: [MasterPasswordServiceAbstraction, KeyService, KdfConfigService, ChangeKdfApiService], + }), safeProvider({ provide: AuthRequestServiceAbstraction, useClass: AuthRequestService, diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 32b02bf016a..dda471c7129 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -303,6 +303,14 @@ export class InputPasswordComponent implements OnInit { throw new Error("KdfConfig is required to create master key."); } + const salt = + this.userId != null + ? await firstValueFrom(this.masterPasswordService.saltForUser$(this.userId)) + : this.masterPasswordService.emailToSalt(this.email); + if (salt == null) { + throw new Error("Salt is required to create master key."); + } + // 2. Verify current password is correct (if necessary) if ( this.flow === InputPasswordFlow.ChangePassword || @@ -348,6 +356,7 @@ export class InputPasswordComponent implements OnInit { const passwordInputResult: PasswordInputResult = { newPassword, + salt, newMasterKey, newServerMasterKeyHash, newLocalMasterKeyHash, diff --git a/libs/auth/src/angular/input-password/password-input-result.ts b/libs/auth/src/angular/input-password/password-input-result.ts index 37f337291e5..11c8f0d274d 100644 --- a/libs/auth/src/angular/input-password/password-input-result.ts +++ b/libs/auth/src/angular/input-password/password-input-result.ts @@ -1,18 +1,26 @@ +import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { MasterKey } from "@bitwarden/common/types/key"; import { KdfConfig } from "@bitwarden/key-management"; export interface PasswordInputResult { currentPassword?: string; + newPassword: string; + kdfConfig?: KdfConfig; + salt?: MasterPasswordSalt; + newPasswordHint?: string; + rotateUserKey?: boolean; + + /** @deprecated This low-level cryptographic state will be removed. It will be replaced by high level calls to masterpassword service, in the consumers of this interface. */ currentMasterKey?: MasterKey; + /** @deprecated */ currentServerMasterKeyHash?: string; + /** @deprecated */ currentLocalMasterKeyHash?: string; - newPassword: string; - newPasswordHint?: string; + /** @deprecated */ newMasterKey?: MasterKey; + /** @deprecated */ newServerMasterKeyHash?: string; + /** @deprecated */ newLocalMasterKeyHash?: string; - - kdfConfig?: KdfConfig; - rotateUserKey?: boolean; } diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 87b2dda46a5..e2f326d836d 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -38,7 +38,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, @@ -61,7 +61,7 @@ const masterPassword = "password"; const deviceId = Utils.newGuid(); const accessToken = "ACCESS_TOKEN"; const refreshToken = "REFRESH_TOKEN"; -const encryptedUserKey = makeEncString("USER_KEY"); +const encryptedUserKey = "USER_KEY"; const privateKey = "PRIVATE_KEY"; const kdf = 0; const kdfIterations = 10000; @@ -76,7 +76,7 @@ const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerRe KdfType: kdf, Iterations: kdfIterations, }, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }, }; @@ -99,7 +99,7 @@ export function identityTokenResponseFactory( ForcePasswordReset: false, Kdf: kdf, KdfIterations: kdfIterations, - Key: encryptedUserKey.encryptedString, + Key: encryptedUserKey, PrivateKey: privateKey, ResetMasterPassword: false, access_token: accessToken, diff --git a/libs/common/src/auth/models/request/email.request.ts b/libs/common/src/auth/models/request/email.request.ts index 12fdff149cc..10a2385a3e0 100644 --- a/libs/common/src/auth/models/request/email.request.ts +++ b/libs/common/src/auth/models/request/email.request.ts @@ -1,9 +1,27 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; + import { EmailTokenRequest } from "./email-token.request"; export class EmailRequest extends EmailTokenRequest { newMasterPasswordHash: string; token: string; key: string; + + // This will eventually be changed to be an actual constructor, once all callers are updated. + // The body of this request will be changed to carry the authentication data and unlock data. + // https://bitwarden.atlassian.net/browse/PM-23234 + static newConstructor( + authenticationData: MasterPasswordAuthenticationData, + unlockData: MasterPasswordUnlockData, + ): EmailRequest { + const request = new EmailRequest(); + request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash; + request.key = unlockData.masterKeyWrappedUserKey; + return request; + } } diff --git a/libs/common/src/auth/models/request/password.request.ts b/libs/common/src/auth/models/request/password.request.ts index 674754ff41a..e359540140b 100644 --- a/libs/common/src/auth/models/request/password.request.ts +++ b/libs/common/src/auth/models/request/password.request.ts @@ -1,9 +1,31 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; + import { SecretVerificationRequest } from "./secret-verification.request"; export class PasswordRequest extends SecretVerificationRequest { newMasterPasswordHash: string; masterPasswordHint: string; key: string; + + authenticationData?: MasterPasswordAuthenticationData; + unlockData?: MasterPasswordUnlockData; + + // This will eventually be changed to be an actual constructor, once all callers are updated. + // https://bitwarden.atlassian.net/browse/PM-23234 + static newConstructor( + authenticationData: MasterPasswordAuthenticationData, + unlockData: MasterPasswordUnlockData, + ): PasswordRequest { + const request = new PasswordRequest(); + request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash; + request.key = unlockData.masterKeyWrappedUserKey; + request.authenticationData = authenticationData; + request.unlockData = unlockData; + return request; + } } diff --git a/libs/common/src/auth/models/request/secret-verification.request.ts b/libs/common/src/auth/models/request/secret-verification.request.ts index bb5d913656e..0572b11bac5 100644 --- a/libs/common/src/auth/models/request/secret-verification.request.ts +++ b/libs/common/src/auth/models/request/secret-verification.request.ts @@ -1,7 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line + +import { MasterPasswordAuthenticationData } from "@bitwarden/common/key-management/master-password/types/master-password.types"; + // @ts-strict-ignore export class SecretVerificationRequest { masterPasswordHash: string; otp: string; authRequestAccessCode: string; + + /** + * Mutates this request to include the master password authentication data, to authenticate the request. + */ + authenticateWith( + masterPasswordAuthenticationData: MasterPasswordAuthenticationData, + ): SecretVerificationRequest { + this.masterPasswordHash = masterPasswordAuthenticationData.masterPasswordAuthenticationHash; + return this; + } } diff --git a/libs/common/src/auth/models/request/set-password.request.ts b/libs/common/src/auth/models/request/set-password.request.ts index 7206cd98623..b48c060198e 100644 --- a/libs/common/src/auth/models/request/set-password.request.ts +++ b/libs/common/src/auth/models/request/set-password.request.ts @@ -1,6 +1,11 @@ // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. + +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; // eslint-disable-next-line no-restricted-imports -import { KdfType } from "@bitwarden/key-management"; +import { KdfConfig, KdfType } from "@bitwarden/key-management"; import { KeysRequest } from "../../../models/request/keys.request"; @@ -21,19 +26,45 @@ export class SetPasswordRequest { masterPasswordHint: string, orgIdentifier: string, keys: KeysRequest | null, - kdf: KdfType, - kdfIterations: number, - kdfMemory?: number, - kdfParallelism?: number, + kdf: KdfConfig, ) { this.masterPasswordHash = masterPasswordHash; this.key = key; this.masterPasswordHint = masterPasswordHint; - this.kdf = kdf; - this.kdfIterations = kdfIterations; - this.kdfMemory = kdfMemory; - this.kdfParallelism = kdfParallelism; this.orgIdentifier = orgIdentifier; this.keys = keys; + + if (kdf.kdfType === KdfType.PBKDF2_SHA256) { + this.kdf = KdfType.PBKDF2_SHA256; + this.kdfIterations = kdf.iterations; + } else if (kdf.kdfType === KdfType.Argon2id) { + this.kdf = KdfType.Argon2id; + this.kdfIterations = kdf.iterations; + this.kdfMemory = kdf.memory; + this.kdfParallelism = kdf.parallelism; + } else { + throw new Error(`Unsupported KDF type: ${kdf}`); + } + } + + // This will eventually be changed to be an actual constructor, once all callers are updated. + // The body of this request will be changed to carry the authentication data and unlock data. + // https://bitwarden.atlassian.net/browse/PM-23234 + static newConstructor( + authenticationData: MasterPasswordAuthenticationData, + unlockData: MasterPasswordUnlockData, + masterPasswordHint: string, + orgIdentifier: string, + keys: KeysRequest | null, + ): SetPasswordRequest { + const request = new SetPasswordRequest( + authenticationData.masterPasswordAuthenticationHash, + unlockData.masterKeyWrappedUserKey, + masterPasswordHint, + orgIdentifier, + keys, + unlockData.kdf, + ); + return request; } } diff --git a/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.spec.ts b/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.spec.ts index b74648b418b..01f918f403d 100644 --- a/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.spec.ts +++ b/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.spec.ts @@ -1,14 +1,12 @@ // eslint-disable-next-line no-restricted-imports import { KdfType } from "@bitwarden/key-management"; -import { makeEncString } from "../../../../../spec"; - import { UserDecryptionOptionsResponse } from "./user-decryption-options.response"; describe("UserDecryptionOptionsResponse", () => { it("should create response when master password unlock is present", () => { const salt = "test@example.com"; - const encryptedUserKey = makeEncString("testUserKey"); + const encryptedUserKey = "testUserKey"; const response = new UserDecryptionOptionsResponse({ HasMasterPassword: true, @@ -18,7 +16,7 @@ describe("UserDecryptionOptionsResponse", () => { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000, }, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }, }); diff --git a/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts b/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts index 7a3b8762c13..2c26472174a 100644 --- a/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts +++ b/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts @@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; // 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 { KdfType } from "@bitwarden/key-management"; +import { PBKDF2KdfConfig } from "@bitwarden/key-management"; import { PasswordRequest } from "../../models/request/password.request"; import { SetPasswordRequest } from "../../models/request/set-password.request"; @@ -42,8 +42,7 @@ describe("MasterPasswordApiService", () => { publicKey: "publicKey", encryptedPrivateKey: "encryptedPrivateKey", }, - KdfType.PBKDF2_SHA256, - 600_000, + new PBKDF2KdfConfig(600_000), ); // Act diff --git a/libs/common/src/key-management/kdf/change-kdf-api.service.abstraction.ts b/libs/common/src/key-management/kdf/change-kdf-api.service.abstraction.ts new file mode 100644 index 00000000000..04a454a0537 --- /dev/null +++ b/libs/common/src/key-management/kdf/change-kdf-api.service.abstraction.ts @@ -0,0 +1,9 @@ +import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; + +export abstract class ChangeKdfApiService { + /** + * Sends a request to update the user's KDF parameters. + * @param request The KDF request containing authentication data, unlock data, and old authentication data + */ + abstract updateUserKdfParams(request: KdfRequest): Promise; +} diff --git a/libs/common/src/key-management/kdf/change-kdf-api.service.ts b/libs/common/src/key-management/kdf/change-kdf-api.service.ts new file mode 100644 index 00000000000..0420dbbdfb2 --- /dev/null +++ b/libs/common/src/key-management/kdf/change-kdf-api.service.ts @@ -0,0 +1,15 @@ +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; + +import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction"; + +/** + * @internal + */ +export class DefaultChangeKdfApiService implements ChangeKdfApiService { + constructor(private apiService: ApiService) {} + + async updateUserKdfParams(request: KdfRequest): Promise { + return this.apiService.send("POST", "/accounts/kdf", request, true, false); + } +} diff --git a/libs/common/src/key-management/kdf/change-kdf-service.abstraction.ts b/libs/common/src/key-management/kdf/change-kdf-service.abstraction.ts new file mode 100644 index 00000000000..b0eb06f9037 --- /dev/null +++ b/libs/common/src/key-management/kdf/change-kdf-service.abstraction.ts @@ -0,0 +1,20 @@ +import { UserId } from "@bitwarden/common/types/guid"; +// eslint-disable-next-line no-restricted-imports +import { KdfConfig } from "@bitwarden/key-management"; + +export abstract class ChangeKdfService { + /** + * Updates the user's KDF parameters + * @param masterPassword The user's current master password + * @param kdf The new KDF configuration to apply + * @param userId The ID of the user whose KDF parameters are being updated + * @throws If any of the parameters is null + * @throws If the user is locked or logged out + * @throws If the kdf change request fails + */ + abstract updateUserKdfParams( + masterPassword: string, + kdf: KdfConfig, + userId: UserId, + ): Promise; +} diff --git a/libs/common/src/key-management/kdf/change-kdf-service.spec.ts b/libs/common/src/key-management/kdf/change-kdf-service.spec.ts new file mode 100644 index 00000000000..07cba1cc7eb --- /dev/null +++ b/libs/common/src/key-management/kdf/change-kdf-service.spec.ts @@ -0,0 +1,167 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +// eslint-disable-next-line no-restricted-imports +import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; + +import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction"; +import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationHash, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "../master-password/types/master-password.types"; + +import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction"; +import { DefaultChangeKdfService } from "./change-kdf-service"; + +describe("ChangeKdfService", () => { + const changeKdfApiService = mock(); + const masterPasswordService = mock(); + const keyService = mock(); + const kdfConfigService = mock(); + + let sut: DefaultChangeKdfService = mock(); + + const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; + const mockOldKdfConfig = new PBKDF2KdfConfig(100000); + const mockNewKdfConfig = new PBKDF2KdfConfig(200000); + const mockOldHash = "oldHash" as MasterPasswordAuthenticationHash; + const mockNewHash = "newHash" as MasterPasswordAuthenticationHash; + const mockUserId = "00000000-0000-0000-0000-000000000000" as UserId; + const mockSalt = "test@bitwarden.com" as MasterPasswordSalt; + const mockWrappedUserKey = "wrappedUserKey"; + + beforeEach(() => { + sut = new DefaultChangeKdfService( + masterPasswordService, + keyService, + kdfConfigService, + changeKdfApiService, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("updateUserKdfParams", () => { + it("should throw an error if masterPassword is null", async () => { + await expect( + sut.updateUserKdfParams(null as unknown as string, mockNewKdfConfig, mockUserId), + ).rejects.toThrow("masterPassword"); + }); + + it("should throw an error if masterPassword is undefined", async () => { + await expect( + sut.updateUserKdfParams(undefined as unknown as string, mockNewKdfConfig, mockUserId), + ).rejects.toThrow("masterPassword"); + }); + + it("should throw an error if kdf is null", async () => { + await expect( + sut.updateUserKdfParams("masterPassword", null as unknown as PBKDF2KdfConfig, mockUserId), + ).rejects.toThrow("kdf"); + }); + + it("should throw an error if kdf is undefined", async () => { + await expect( + sut.updateUserKdfParams( + "masterPassword", + undefined as unknown as PBKDF2KdfConfig, + mockUserId, + ), + ).rejects.toThrow("kdf"); + }); + + it("should throw an error if userId is null", async () => { + await expect( + sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, null as unknown as UserId), + ).rejects.toThrow("userId"); + }); + + it("should throw an error if userId is undefined", async () => { + await expect( + sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, undefined as unknown as UserId), + ).rejects.toThrow("userId"); + }); + + it("should throw an error if userKey is null", async () => { + keyService.userKey$.mockReturnValueOnce(of(null)); + masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); + kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); + await expect( + sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), + ).rejects.toThrow(); + }); + + it("should throw an error if salt is null", async () => { + keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); + masterPasswordService.saltForUser$.mockReturnValueOnce(of(null)); + kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); + await expect( + sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), + ).rejects.toThrow("Failed to get salt"); + }); + + it("should throw an error if oldKdfConfig is null", async () => { + keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); + masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); + kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(null)); + await expect( + sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), + ).rejects.toThrow("Failed to get oldKdfConfig"); + }); + + it("should call apiService.send with correct parameters", async () => { + keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); + masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); + kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); + + masterPasswordService.makeMasterPasswordAuthenticationData + .mockResolvedValueOnce({ + salt: mockSalt, + kdf: mockOldKdfConfig, + masterPasswordAuthenticationHash: mockOldHash, + }) + .mockResolvedValueOnce({ + salt: mockSalt, + kdf: mockNewKdfConfig, + masterPasswordAuthenticationHash: mockNewHash, + }); + + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValueOnce( + new MasterPasswordUnlockData( + mockSalt, + mockNewKdfConfig, + mockWrappedUserKey as MasterKeyWrappedUserKey, + ), + ); + + await sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId); + + const expected = new KdfRequest( + { + salt: mockSalt, + kdf: mockNewKdfConfig, + masterPasswordAuthenticationHash: mockNewHash, + }, + new MasterPasswordUnlockData( + mockSalt, + mockNewKdfConfig, + mockWrappedUserKey as MasterKeyWrappedUserKey, + ), + ).authenticateWith({ + salt: mockSalt, + kdf: mockOldKdfConfig, + masterPasswordAuthenticationHash: mockOldHash, + }); + + expect(changeKdfApiService.updateUserKdfParams).toHaveBeenCalledWith(expected); + }); + }); +}); diff --git a/libs/common/src/key-management/kdf/change-kdf-service.ts b/libs/common/src/key-management/kdf/change-kdf-service.ts new file mode 100644 index 00000000000..d8bc3e21c1a --- /dev/null +++ b/libs/common/src/key-management/kdf/change-kdf-service.ts @@ -0,0 +1,59 @@ +import { assertNonNullish } from "@bitwarden/common/auth/utils"; +import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; +import { UserId } from "@bitwarden/common/types/guid"; +// eslint-disable-next-line no-restricted-imports +import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management"; + +import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction"; +import { firstValueFromOrThrow } from "../utils"; + +import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction"; +import { ChangeKdfService } from "./change-kdf-service.abstraction"; + +export class DefaultChangeKdfService implements ChangeKdfService { + constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, + private keyService: KeyService, + private kdfConfigService: KdfConfigService, + private changeKdfApiService: ChangeKdfApiService, + ) {} + + async updateUserKdfParams(masterPassword: string, kdf: KdfConfig, userId: UserId): Promise { + assertNonNullish(masterPassword, "masterPassword"); + assertNonNullish(kdf, "kdf"); + assertNonNullish(userId, "userId"); + + const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); + const salt = await firstValueFromOrThrow( + this.masterPasswordService.saltForUser$(userId), + "salt", + ); + const oldKdfConfig = await firstValueFromOrThrow( + this.kdfConfigService.getKdfConfig$(userId), + "oldKdfConfig", + ); + + const oldAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + masterPassword, + oldKdfConfig, + salt, + ); + const authenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + masterPassword, + kdf, + salt, + ); + const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData( + masterPassword, + kdf, + salt, + userKey, + ); + + const request = new KdfRequest(authenticationData, unlockData); + request.authenticateWith(oldAuthenticationData); + await this.changeKdfApiService.updateUserKdfParams(request); + } +} diff --git a/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts b/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts index 002cce662c0..8ef14904bce 100644 --- a/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts +++ b/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts @@ -27,6 +27,11 @@ export abstract class MasterPasswordServiceAbstraction { * @throws If the user ID is provided, but the user is not found. */ abstract saltForUser$: (userId: UserId) => Observable; + /** + * Converts an email to a master password salt. This is a canonical encoding of the + * email, no matter how the email is capitalized. + */ + abstract emailToSalt(email: string): MasterPasswordSalt; /** * An observable that emits the master key for the user. * @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead. diff --git a/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.spec.ts b/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.spec.ts index 3dd1a28cc43..38701f2dc1c 100644 --- a/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.spec.ts +++ b/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.spec.ts @@ -1,13 +1,11 @@ // eslint-disable-next-line no-restricted-imports import { KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { makeEncString } from "../../../../../spec"; - import { MasterPasswordUnlockResponse } from "./master-password-unlock.response"; describe("MasterPasswordUnlockResponse", () => { const salt = "test@example.com"; - const encryptedUserKey = makeEncString("testUserKey"); + const encryptedUserKey = "testUserKey"; const testKdfResponse = { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000 }; it("should throw error when salt is not provided", () => { @@ -15,7 +13,7 @@ describe("MasterPasswordUnlockResponse", () => { new MasterPasswordUnlockResponse({ Salt: undefined, Kdf: testKdfResponse, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }); }).toThrow("MasterPasswordUnlockResponse does not contain a valid salt"); }); @@ -36,7 +34,7 @@ describe("MasterPasswordUnlockResponse", () => { const response = new MasterPasswordUnlockResponse({ Salt: salt, Kdf: testKdfResponse, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }); expect(response.salt).toBe(salt); @@ -50,7 +48,7 @@ describe("MasterPasswordUnlockResponse", () => { const response = new MasterPasswordUnlockResponse({ Salt: salt, Kdf: testKdfResponse, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }); const unlockData = response.toMasterPasswordUnlockData(); diff --git a/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.ts b/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.ts index 294d5f48a73..41b16d43f56 100644 --- a/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.ts +++ b/libs/common/src/key-management/master-password/models/response/master-password-unlock.response.ts @@ -1,5 +1,4 @@ import { BaseResponse } from "../../../../models/response/base.response"; -import { EncString } from "../../../crypto/models/enc-string"; import { KdfConfigResponse } from "../../../models/response/kdf-config.response"; import { MasterKeyWrappedUserKey, @@ -29,9 +28,7 @@ export class MasterPasswordUnlockResponse extends BaseResponse { "MasterPasswordUnlockResponse does not contain a valid master key encrypted user key", ); } - this.masterKeyWrappedUserKey = new EncString( - masterKeyEncryptedUserKey, - ) as MasterKeyWrappedUserKey; + this.masterKeyWrappedUserKey = masterKeyEncryptedUserKey as MasterKeyWrappedUserKey; } toMasterPasswordUnlockData() { diff --git a/libs/common/src/key-management/master-password/services/fake-master-password.service.ts b/libs/common/src/key-management/master-password/services/fake-master-password.service.ts index 246132e2c85..81aea5e480a 100644 --- a/libs/common/src/key-management/master-password/services/fake-master-password.service.ts +++ b/libs/common/src/key-management/master-password/services/fake-master-password.service.ts @@ -33,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA this.masterKeyHashSubject.next(initialMasterKeyHash); } + emailToSalt(email: string): MasterPasswordSalt { + return this.mock.emailToSalt(email); + } + saltForUser$(userId: UserId): Observable { return this.mock.saltForUser$(userId); } diff --git a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts index 8fd3f0b85c2..02b4e9a895a 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts @@ -10,7 +10,6 @@ import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden import { FakeAccountService, - makeEncString, makeSymmetricCryptoKey, mockAccountServiceWith, } from "../../../../spec"; @@ -385,7 +384,7 @@ describe("MasterPasswordService", () => { const kdfPBKDF2: KdfConfig = new PBKDF2KdfConfig(600_000); const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3); const salt = "test@bitwarden.com" as MasterPasswordSalt; - const encryptedUserKey = makeEncString("testUserKet") as MasterKeyWrappedUserKey; + const encryptedUserKey = "testUserKet" as MasterKeyWrappedUserKey; it("returns null when value is null", () => { const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer( @@ -401,7 +400,7 @@ describe("MasterPasswordService", () => { kdfType: KdfType.PBKDF2_SHA256, iterations: kdfPBKDF2.iterations, }, - masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string, + masterKeyWrappedUserKey: encryptedUserKey as string, }; const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data); @@ -419,7 +418,7 @@ describe("MasterPasswordService", () => { memory: kdfArgon2.memory, parallelism: kdfArgon2.parallelism, }, - masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string, + masterKeyWrappedUserKey: encryptedUserKey as string, }; const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data); diff --git a/libs/common/src/key-management/master-password/services/master-password.service.ts b/libs/common/src/key-management/master-password/services/master-password.service.ts index 94465bd24fd..9f7e054d64c 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.ts @@ -132,7 +132,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr return EncString.fromJSON(key); } - private emailToSalt(email: string): MasterPasswordSalt { + emailToSalt(email: string): MasterPasswordSalt { return email.toLowerCase().trim() as MasterPasswordSalt; } @@ -256,6 +256,9 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr assertNonNullish(password, "password"); assertNonNullish(kdf, "kdf"); assertNonNullish(salt, "salt"); + if (password === "") { + throw new Error("Master password cannot be empty."); + } // We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly. salt = salt.toLowerCase().trim() as MasterPasswordSalt; @@ -294,18 +297,19 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr assertNonNullish(kdf, "kdf"); assertNonNullish(salt, "salt"); assertNonNullish(userKey, "userKey"); + if (password === "") { + throw new Error("Master password cannot be empty."); + } // We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly. salt = salt.toLowerCase().trim() as MasterPasswordSalt; await SdkLoadService.Ready; - const masterKeyWrappedUserKey = new EncString( - PureCrypto.encrypt_user_key_with_master_password( - userKey.toEncoded(), - password, - salt, - kdf.toSdkConfig(), - ), + const masterKeyWrappedUserKey = PureCrypto.encrypt_user_key_with_master_password( + userKey.toEncoded(), + password, + salt, + kdf.toSdkConfig(), ) as MasterKeyWrappedUserKey; return new MasterPasswordUnlockData(salt, kdf, masterKeyWrappedUserKey); } @@ -320,7 +324,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr await SdkLoadService.Ready; const userKey = new SymmetricCryptoKey( PureCrypto.decrypt_user_key_with_master_password( - masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString, + masterPasswordUnlockData.masterKeyWrappedUserKey, password, masterPasswordUnlockData.salt, masterPasswordUnlockData.kdf.toSdkConfig(), diff --git a/libs/common/src/key-management/master-password/types/master-password.types.ts b/libs/common/src/key-management/master-password/types/master-password.types.ts index f71bb0c317c..416b296623f 100644 --- a/libs/common/src/key-management/master-password/types/master-password.types.ts +++ b/libs/common/src/key-management/master-password/types/master-password.types.ts @@ -2,8 +2,7 @@ import { Jsonify, Opaque } from "type-fest"; // eslint-disable-next-line no-restricted-imports import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management"; - -import { EncString } from "../../crypto/models/enc-string"; +import { EncString } from "@bitwarden/sdk-internal"; /** * The Base64-encoded master password authentication hash, that is sent to the server for authentication. @@ -13,7 +12,7 @@ export type MasterPasswordAuthenticationHash = Opaque; -export type MasterKeyWrappedUserKey = Opaque; +export type MasterKeyWrappedUserKey = Opaque; /** * The data required to unlock with the master password. @@ -29,7 +28,7 @@ export class MasterPasswordUnlockData { return { salt: this.salt, kdf: this.kdf, - masterKeyWrappedUserKey: this.masterKeyWrappedUserKey.toJSON(), + masterKeyWrappedUserKey: this.masterKeyWrappedUserKey, }; } @@ -43,7 +42,7 @@ export class MasterPasswordUnlockData { obj.kdf.kdfType === KdfType.PBKDF2_SHA256 ? PBKDF2KdfConfig.fromJSON(obj.kdf) : Argon2KdfConfig.fromJSON(obj.kdf), - EncString.fromJSON(obj.masterKeyWrappedUserKey) as MasterKeyWrappedUserKey, + obj.masterKeyWrappedUserKey as MasterKeyWrappedUserKey, ); } } diff --git a/libs/common/src/key-management/models/response/user-decryption.response.spec.ts b/libs/common/src/key-management/models/response/user-decryption.response.spec.ts index 20596a75ef2..06c19829900 100644 --- a/libs/common/src/key-management/models/response/user-decryption.response.spec.ts +++ b/libs/common/src/key-management/models/response/user-decryption.response.spec.ts @@ -1,14 +1,12 @@ // eslint-disable-next-line no-restricted-imports import { KdfType } from "@bitwarden/key-management"; -import { makeEncString } from "../../../../spec"; - import { UserDecryptionResponse } from "./user-decryption.response"; describe("UserDecryptionResponse", () => { it("should create response when masterPasswordUnlock provided", () => { const salt = "test@example.com"; - const encryptedUserKey = makeEncString("testUserKey"); + const encryptedUserKey = "testUserKey"; const kdfIterations = 600_000; const response = { @@ -18,7 +16,7 @@ describe("UserDecryptionResponse", () => { KdfType: KdfType.PBKDF2_SHA256 as number, Iterations: kdfIterations, }, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }, }; diff --git a/libs/common/src/key-management/utils.ts b/libs/common/src/key-management/utils.ts new file mode 100644 index 00000000000..d0d8854df58 --- /dev/null +++ b/libs/common/src/key-management/utils.ts @@ -0,0 +1,12 @@ +import { firstValueFrom, Observable } from "rxjs"; + +export async function firstValueFromOrThrow( + value: Observable, + name: string, +): Promise { + const result = await firstValueFrom(value); + if (result == null) { + throw new Error(`Failed to get ${name}`); + } + return result; +} diff --git a/libs/common/src/models/request/kdf.request.ts b/libs/common/src/models/request/kdf.request.ts index 7ffdbcb4a4b..e49d8ffe6b8 100644 --- a/libs/common/src/models/request/kdf.request.ts +++ b/libs/common/src/models/request/kdf.request.ts @@ -1,14 +1,20 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -// 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 { KdfType } from "@bitwarden/key-management"; +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { PasswordRequest } from "../../auth/models/request/password.request"; export class KdfRequest extends PasswordRequest { - kdf: KdfType; - kdfIterations: number; - kdfMemory?: number; - kdfParallelism?: number; + constructor( + authenticationData: MasterPasswordAuthenticationData, + unlockData: MasterPasswordUnlockData, + ) { + super(); + // Note, this init code should be in the super constructor, once PasswordRequest's constructor is updated. + this.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash; + this.key = unlockData.masterKeyWrappedUserKey; + this.authenticationData = authenticationData; + this.unlockData = unlockData; + } } diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index 4fcafd885e4..3c3e1c3677f 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -15,7 +15,6 @@ import { // eslint-disable-next-line no-restricted-imports import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { makeEncString } from "../../../spec"; import { Matrix } from "../../../spec/matrix"; import { ApiService } from "../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; @@ -247,7 +246,7 @@ describe("DefaultSyncService", () => { describe("syncUserDecryption", () => { const salt = "test@example.com"; const kdf = new PBKDF2KdfConfig(600_000); - const encryptedUserKey = makeEncString("testUserKey"); + const encryptedUserKey = "testUserKey"; it("should set master password unlock when present in user decryption", async () => { const syncResponse = new SyncResponse({ @@ -261,7 +260,7 @@ describe("DefaultSyncService", () => { KdfType: kdf.kdfType, Iterations: kdf.iterations, }, - MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString, + MasterKeyEncryptedUserKey: encryptedUserKey, }, }, });