From 616b94dde4f4b5e575965b2f264c0a161c39ccee Mon Sep 17 00:00:00 2001 From: Jared Snider Date: Thu, 20 Mar 2025 14:20:24 -0400 Subject: [PATCH] MVP of Opaque Login Strategy decryption --- .../src/services/jslib-services.module.ts | 2 +- .../login-strategies/opaque-login.strategy.ts | 79 ++++++++++++++----- .../common/models/domain/login-credentials.ts | 2 +- .../opaque-user-decryption-option.response.ts | 22 ++++++ .../user-decryption-options.response.ts | 12 +++ .../default-opaque-key-exchange.service.ts | 68 ++++++++++++++-- .../opaque/opaque-key-exchange.service.ts | 14 +++- libs/common/src/types/key.ts | 1 + 8 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 libs/common/src/auth/models/response/user-decryption-options/opaque-user-decryption-option.response.ts diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3ddc139ee34..24f73eee7c3 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1508,7 +1508,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OpaqueKeyExchangeService, useClass: DefaultOpaqueKeyExchangeService, - deps: [OpaqueKeyExchangeApiService, SdkService], + deps: [OpaqueKeyExchangeApiService, SdkService, EncryptService, LogService], }), safeProvider({ provide: MasterPasswordApiServiceAbstraction, diff --git a/libs/auth/src/common/login-strategies/opaque-login.strategy.ts b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts index ddcf823b539..846b64b57a8 100644 --- a/libs/auth/src/common/login-strategies/opaque-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/opaque-login.strategy.ts @@ -19,7 +19,7 @@ import { HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey } from "@bitwarden/common/types/key"; +import { MasterKey, OpaqueExportKey } from "@bitwarden/common/types/key"; import { OpaqueLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -40,7 +40,13 @@ export class OpaqueLoginStrategyData implements LoginStrategyData { /* The user's OPAQUE cipher configuration which controls the encryption schemes used during key derivation and key exchange */ - cipherConfiguration: OpaqueCipherConfiguration; + opaqueCipherConfiguration: OpaqueCipherConfiguration; + + /** + * The export key used to decrypt the user's key. We have to persist + * it to the cache so that we can decrypt after 2FA if required. + */ + opaqueExportKey: OpaqueExportKey; /** * Tracks if the user needs to be forced to update their password @@ -88,14 +94,12 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { } override async logIn(credentials: OpaqueLoginCredentials) { - const { email, masterPassword, kdfConfig, cipherConfiguration, twoFactor } = credentials; + const { email, masterPassword, kdfConfig, opaqueCipherConfiguration, twoFactor } = credentials; - // TODO: login returns export key, but we don't use it yet for decryption - // we must persist export key to cache and use it for decryption in setUserKey - const { sessionId } = await this.opaqueKeyExchangeService.login( + const { sessionId, opaqueExportKey } = await this.opaqueKeyExchangeService.login( email, masterPassword, - OpaqueCipherConfiguration.fromAny(cipherConfiguration), + OpaqueCipherConfiguration.fromAny(opaqueCipherConfiguration), ); const data = new OpaqueLoginStrategyData(); @@ -113,7 +117,8 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { HashPurpose.LocalAuthorization, ); - data.cipherConfiguration = cipherConfiguration; + data.opaqueCipherConfiguration = opaqueCipherConfiguration; + data.opaqueExportKey = opaqueExportKey; data.tokenRequest = new OpaqueTokenRequest( email, @@ -204,17 +209,55 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy { // We still need this for local user verification scenarios await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId); - // TODO: why not re-use master key from strategy data cache? - // const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - // if (masterKey) { - // const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - // masterKey, - // userId, - // ); - // await this.keyService.setUserKey(userKey, userId); - // } + await this.trySetUserKeyWithOpaqueExportKey(userId, response); + } - // TODO: follow trySetUserKeyWithDeviceKey pattern from SSO login strategy + private async trySetUserKeyWithOpaqueExportKey( + userId: UserId, + identityTokenResponse: IdentityTokenResponse, + ) { + const opaqueDecryptionOption = identityTokenResponse.userDecryptionOptions?.opaqueOption; + + if (!opaqueDecryptionOption) { + this.logService.error( + "Unable to set user key due to missing userDecryptionOptions.opaqueOption.", + ); + return; + } + + const opaqueExportKey = this.cache.value.opaqueExportKey; + const exportKeyEncryptedOpaquePrivateKey = opaqueDecryptionOption.encryptedPrivateKey; + const opaquePublicKeyEncryptedUserKey = opaqueDecryptionOption.encryptedUserKey; + + if (!opaqueExportKey) { + this.logService.error("Unable to set user key due to missing opaqueExportKey."); + return; + } + + if (!exportKeyEncryptedOpaquePrivateKey) { + this.logService.error( + "Unable to set user key due to missing exportKeyEncryptedOpaquePrivateKey.", + ); + return; + } + + if (!opaquePublicKeyEncryptedUserKey) { + this.logService.error( + "Unable to set user key due to missing opaquePublicKeyEncryptedUserKey.", + ); + return; + } + + const userKey = await this.opaqueKeyExchangeService.decryptUserKeyWithExportKey( + userId, + exportKeyEncryptedOpaquePrivateKey, + opaquePublicKeyEncryptedUserKey, + opaqueExportKey, + ); + + if (userKey) { + await this.keyService.setUserKey(userKey, userId); + } } protected override async setPrivateKey( diff --git a/libs/auth/src/common/models/domain/login-credentials.ts b/libs/auth/src/common/models/domain/login-credentials.ts index 00a60fa6b04..d391df71f6d 100644 --- a/libs/auth/src/common/models/domain/login-credentials.ts +++ b/libs/auth/src/common/models/domain/login-credentials.ts @@ -160,7 +160,7 @@ export class OpaqueLoginCredentials { public email: string, public masterPassword: string, public kdfConfig: KdfConfig, - public cipherConfiguration: OpaqueCipherConfiguration, + public opaqueCipherConfiguration: OpaqueCipherConfiguration, public twoFactor?: TokenTwoFactorRequest, ) {} } diff --git a/libs/common/src/auth/models/response/user-decryption-options/opaque-user-decryption-option.response.ts b/libs/common/src/auth/models/response/user-decryption-options/opaque-user-decryption-option.response.ts new file mode 100644 index 00000000000..3e7a01ee621 --- /dev/null +++ b/libs/common/src/auth/models/response/user-decryption-options/opaque-user-decryption-option.response.ts @@ -0,0 +1,22 @@ +import { BaseResponse } from "../../../../models/response/base.response"; +import { EncString } from "../../../../platform/models/domain/enc-string"; + +export interface IOpaqueDecryptionOptionServerResponse { + EncryptedPrivateKey: string; + EncryptedUserKey: string; +} + +export class OpaqueDecryptionOptionResponse extends BaseResponse { + encryptedPrivateKey: EncString | undefined; + encryptedUserKey: EncString | undefined; + + constructor(response: IOpaqueDecryptionOptionServerResponse) { + super(response); + if (response.EncryptedPrivateKey) { + this.encryptedPrivateKey = new EncString(this.getResponseProperty("EncryptedPrivateKey")); + } + if (response.EncryptedUserKey) { + this.encryptedUserKey = new EncString(this.getResponseProperty("EncryptedUserKey")); + } + } +} diff --git a/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.ts b/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.ts index 154c1f632c1..d74a82da129 100644 --- a/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.ts +++ b/libs/common/src/auth/models/response/user-decryption-options/user-decryption-options.response.ts @@ -4,6 +4,10 @@ import { IKeyConnectorUserDecryptionOptionServerResponse, KeyConnectorUserDecryptionOptionResponse, } from "./key-connector-user-decryption-option.response"; +import { + IOpaqueDecryptionOptionServerResponse, + OpaqueDecryptionOptionResponse, +} from "./opaque-user-decryption-option.response"; import { ITrustedDeviceUserDecryptionOptionServerResponse, TrustedDeviceUserDecryptionOptionResponse, @@ -18,6 +22,7 @@ export interface IUserDecryptionOptionsServerResponse { TrustedDeviceOption?: ITrustedDeviceUserDecryptionOptionServerResponse; KeyConnectorOption?: IKeyConnectorUserDecryptionOptionServerResponse; WebAuthnPrfOption?: IWebAuthnPrfDecryptionOptionServerResponse; + OpaqueOption?: IOpaqueDecryptionOptionServerResponse; } export class UserDecryptionOptionsResponse extends BaseResponse { @@ -25,6 +30,7 @@ export class UserDecryptionOptionsResponse extends BaseResponse { trustedDeviceOption?: TrustedDeviceUserDecryptionOptionResponse; keyConnectorOption?: KeyConnectorUserDecryptionOptionResponse; webAuthnPrfOption?: WebAuthnPrfDecryptionOptionResponse; + opaqueOption?: OpaqueDecryptionOptionResponse; constructor(response: IUserDecryptionOptionsServerResponse) { super(response); @@ -46,5 +52,11 @@ export class UserDecryptionOptionsResponse extends BaseResponse { this.getResponseProperty("WebAuthnPrfOption"), ); } + + if (response.OpaqueOption) { + this.opaqueOption = new OpaqueDecryptionOptionResponse( + this.getResponseProperty("OpaqueOption"), + ); + } } } diff --git a/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts b/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts index 508a63af628..9a859a30a62 100644 --- a/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts +++ b/libs/common/src/auth/opaque/default-opaque-key-exchange.service.ts @@ -4,9 +4,12 @@ import { RotateableKeySet } from "@bitwarden/auth/common"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { OpaqueSessionId } from "@bitwarden/common/types/guid"; +import { OpaqueSessionId, UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "../../types/key"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; +import { LogService } from "../../platform/abstractions/log.service"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { OpaqueExportKey, UserKey } from "../../types/key"; import { LoginFinishRequest } from "./models/login-finish.request"; import { LoginStartRequest } from "./models/login-start.request"; @@ -21,6 +24,8 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService constructor( private opaqueKeyExchangeApiService: OpaqueKeyExchangeApiService, private sdkService: SdkService, + private encryptService: EncryptService, + private logService: LogService, ) {} async register( @@ -81,14 +86,11 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService ); } - // TODO: we will likely have to break this apart to return the start / finish requests - // so that the opaque login strategy can send both to the identity token endpoint - // in separate calls. async login( email: string, masterPassword: string, cipherConfig: OpaqueCipherConfiguration, - ): Promise<{ sessionId: string; exportKey: Uint8Array }> { + ): Promise<{ sessionId: string; opaqueExportKey: OpaqueExportKey }> { if (!email || !masterPassword || !cipherConfig) { throw new Error( `Unable to log in user with missing parameters. email exists: ${!!email}; masterPassword exists: ${!!masterPassword}; cipherConfig exists: ${!!cipherConfig}`, @@ -119,6 +121,58 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService throw new Error("Login failed"); } - return { sessionId: loginStartResponse.sessionId, exportKey: loginFinish.export_key }; + const exportKey = new SymmetricCryptoKey(loginFinish.export_key) as OpaqueExportKey; + + return { sessionId: loginStartResponse.sessionId, opaqueExportKey: exportKey }; + } + + async decryptUserKeyWithExportKey( + userId: UserId, + exportKeyEncryptedOpaquePrivateKey: EncString, + opaquePublicKeyEncryptedUserKey: EncString, + exportKey: OpaqueExportKey, + ): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot decrypt user key with export key."); + } + + if (!exportKeyEncryptedOpaquePrivateKey) { + throw new Error( + "Encrypted OPAQUE private key is required. Cannot decrypt user key with export key.", + ); + } + + if (!opaquePublicKeyEncryptedUserKey) { + throw new Error("Encrypted user key is required. Cannot decrypt user key with export key."); + } + + if (!exportKey) { + // User doesn't have an export key, so we can't decrypt the user key. + return null; + } + + try { + // attempt to decrypt exportKeyEncryptedOpaquePrivateKey with exportKey + const opaquePrivateKey = await this.encryptService.decryptToBytes( + exportKeyEncryptedOpaquePrivateKey, + exportKey, + ); + + if (!opaquePrivateKey) { + throw new Error("Failed to decrypt opaque private key with export key."); + } + + // Attempt to decrypt opaquePublicKeyEncryptedUserKey with opaquePrivateKey + const userKey = await this.encryptService.rsaDecrypt( + opaquePublicKeyEncryptedUserKey, + opaquePrivateKey, + ); + + return new SymmetricCryptoKey(userKey) as UserKey; + } catch (e) { + this.logService.error("Failed to decrypt using export key. Error: ", e); + + return null; + } } } diff --git a/libs/common/src/auth/opaque/opaque-key-exchange.service.ts b/libs/common/src/auth/opaque/opaque-key-exchange.service.ts index 172c3806d75..1622ba4a8b4 100644 --- a/libs/common/src/auth/opaque/opaque-key-exchange.service.ts +++ b/libs/common/src/auth/opaque/opaque-key-exchange.service.ts @@ -1,6 +1,7 @@ -import { OpaqueSessionId } from "@bitwarden/common/types/guid"; +import { OpaqueSessionId, UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "../../types/key"; +import { EncString } from "../../platform/models/domain/enc-string"; +import { OpaqueExportKey, UserKey } from "../../types/key"; import { OpaqueCipherConfiguration } from "./models/opaque-cipher-configuration"; @@ -30,6 +31,13 @@ export abstract class OpaqueKeyExchangeService { cipherConfiguration: OpaqueCipherConfiguration, ): Promise<{ sessionId: string; - exportKey: Uint8Array; + opaqueExportKey: OpaqueExportKey; }>; + + abstract decryptUserKeyWithExportKey( + userId: UserId, + exportKeyEncryptedOpaquePrivateKey: EncString, + opaquePublicKeyEncryptedUserKey: EncString, + exportKey: OpaqueExportKey, + ): Promise; } diff --git a/libs/common/src/types/key.ts b/libs/common/src/types/key.ts index c9fd6975960..3000c81185b 100644 --- a/libs/common/src/types/key.ts +++ b/libs/common/src/types/key.ts @@ -11,6 +11,7 @@ export type PinKey = Opaque; export type OrgKey = Opaque; export type ProviderKey = Opaque; export type CipherKey = Opaque; +export type OpaqueExportKey = Opaque; // asymmetric keys export type UserPrivateKey = Opaque;