diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8f416e09511..d2b5cfaad51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -92,6 +92,7 @@ libs/angular/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev libs/billing @bitwarden/team-billing-dev libs/pricing @bitwarden/team-billing-dev +libs/subscription @bitwarden/team-billing-dev bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev ## Platform team files ## @@ -191,6 +192,7 @@ libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev libs/user-crypto-management @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev +libs/unlock @bitwarden/team-key-management-dev # Node-cryptofunction service libs/node @bitwarden/team-key-management-dev @@ -247,4 +249,3 @@ apps/desktop/native-messaging-test-runner/package-lock.json @bitwarden/team-plat .claude/ @bitwarden/team-ai-sme .github/workflows/respond.yml @bitwarden/team-ai-sme .github/workflows/review-code.yml @bitwarden/team-ai-sme -libs/subscription @bitwarden/team-billing-dev diff --git a/jest.config.js b/jest.config.js index 5ea699febff..b0e4a9f6b13 100644 --- a/jest.config.js +++ b/jest.config.js @@ -61,6 +61,7 @@ module.exports = { "/libs/vault/jest.config.js", "/libs/auto-confirm/jest.config.js", "/libs/subscription/jest.config.js", + "/libs/unlock/jest.config.js", "/libs/user-crypto-management/jest.config.js", ], diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 964df445f17..fea65242346 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -389,6 +389,7 @@ import { DefaultStateService, } from "@bitwarden/state-internal"; import { SafeInjectionToken } from "@bitwarden/ui-common"; +import { DefaultUnlockService, UnlockService } from "@bitwarden/unlock"; // 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 { PasswordRepromptService } from "@bitwarden/vault"; @@ -919,6 +920,22 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultAccountCryptographicStateService, deps: [StateProvider], }), + safeProvider({ + provide: UnlockService, + useClass: DefaultUnlockService, + deps: [ + RegisterSdkService, + AccountCryptographicStateService, + PinStateServiceAbstraction, + KdfConfigService, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + CryptoFunctionServiceAbstraction, + StateProvider, + LogService, + BiometricsService, + ], + }), safeProvider({ provide: BroadcasterService, useClass: DefaultBroadcasterService, 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 f1a074ff14c..ae5cae7b819 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 @@ -45,10 +45,14 @@ export const MASTER_KEY = new UserKeyDefinition(MASTER_PASSWORD_MEMOR }); /** Disk since master key hash is used for unlock */ -const MASTER_KEY_HASH = new UserKeyDefinition(MASTER_PASSWORD_DISK, "masterKeyHash", { - deserializer: (masterKeyHash) => masterKeyHash, - clearOn: ["logout"], -}); +export const MASTER_KEY_HASH = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "masterKeyHash", + { + deserializer: (masterKeyHash) => masterKeyHash, + clearOn: ["logout"], + }, +); /** Disk to persist through lock */ export const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition( 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 5ba22905140..ff247ba4ae2 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 @@ -45,6 +45,14 @@ export class MasterPasswordUnlockData { ); } + toSdk(): SdkMasterPasswordUnlockData { + return { + salt: this.salt, + kdf: this.kdf.toSdkConfig(), + masterKeyWrappedUserKey: this.masterKeyWrappedUserKey, + }; + } + toJSON(): any { return { salt: this.salt, diff --git a/libs/unlock/README.md b/libs/unlock/README.md new file mode 100644 index 00000000000..1b0338db337 --- /dev/null +++ b/libs/unlock/README.md @@ -0,0 +1,5 @@ +# Unlock + +Owned by: key-management + +Unlock the account of a user using any of their unlock methods. diff --git a/libs/unlock/eslint.config.mjs b/libs/unlock/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/unlock/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/unlock/jest.config.js b/libs/unlock/jest.config.js new file mode 100644 index 00000000000..a334dda3126 --- /dev/null +++ b/libs/unlock/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "unlock", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/unlock", +}; diff --git a/libs/unlock/package.json b/libs/unlock/package.json new file mode 100644 index 00000000000..f98da8683d0 --- /dev/null +++ b/libs/unlock/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/unlock", + "version": "0.0.1", + "description": "Unlock the account of a user", + "private": true, + "type": "commonjs", + "main": "index.js", + "types": "index.d.ts", + "license": "GPL-3.0", + "author": "key-management" +} diff --git a/libs/unlock/project.json b/libs/unlock/project.json new file mode 100644 index 00000000000..5bebf6c2609 --- /dev/null +++ b/libs/unlock/project.json @@ -0,0 +1,34 @@ +{ + "name": "unlock", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/unlock/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/unlock", + "main": "libs/unlock/src/index.ts", + "tsConfig": "libs/unlock/tsconfig.lib.json", + "assets": ["libs/unlock/*.md"], + "rootDir": "libs/unlock/src" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/unlock/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/unlock/jest.config.js" + } + } + } +} diff --git a/libs/unlock/src/default-unlock.service.spec.ts b/libs/unlock/src/default-unlock.service.spec.ts new file mode 100644 index 00000000000..1c82835855e --- /dev/null +++ b/libs/unlock/src/default-unlock.service.spec.ts @@ -0,0 +1,219 @@ +// Polyfill for Symbol.dispose required by the service's use of `using` keyword +import "core-js/proposals/explicit-resource-management"; + +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { PinStateServiceAbstraction } from "@bitwarden/common/key-management/pin/pin-state.service.abstraction"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsService, KdfConfigService } from "@bitwarden/key-management"; +import { LogService } from "@bitwarden/logging"; +import { PureCrypto } from "@bitwarden/sdk-internal"; +import { StateProvider } from "@bitwarden/state"; + +import { DefaultUnlockService } from "./default-unlock.service"; + +const mockUserId = "b1e2d3c4-a1b2-c3d4-e5f6-a1b2c3d4e5f6" as UserId; +const mockEmail = "test@example.com"; +const mockPin = "1234"; +const mockMasterPassword = "master-password"; +const mockKdfParams = { type: "pbkdf2" } as any; +const mockAccountCryptographicState = { some: "state" } as any; +const mockPinProtectedUserKeyEnvelope = { some: "envelope" } as any; +const mockMasterPasswordUnlockData = { some: "unlockData", salt: "salt", kdf: "pbkdf2" } as any; + +describe("DefaultUnlockService", () => { + const registerSdkService = mock(); + const accountCryptographicStateService = mock(); + const pinStateService = mock(); + const kdfService = mock(); + const accountService = mock(); + const masterPasswordService = mock(); + const cryptoFunctionService = mock(); + const stateProvider = mock(); + const logService = mock(); + const biometricsService = mock(); + + let service: DefaultUnlockService; + let mockSdkRef: any; + let mockSdk: any; + let mockCrypto: any; + + beforeEach(() => { + jest.resetAllMocks(); + + mockCrypto = { + initialize_user_crypto: jest.fn().mockResolvedValue(undefined), + }; + + mockSdkRef = { + value: { + crypto: jest.fn().mockReturnValue(mockCrypto), + }, + [Symbol.dispose]: jest.fn(), + }; + + mockSdk = { + take: jest.fn().mockReturnValue(mockSdkRef), + }; + + registerSdkService.registerClient$.mockReturnValue(of(mockSdk)); + accountCryptographicStateService.accountCryptographicState$.mockReturnValue( + of(mockAccountCryptographicState), + ); + kdfService.getKdfConfig$.mockReturnValue(of({ toSdkConfig: () => mockKdfParams } as any)); + accountService.accounts$ = of({ + [mockUserId]: { email: mockEmail }, + } as any); + pinStateService.getPinLockType.mockResolvedValue("PERSISTENT" as any); + pinStateService.getPinProtectedUserKeyEnvelope.mockResolvedValue( + mockPinProtectedUserKeyEnvelope, + ); + masterPasswordService.masterPasswordUnlockData$.mockReturnValue( + of({ toSdk: () => mockMasterPasswordUnlockData } as any), + ); + + Object.defineProperty(SdkLoadService, "Ready", { + value: Promise.resolve(), + writable: true, + configurable: true, + }); + + jest.spyOn(PureCrypto, "derive_kdf_material").mockReturnValue(new Uint8Array(32)); + + cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32)); + + const mockStateUpdate = jest.fn().mockResolvedValue(undefined); + stateProvider.getUser.mockReturnValue({ update: mockStateUpdate } as any); + + service = new DefaultUnlockService( + registerSdkService, + accountCryptographicStateService, + pinStateService, + kdfService, + accountService, + masterPasswordService, + cryptoFunctionService, + stateProvider, + logService, + biometricsService, + ); + }); + + describe("unlockWithPin", () => { + it("calls SDK initialize_user_crypto with correct pin method", async () => { + await service.unlockWithPin(mockUserId, mockPin); + + expect(mockCrypto.initialize_user_crypto).toHaveBeenCalledWith({ + userId: mockUserId, + kdfParams: mockKdfParams, + email: mockEmail, + accountCryptographicState: mockAccountCryptographicState, + method: { + pinEnvelope: { + pin: mockPin, + pin_protected_user_key_envelope: mockPinProtectedUserKeyEnvelope, + }, + }, + }); + }); + + it("throws when SDK is not available", async () => { + registerSdkService.registerClient$.mockReturnValue(of(null as any)); + + await expect(service.unlockWithPin(mockUserId, mockPin)).rejects.toThrow("SDK not available"); + }); + + it("fetches PERSISTENT pin envelope when the pin lock type is persistent", async () => { + pinStateService.getPinLockType.mockResolvedValue("PERSISTENT" as any); + await service.unlockWithPin(mockUserId, mockPin); + expect(pinStateService.getPinProtectedUserKeyEnvelope).toHaveBeenCalledWith( + mockUserId, + "PERSISTENT", + ); + }); + + it("fetches EPHEMERAL pin envelope when the pin lock type is ephemeral", async () => { + pinStateService.getPinLockType.mockResolvedValue("EPHEMERAL" as any); + await service.unlockWithPin(mockUserId, mockPin); + expect(pinStateService.getPinProtectedUserKeyEnvelope).toHaveBeenCalledWith( + mockUserId, + "EPHEMERAL", + ); + }); + }); + + describe("unlockWithMasterPassword", () => { + it("calls SDK initialize_user_crypto with correct master password method", async () => { + await service.unlockWithMasterPassword(mockUserId, mockMasterPassword); + + expect(mockCrypto.initialize_user_crypto).toHaveBeenCalledWith({ + userId: mockUserId, + kdfParams: mockKdfParams, + email: mockEmail, + accountCryptographicState: mockAccountCryptographicState, + method: { + masterPasswordUnlock: { + password: mockMasterPassword, + master_password_unlock: mockMasterPasswordUnlockData, + }, + }, + }); + }); + + it("throws when SDK is not available", async () => { + registerSdkService.registerClient$.mockReturnValue(of(null as any)); + + await expect( + service.unlockWithMasterPassword(mockUserId, mockMasterPassword), + ).rejects.toThrow("SDK not available"); + }); + }); + + describe("unlockWithBiometrics", () => { + const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey; + + it("calls SDK initialize_user_crypto with decrypted key from biometrics", async () => { + biometricsService.unlockWithBiometricsForUser.mockResolvedValue(mockUserKey); + + await service.unlockWithBiometrics(mockUserId); + + expect(biometricsService.unlockWithBiometricsForUser).toHaveBeenCalledWith(mockUserId); + expect(mockCrypto.initialize_user_crypto).toHaveBeenCalledWith({ + userId: mockUserId, + kdfParams: mockKdfParams, + email: mockEmail, + accountCryptographicState: mockAccountCryptographicState, + method: { + decryptedKey: { + decrypted_user_key: mockUserKey.toBase64(), + }, + }, + }); + }); + + it("throws when biometrics returns null", async () => { + biometricsService.unlockWithBiometricsForUser.mockResolvedValue(null); + + await expect(service.unlockWithBiometrics(mockUserId)).rejects.toThrow( + "Failed to unlock with biometrics", + ); + }); + + it("throws when SDK is not available", async () => { + biometricsService.unlockWithBiometricsForUser.mockResolvedValue(mockUserKey); + registerSdkService.registerClient$.mockReturnValue(of(null as any)); + + await expect(service.unlockWithBiometrics(mockUserId)).rejects.toThrow("SDK not available"); + }); + }); +}); diff --git a/libs/unlock/src/default-unlock.service.ts b/libs/unlock/src/default-unlock.service.ts new file mode 100644 index 00000000000..7a7b847937a --- /dev/null +++ b/libs/unlock/src/default-unlock.service.ts @@ -0,0 +1,227 @@ +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { + MASTER_KEY, + MASTER_KEY_HASH, +} from "@bitwarden/common/key-management/master-password/services/master-password.service"; +import { PinStateServiceAbstraction } from "@bitwarden/common/key-management/pin/pin-state.service.abstraction"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { MasterKey } from "@bitwarden/common/types/key"; +import { BiometricsService, KdfConfig, KdfConfigService } from "@bitwarden/key-management"; +import { LogService } from "@bitwarden/logging"; +import { + Kdf, + MasterPasswordUnlockData, + PasswordProtectedKeyEnvelope, + PureCrypto, + WrappedAccountCryptographicState, +} from "@bitwarden/sdk-internal"; +import { StateProvider } from "@bitwarden/state"; +import { UserId } from "@bitwarden/user-core"; + +import { UnlockService } from "./unlock.service"; + +export class DefaultUnlockService implements UnlockService { + constructor( + private registerSdkService: RegisterSdkService, + private accountCryptographicStateService: AccountCryptographicStateService, + private pinStateService: PinStateServiceAbstraction, + private kdfService: KdfConfigService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private cryptoFunctionService: CryptoFunctionService, + private stateProvider: StateProvider, + private logService: LogService, + private biometricsService: BiometricsService, + ) {} + + async unlockWithPin(userId: UserId, pin: string): Promise { + const startTime = performance.now(); + await firstValueFrom( + this.registerSdkService.registerClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + return ref.value.crypto().initialize_user_crypto({ + userId: asUuid(userId), + kdfParams: await this.getKdfParams(userId), + email: await this.getEmail(userId)!, + accountCryptographicState: await this.getAccountCryptographicState(userId), + method: { + pinEnvelope: { + pin: pin, + pin_protected_user_key_envelope: await this.getPinProtectedUserKeyEnvelope(userId), + }, + }, + }); + }), + ), + ); + this.logService.measure(startTime, "Unlock", "DefaultUnlockService", "unlockWithPin"); + } + + async unlockWithMasterPassword(userId: UserId, masterPassword: string): Promise { + const startTime = performance.now(); + await firstValueFrom( + this.registerSdkService.registerClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + return ref.value.crypto().initialize_user_crypto({ + userId: asUuid(userId), + kdfParams: await this.getKdfParams(userId), + email: await this.getEmail(userId), + accountCryptographicState: await this.getAccountCryptographicState(userId), + method: { + masterPasswordUnlock: { + password: masterPassword, + master_password_unlock: await this.getMasterPasswordUnlockData(userId), + }, + }, + }); + }), + ), + ); + await this.setLegacyMasterKeyFromUnlockData( + masterPassword, + await this.getMasterPasswordUnlockData(userId), + userId, + ); + this.logService.measure( + startTime, + "Unlock", + "DefaultUnlockService", + "unlockWithMasterPassword", + ); + } + + async unlockWithBiometrics(userId: UserId): Promise { + // First, get the biometrics-protected user key. This will prompt the user to authenticate with biometrics. + const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId); + if (!userKey) { + throw new Error("Failed to unlock with biometrics"); + } + + // Now that we have the biometrics-protected user key, we can initialize the SDK with it to complete the unlock process. + const startTime = performance.now(); + await firstValueFrom( + this.registerSdkService.registerClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + return ref.value.crypto().initialize_user_crypto({ + userId: asUuid(userId), + kdfParams: await this.getKdfParams(userId), + email: await this.getEmail(userId), + accountCryptographicState: await this.getAccountCryptographicState(userId), + method: { + decryptedKey: { + decrypted_user_key: userKey.toBase64(), + }, + }, + }); + }), + ), + ); + this.logService.measure(startTime, "Unlock", "DefaultUnlockService", "unlockWithBiometrics"); + } + + private async getAccountCryptographicState( + userId: UserId, + ): Promise { + const accountCryptographicState = await firstValueFrom( + this.accountCryptographicStateService.accountCryptographicState$(userId), + ); + assertNonNullish(accountCryptographicState, "Account cryptographic state is required"); + return accountCryptographicState!; + } + + private async getKdfParams(userId: UserId): Promise { + const kdfParams = await firstValueFrom( + this.kdfService.getKdfConfig$(userId).pipe( + map((config: KdfConfig | null) => { + return config?.toSdkConfig(); + }), + ), + ); + assertNonNullish(kdfParams, "KDF parameters are required"); + return kdfParams!; + } + + private async getEmail(userId: UserId): Promise { + const accounts = await firstValueFrom(this.accountService.accounts$); + const email = accounts[userId].email; + assertNonNullish(email, "Email is required"); + return email; + } + + private async getPinProtectedUserKeyEnvelope( + userId: UserId, + ): Promise { + const pinLockType = await this.pinStateService.getPinLockType(userId); + const pinEnvelope = await this.pinStateService.getPinProtectedUserKeyEnvelope( + userId, + pinLockType, + ); + assertNonNullish(pinEnvelope, "User is not enrolled in PIN unlock"); + return pinEnvelope!; + } + + private async getMasterPasswordUnlockData(userId: UserId): Promise { + const unlockData = await firstValueFrom( + this.masterPasswordService.masterPasswordUnlockData$(userId), + ); + assertNonNullish(unlockData, "Master password unlock data is required"); + return unlockData.toSdk(); + } + + private async setLegacyMasterKeyFromUnlockData( + password: string, + masterPasswordUnlockData: MasterPasswordUnlockData, + userId: UserId, + ): Promise { + assertNonNullish(password, "password"); + assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData"); + assertNonNullish(userId, "userId"); + this.logService.info("[DefaultUnlockService] Setting legacy master key from unlock data"); + + // NOTE: This entire section is deprecated and will be removed as soon as + // the masterkey is dropped from state. It is very temporary. + await SdkLoadService.Ready; + + const passwordBuffer = new TextEncoder().encode(password); + const saltBuffer = new TextEncoder().encode(masterPasswordUnlockData.salt); + const masterKey = PureCrypto.derive_kdf_material( + passwordBuffer, + saltBuffer, + masterPasswordUnlockData.kdf, + ); + const hash = await this.cryptoFunctionService.pbkdf2( + masterKey, + password, + "sha256", + 2, // HashPurpose.LocalAuthorization + ); + await this.stateProvider + .getUser(userId, MASTER_KEY) + .update((_) => new SymmetricCryptoKey(masterKey) as MasterKey); + await this.stateProvider + .getUser(userId, MASTER_KEY_HASH) + .update((_) => Utils.fromBufferToB64(hash)); + } +} diff --git a/libs/unlock/src/index.ts b/libs/unlock/src/index.ts new file mode 100644 index 00000000000..76dc28146f9 --- /dev/null +++ b/libs/unlock/src/index.ts @@ -0,0 +1,2 @@ +export { UnlockService } from "./unlock.service"; +export { DefaultUnlockService } from "./default-unlock.service"; diff --git a/libs/unlock/src/unlock.service.ts b/libs/unlock/src/unlock.service.ts new file mode 100644 index 00000000000..4188ac843cf --- /dev/null +++ b/libs/unlock/src/unlock.service.ts @@ -0,0 +1,35 @@ +import { UserId } from "@bitwarden/common/types/guid"; + +/** + * Service for unlocking a user's account with various methods. + */ +export abstract class UnlockService { + /** + * Unlocks the user's account using their PIN. + * + * @param userId - The user's id + * @param pin - The user's PIN + * @throws If the SDK is not available + * @throws If the PIN is invalid or decryption fails + */ + abstract unlockWithPin(userId: UserId, pin: string): Promise; + + /** + * Unlocks the user's account using their master password. + * + * @param userId - The user's id + * @param masterPassword - The user's master password + * @throws If the SDK is not available + * @throws If the master password is invalid or decryption fails + */ + abstract unlockWithMasterPassword(userId: UserId, masterPassword: string): Promise; + + /** + * Unlocks the user's account using biometrics. + * + * @param userId - The user's id + * @throws If the SDK is not available + * @throws If biometric authentication fails + */ + abstract unlockWithBiometrics(userId: UserId): Promise; +} diff --git a/libs/unlock/tsconfig.eslint.json b/libs/unlock/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/unlock/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/unlock/tsconfig.json b/libs/unlock/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/unlock/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/unlock/tsconfig.lib.json b/libs/unlock/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/unlock/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/unlock/tsconfig.spec.json b/libs/unlock/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/unlock/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/package-lock.json b/package-lock.json index 06e15223f24..5f30cd23b78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -637,6 +637,11 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "libs/unlock": { + "name": "@bitwarden/unlock", + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/user-core": { "name": "@bitwarden/user-core", "version": "0.0.0", @@ -5108,6 +5113,10 @@ "resolved": "libs/ui/common", "link": true }, + "node_modules/@bitwarden/unlock": { + "resolved": "libs/unlock", + "link": true + }, "node_modules/@bitwarden/user-core": { "resolved": "libs/user-core", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index fb76ea752a7..52baffb5a6a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -64,6 +64,7 @@ "@bitwarden/subscription": ["./libs/subscription/src/index.ts"], "@bitwarden/ui-common": ["./libs/ui/common/src"], "@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"], + "@bitwarden/unlock": ["./libs/unlock/src/index.ts"], "@bitwarden/user-core": ["./libs/user-core/src/index.ts"], "@bitwarden/user-crypto-management": ["./libs/user-crypto-management/src/index.ts"], "@bitwarden/vault": ["./libs/vault/src"],