1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

MVP of Opaque Login Strategy decryption

This commit is contained in:
Jared Snider
2025-03-20 14:20:24 -04:00
parent a6110b0524
commit 616b94dde4
8 changed files with 170 additions and 30 deletions

View File

@@ -1508,7 +1508,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: OpaqueKeyExchangeService,
useClass: DefaultOpaqueKeyExchangeService,
deps: [OpaqueKeyExchangeApiService, SdkService],
deps: [OpaqueKeyExchangeApiService, SdkService, EncryptService, LogService],
}),
safeProvider({
provide: MasterPasswordApiServiceAbstraction,

View File

@@ -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(

View File

@@ -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,
) {}
}

View File

@@ -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"));
}
}
}

View File

@@ -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"),
);
}
}
}

View File

@@ -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<UserKey | null> {
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;
}
}
}

View File

@@ -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<UserKey | null>;
}

View File

@@ -11,6 +11,7 @@ export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;
export type CipherKey = Opaque<SymmetricCryptoKey, "CipherKey">;
export type OpaqueExportKey = Opaque<SymmetricCryptoKey, "OpaqueExportKey">;
// asymmetric keys
export type UserPrivateKey = Opaque<Uint8Array, "UserPrivateKey">;