From a7d3c0f5c208159f175b5adb0f1c6aef5e7b20da Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 29 Jul 2025 18:53:17 +0200 Subject: [PATCH] [PM-23809] Add simplified interface to MP service (#15631) * Add new mp service api * Fix tests * Add test coverage * Add newline * Fix type * Rename to "unwrapUserKeyFromMasterPasswordUnlockData" * Fix build * Fix build on cli * Fix linting * Re-sort spec * Add tests * Fix test and build issues * Fix build * Clean up * Remove introduced function * Clean up comments * Fix abstract class types * Fix comments * Cleanup * Cleanup * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/services/master-password.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add comments * Fix build * Add arg null check * Cleanup * Fix build * Fix build on browser * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for null params * Cleanup and deprecate more functions * Fix formatting * Prettier * Clean up * Update libs/key-management/src/abstractions/key.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Make emailToSalt private and expose abstract saltForUser * Add tests * Add docs * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../browser/src/background/main.background.ts | 2 + .../service-container/service-container.ts | 5 +- .../src/services/jslib-services.module.ts | 2 + .../master-password.service.abstraction.ts | 61 ++++++++ .../services/fake-master-password.service.ts | 37 +++++ .../services/master-password.service.spec.ts | 139 +++++++++++++++++- .../services/master-password.service.ts | 115 +++++++++++++++ .../types/master-password.types.ts | 34 +++++ libs/common/src/types/key.ts | 1 + .../src/abstractions/key.service.ts | 12 ++ libs/key-management/src/key.service.ts | 16 ++ 11 files changed, 418 insertions(+), 6 deletions(-) create mode 100644 libs/common/src/key-management/master-password/types/master-password.types.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3aaf24c07a..5c1d233575 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -668,6 +668,8 @@ export default class MainBackground { this.keyGenerationService, this.encryptService, this.logService, + this.cryptoFunctionService, + this.accountService, ); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index ddab6fb7cf..820041b46d 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -431,16 +431,17 @@ export class ServiceContainer { migrationRunner, ); + this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); this.masterPasswordService = new MasterPasswordService( this.stateProvider, this.stateService, this.keyGenerationService, this.encryptService, this.logService, + this.cryptoFunctionService, + this.accountService, ); - this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); - this.pinService = new PinService( this.accountService, this.cryptoFunctionService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index aaabc405da..f619588dc9 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1021,6 +1021,8 @@ const safeProviders: SafeProvider[] = [ KeyGenerationServiceAbstraction, EncryptService, LogService, + CryptoFunctionServiceAbstraction, + AccountServiceAbstraction, ], }), safeProvider({ 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 641e969c5a..e8346b67b5 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 @@ -1,9 +1,17 @@ import { Observable } from "rxjs"; +// eslint-disable-next-line no-restricted-imports +import { KdfConfig } from "@bitwarden/key-management"; + import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { UserId } from "../../../types/guid"; import { MasterKey, UserKey } from "../../../types/key"; import { EncString } from "../../crypto/models/enc-string"; +import { + MasterPasswordAuthenticationData, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "../types/master-password.types"; export abstract class MasterPasswordServiceAbstraction { /** @@ -12,14 +20,23 @@ export abstract class MasterPasswordServiceAbstraction { * @throws If the user ID is missing. */ abstract forceSetPasswordReason$: (userId: UserId) => Observable; + /** + * An observable that emits the master password salt for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + * @throws If the user ID is provided, but the user is not found. + */ + abstract saltForUser$: (userId: UserId) => Observable; /** * 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. * @param userId The user ID. * @throws If the user ID is missing. */ abstract masterKey$: (userId: UserId) => Observable; /** * An observable that emits the master key hash for the user. + * @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordAuthenticationData}. * @param userId The user ID. * @throws If the user ID is missing. */ @@ -32,6 +49,7 @@ export abstract class MasterPasswordServiceAbstraction { abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise; /** * Decrypts the user key with the provided master key + * @deprecated Interacting with the master-key directly is deprecated. Please use {@link unwrapUserKeyFromMasterPasswordUnlockData} instead. * @param masterKey The user's master key * * @param userId The desired user * @param userKey The user's encrypted symmetric key @@ -44,12 +62,52 @@ export abstract class MasterPasswordServiceAbstraction { userId: string, userKey?: EncString, ) => Promise; + + /** + * Makes the authentication hash for authenticating to the server with the master password. + * @param password The master password. + * @param kdf The KDF configuration. + * @param salt The master password salt to use. See {@link saltForUser$} for current salt. + * @throws If password, KDF or salt are null or undefined. + */ + abstract makeMasterPasswordAuthenticationData: ( + password: string, + kdf: KdfConfig, + salt: MasterPasswordSalt, + ) => Promise; + + /** + * Creates a MasterPasswordUnlockData bundle that encrypts the user-key with a key derived from the password. The + * bundle also contains the KDF settings and salt used to derive the key, which are required to decrypt the user-key later. + * @param password The master password. + * @param kdf The KDF configuration. + * @param salt The master password salt to use. See {@link saltForUser$} for current salt. + * @param userKey The user's userKey to encrypt. + * @throws If password, KDF, salt, or userKey are null or undefined. + */ + abstract makeMasterPasswordUnlockData: ( + password: string, + kdf: KdfConfig, + salt: MasterPasswordSalt, + userKey: UserKey, + ) => Promise; + + /** + * Unwraps a user-key that was wrapped with a password provided KDF settings. The same KDF settings and salt must be provided to unwrap the user-key, otherwise it will fail to decrypt. + * @throws If the encryption type is not supported. + * @throws If the password, KDF, or salt don't match the original wrapping parameters. + */ + abstract unwrapUserKeyFromMasterPasswordUnlockData: ( + password: string, + masterPasswordUnlockData: MasterPasswordUnlockData, + ) => Promise; } export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { /** * Set the master key for the user. * Note: Use {@link clearMasterKey} to clear the master key. + * @deprecated Interacting with the master-key directly is deprecated. * @param masterKey The master key. * @param userId The user ID. * @throws If the user ID or master key is missing. @@ -57,6 +115,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise; /** * Clear the master key for the user. + * @deprecated Interacting with the master-key directly is deprecated. * @param userId The user ID. * @throws If the user ID is missing. */ @@ -64,6 +123,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas /** * Set the master key hash for the user. * Note: Use {@link clearMasterKeyHash} to clear the master key hash. + * @deprecated Interacting with the master-key directly is deprecated. * @param masterKeyHash The master key hash. * @param userId The user ID. * @throws If the user ID or master key hash is missing. @@ -71,6 +131,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise; /** * Clear the master key hash for the user. + * @deprecated Interacting with the master-key directly is deprecated. * @param userId The user ID. * @throws If the user ID is missing. */ 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 8ae3a4265c..465a0c0403 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 @@ -3,11 +3,20 @@ import { mock } from "jest-mock-extended"; import { ReplaySubject, Observable } from "rxjs"; +// FIXME: Update this file to be type safe and remove this and next line +// eslint-disable-next-line no-restricted-imports +import { KdfConfig } from "@bitwarden/key-management"; + import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { UserId } from "../../../types/guid"; import { MasterKey, UserKey } from "../../../types/key"; import { EncString } from "../../crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; +import { + MasterPasswordAuthenticationData, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "../types/master-password.types"; export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction { mock = mock(); @@ -24,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA this.masterKeyHashSubject.next(initialMasterKeyHash); } + saltForUser$(userId: UserId): Observable { + return this.mock.saltForUser$(userId); + } + masterKey$(userId: UserId): Observable { return this.masterKeySubject.asObservable(); } @@ -71,4 +84,28 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA ): Promise { return this.mock.decryptUserKeyWithMasterKey(masterKey, userId, userKey); } + + makeMasterPasswordAuthenticationData( + password: string, + kdf: KdfConfig, + salt: MasterPasswordSalt, + ): Promise { + return this.mock.makeMasterPasswordAuthenticationData(password, kdf, salt); + } + + makeMasterPasswordUnlockData( + password: string, + kdf: KdfConfig, + salt: MasterPasswordSalt, + userKey: UserKey, + ): Promise { + return this.mock.makeMasterPasswordUnlockData(password, kdf, salt, userKey); + } + + unwrapUserKeyFromMasterPasswordUnlockData( + password: string, + masterPasswordUnlockData: MasterPasswordUnlockData, + ): Promise { + return this.mock.unwrapUserKeyFromMasterPasswordUnlockData(password, masterPasswordUnlockData); + } } 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 5486ed68e9..a09de9008d 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 @@ -1,8 +1,17 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { of } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; import * as rxjs from "rxjs"; -import { makeSymmetricCryptoKey } from "../../../../spec"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +// eslint-disable-next-line no-restricted-imports +import { KdfConfig, PBKDF2KdfConfig } from "@bitwarden/key-management"; + +import { + FakeAccountService, + makeSymmetricCryptoKey, + mockAccountServiceWith, +} from "../../../../spec"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { LogService } from "../../../platform/abstractions/log.service"; @@ -10,9 +19,11 @@ import { StateService } from "../../../platform/abstractions/state.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; -import { MasterKey } from "../../../types/key"; +import { MasterKey, UserKey } from "../../../types/key"; +import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service"; import { EncryptService } from "../../crypto/abstractions/encrypt.service"; import { EncString } from "../../crypto/models/enc-string"; +import { MasterPasswordSalt } from "../types/master-password.types"; import { MasterPasswordService } from "./master-password.service"; @@ -24,8 +35,10 @@ describe("MasterPasswordService", () => { let keyGenerationService: MockProxy; let encryptService: MockProxy; let logService: MockProxy; + let cryptoFunctionService: MockProxy; + let accountService: FakeAccountService; - const userId = "user-id" as UserId; + const userId = "00000000-0000-0000-0000-000000000000" as UserId; const mockUserState = { state$: of(null), update: jest.fn().mockResolvedValue(null), @@ -45,6 +58,8 @@ describe("MasterPasswordService", () => { keyGenerationService = mock(); encryptService = mock(); logService = mock(); + cryptoFunctionService = mock(); + accountService = mockAccountServiceWith(userId); stateProvider.getUser.mockReturnValue(mockUserState as any); @@ -56,10 +71,33 @@ describe("MasterPasswordService", () => { keyGenerationService, encryptService, logService, + cryptoFunctionService, + accountService, ); encryptService.unwrapSymmetricKey.mockResolvedValue(makeSymmetricCryptoKey(64, 1)); keyGenerationService.stretchKey.mockResolvedValue(makeSymmetricCryptoKey(64, 3)); + Object.defineProperty(SdkLoadService, "Ready", { + value: Promise.resolve(), + configurable: true, + }); + }); + + describe("saltForUser$", () => { + it("throws when userid not present", async () => { + expect(() => { + sut.saltForUser$(null as unknown as UserId); + }).toThrow("userId is null or undefined."); + }); + it("throws when userid present but not in account service", async () => { + await expect( + firstValueFrom(sut.saltForUser$("00000000-0000-0000-0000-000000000001" as UserId)), + ).rejects.toThrow("Cannot read properties of undefined (reading 'email')"); + }); + it("returns salt", async () => { + const salt = await firstValueFrom(sut.saltForUser$(userId)); + expect(salt).toBeDefined(); + }); }); describe("setForceSetPasswordReason", () => { @@ -190,4 +228,97 @@ describe("MasterPasswordService", () => { expect(updateFn(null)).toEqual(encryptedKey.toJSON()); }); }); + + describe("makeMasterPasswordAuthenticationData", () => { + const password = "test-password"; + const kdf: KdfConfig = new PBKDF2KdfConfig(600_000); + const salt = "test@bitwarden.com" as MasterPasswordSalt; + const masterKey = makeSymmetricCryptoKey(32, 2); + const masterKeyHash = makeSymmetricCryptoKey(32, 3).toEncoded(); + + beforeEach(() => { + keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey); + cryptoFunctionService.pbkdf2.mockResolvedValue(masterKeyHash); + }); + + it("derives master key and creates authentication hash", async () => { + const result = await sut.makeMasterPasswordAuthenticationData(password, kdf, salt); + + expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(password, salt, kdf); + expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith( + masterKey.toEncoded(), + password, + "sha256", + 1, + ); + + expect(result).toEqual({ + kdf, + salt, + masterPasswordAuthenticationHash: Utils.fromBufferToB64(masterKeyHash), + }); + }); + + it("throws if password is null", async () => { + await expect( + sut.makeMasterPasswordAuthenticationData(null as unknown as string, kdf, salt), + ).rejects.toThrow(); + }); + it("throws if kdf is null", async () => { + await expect( + sut.makeMasterPasswordAuthenticationData(password, null as unknown as KdfConfig, salt), + ).rejects.toThrow(); + }); + it("throws if salt is null", async () => { + await expect( + sut.makeMasterPasswordAuthenticationData( + password, + kdf, + null as unknown as MasterPasswordSalt, + ), + ).rejects.toThrow(); + }); + }); + + describe("wrapUnwrapUserKeyWithPassword", () => { + const password = "test-password"; + const kdf: KdfConfig = new PBKDF2KdfConfig(600_000); + const salt = "test@bitwarden.com" as MasterPasswordSalt; + const userKey = makeSymmetricCryptoKey(64, 2) as UserKey; + + it("wraps and unwraps user key with password", async () => { + const unlockData = await sut.makeMasterPasswordUnlockData(password, kdf, salt, userKey); + const unwrappedUserkey = await sut.unwrapUserKeyFromMasterPasswordUnlockData( + password, + unlockData, + ); + expect(unwrappedUserkey).toEqual(userKey); + }); + + it("throws if password is null", async () => { + await expect( + sut.makeMasterPasswordUnlockData(null as unknown as string, kdf, salt, userKey), + ).rejects.toThrow(); + }); + it("throws if kdf is null", async () => { + await expect( + sut.makeMasterPasswordUnlockData(password, null as unknown as KdfConfig, salt, userKey), + ).rejects.toThrow(); + }); + it("throws if salt is null", async () => { + await expect( + sut.makeMasterPasswordUnlockData( + password, + kdf, + null as unknown as MasterPasswordSalt, + userKey, + ), + ).rejects.toThrow(); + }); + it("throws if userKey is null", async () => { + await expect( + sut.makeMasterPasswordUnlockData(password, kdf, salt, null as unknown as UserKey), + ).rejects.toThrow(); + }); + }); }); 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 bd8aa6eb22..75e5032e00 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 @@ -2,6 +2,14 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +// eslint-disable-next-line no-restricted-imports +import { KdfConfig } from "@bitwarden/key-management"; +import { PureCrypto } from "@bitwarden/sdk-internal"; + import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { LogService } from "../../../platform/abstractions/log.service"; @@ -16,9 +24,17 @@ import { } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { MasterKey, UserKey } from "../../../types/key"; +import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service"; import { EncryptService } from "../../crypto/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; +import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "../types/master-password.types"; /** Memory since master key shouldn't be available on lock */ const MASTER_KEY = new UserKeyDefinition(MASTER_PASSWORD_MEMORY, "masterKey", { @@ -59,8 +75,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr private keyGenerationService: KeyGenerationService, private encryptService: EncryptService, private logService: LogService, + private cryptoFunctionService: CryptoFunctionService, + private accountService: AccountService, ) {} + saltForUser$(userId: UserId): Observable { + assertNonNullish(userId, "userId"); + return this.accountService.accounts$.pipe( + map((accounts) => accounts[userId].email), + map((email) => this.emailToSalt(email)), + ); + } + masterKey$(userId: UserId): Observable { if (userId == null) { throw new Error("User ID is required."); @@ -95,6 +121,10 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr return EncString.fromJSON(key); } + private emailToSalt(email: string): MasterPasswordSalt { + return email.toLowerCase().trim() as MasterPasswordSalt; + } + async setMasterKey(masterKey: MasterKey, userId: UserId): Promise { if (masterKey == null) { throw new Error("Master key is required."); @@ -202,4 +232,89 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr return decUserKey as UserKey; } + + async makeMasterPasswordAuthenticationData( + password: string, + kdf: KdfConfig, + salt: MasterPasswordSalt, + ): Promise { + assertNonNullish(password, "password"); + assertNonNullish(kdf, "kdf"); + assertNonNullish(salt, "salt"); + + // We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly. + salt = salt.toLowerCase().trim() as MasterPasswordSalt; + + const SERVER_AUTHENTICATION_HASH_ITERATIONS = 1; + + const masterKey = (await this.keyGenerationService.deriveKeyFromPassword( + password, + salt, + kdf, + )) as MasterKey; + + const masterPasswordAuthenticationHash = Utils.fromBufferToB64( + await this.cryptoFunctionService.pbkdf2( + masterKey.toEncoded(), + password, + "sha256", + SERVER_AUTHENTICATION_HASH_ITERATIONS, + ), + ) as MasterPasswordAuthenticationHash; + + return { + salt, + kdf, + masterPasswordAuthenticationHash, + } as MasterPasswordAuthenticationData; + } + + async makeMasterPasswordUnlockData( + password: string, + kdf: KdfConfig, + salt: MasterPasswordSalt, + userKey: UserKey, + ): Promise { + assertNonNullish(password, "password"); + assertNonNullish(kdf, "kdf"); + assertNonNullish(salt, "salt"); + assertNonNullish(userKey, "userKey"); + + // 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(), + ), + ) as MasterKeyWrappedUserKey; + return { + salt, + kdf, + masterKeyWrappedUserKey, + }; + } + + async unwrapUserKeyFromMasterPasswordUnlockData( + password: string, + masterPasswordUnlockData: MasterPasswordUnlockData, + ): Promise { + assertNonNullish(password, "password"); + assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData"); + + await SdkLoadService.Ready; + const userKey = new SymmetricCryptoKey( + PureCrypto.decrypt_user_key_with_master_password( + masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString, + password, + masterPasswordUnlockData.salt, + masterPasswordUnlockData.kdf.toSdkConfig(), + ), + ); + return userKey as UserKey; + } } 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 new file mode 100644 index 0000000000..76451ed087 --- /dev/null +++ b/libs/common/src/key-management/master-password/types/master-password.types.ts @@ -0,0 +1,34 @@ +import { Opaque } from "type-fest"; + +// eslint-disable-next-line no-restricted-imports +import { KdfConfig } from "@bitwarden/key-management"; + +import { EncString } from "../../crypto/models/enc-string"; + +/** + * The Base64-encoded master password authentication hash, that is sent to the server for authentication. + */ +export type MasterPasswordAuthenticationHash = Opaque; +/** + * You MUST obtain this through the emailToSalt function in MasterPasswordService + */ +export type MasterPasswordSalt = Opaque; +export type MasterKeyWrappedUserKey = Opaque; + +/** + * The data required to unlock with the master password. + */ +export type MasterPasswordUnlockData = { + salt: MasterPasswordSalt; + kdf: KdfConfig; + masterKeyWrappedUserKey: MasterKeyWrappedUserKey; +}; + +/** + * The data required to authenticate with the master password. + */ +export type MasterPasswordAuthenticationData = { + salt: MasterPasswordSalt; + kdf: KdfConfig; + masterPasswordAuthenticationHash: MasterPasswordAuthenticationHash; +}; diff --git a/libs/common/src/types/key.ts b/libs/common/src/types/key.ts index c9fd697596..8984452e70 100644 --- a/libs/common/src/types/key.ts +++ b/libs/common/src/types/key.ts @@ -6,6 +6,7 @@ import { SymmetricCryptoKey } from "../platform/models/domain/symmetric-crypto-k export type DeviceKey = Opaque; export type PrfKey = Opaque; export type UserKey = Opaque; +/** @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. */ export type MasterKey = Opaque; export type PinKey = Opaque; export type OrgKey = Opaque; diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index 3e2fbf6c63..12d0998a86 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -150,11 +150,18 @@ export abstract class KeyService { /** * Generates a new user key + * @deprecated Interacting with the master key directly is prohibited. Use {@link makeUserKeyV1} instead. * @throws Error when master key is null and there is no active user * @param masterKey The user's master key. When null, grabs master key from active user. * @returns A new user key and the master key protected version of it */ abstract makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]>; + /** + * Generates a new user key for a V1 user + * Note: This will be replaced by a higher level function to initialize a whole users cryptographic state in the near future. + * @returns A new user key + */ + abstract makeUserKeyV1(): Promise; /** * Clears the user's stored version of the user key * @param keySuffix The desired version of the key to clear @@ -166,6 +173,7 @@ export abstract class KeyService { * Retrieves the user's master key if it is in state, or derives it from the provided password * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user + * @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. * @throws Error when userId is null/undefined. * @throws Error when email or Kdf configuration cannot be found for the user. * @returns The user's master key if it exists, or a newly derived master key. @@ -173,6 +181,7 @@ export abstract class KeyService { abstract getOrDeriveMasterKey(password: string, userId: UserId): Promise; /** * Generates a master key from the provided password + * @deprecated Interacting with the master key directly is prohibited. * @param password The user's master password * @param email The user's email * @param KdfConfig The user's key derivation function configuration @@ -182,6 +191,7 @@ export abstract class KeyService { /** * Encrypts the existing (or provided) user key with the * provided master key + * @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. * @param masterKey The user's master key * @param userKey The user key * @returns The user key and the master key protected version of it @@ -194,6 +204,7 @@ export abstract class KeyService { * Creates a master password hash from the user's master password. Can * be used for local authentication or for server authentication depending * on the hashPurpose provided. + * @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. * @param password The user's master password * @param key The user's master key or active's user master key. * @param hashPurpose The iterations to use for the hash. Defaults to {@link HashPurpose.ServerAuthorization}. @@ -207,6 +218,7 @@ export abstract class KeyService { ): Promise; /** * Compares the provided master password to the stored password hash. + * @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. * @param masterPassword The user's master password * @param masterKey The user's master key * @param userId The id of the user to do the operation for. diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 7cdc104c36..d60aec8c8a 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -232,6 +232,11 @@ export class DefaultKeyService implements KeyServiceAbstraction { return this.buildProtectedSymmetricKey(masterKey, newUserKey); } + async makeUserKeyV1(): Promise { + const newUserKey = await this.keyGenerationService.createKey(512); + return newUserKey as UserKey; + } + /** * Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key * @param userId The desired user @@ -259,6 +264,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { } } + /** + * @deprecated Please use `makeMasterPasswordAuthenticationData`, `unwrapUserKeyFromMasterPasswordUnlockData` or `makeMasterPasswordUnlockData` in @link MasterPasswordService instead. + */ async getOrDeriveMasterKey(password: string, userId: UserId): Promise { if (userId == null) { throw new Error("User ID is required."); @@ -287,6 +295,8 @@ export class DefaultKeyService implements KeyServiceAbstraction { /** * Derive a master key from a password and email. * + * @deprecated Please use `makeMasterPasswordAuthenticationData`, `makeMasterPasswordAuthenticationData`, `unwrapUserKeyFromMasterPasswordUnlockData` in @link MasterPasswordService instead. + * * @remarks * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. */ @@ -304,6 +314,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { return masterKey; } + /** + * @deprecated Please use `makeMasterPasswordUnlockData` in {@link MasterPasswordService} instead. + */ async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -312,6 +325,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { return await this.buildProtectedSymmetricKey(masterKey, userKey); } + /** + * @deprecated Please use `makeMasterPasswordAuthenticationData` in {@link MasterPasswordService} instead. + */ async hashMasterKey( password: string, key: MasterKey,