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

Innovation/OPAQUE - Add and cleanup some TODOs (#13873)

* LoginStrategyServiceAbstraction - add TODO to refactor makePrePasswordLoginMasterKey in future

* OpaqueLoginCredentials - add kdfConfig so we can derive master key for user verification scenarios.

* LoginStrategyService.logIn - add TODO

* OpaqueTokenRequest - add more docs

* CipherConfiguration - add todo for more docs

* DefaultOpaqueService - add todo

* OpaqueLoginStrategy - (1) Add docs (2) clean up todos (3) add todos
This commit is contained in:
Jared Snider
2025-03-17 12:54:36 -04:00
committed by GitHub
parent a2ba965abd
commit 70d6337ec2
7 changed files with 32 additions and 12 deletions

View File

@@ -70,6 +70,9 @@ export abstract class LoginStrategyServiceAbstraction {
// TODO: PM-15162 - deprecate captchaResponse
captchaResponse: string,
) => Promise<AuthResult>;
// TODO: PM-19273 - Refactor makePrePasswordLoginMasterKey to no longer be on the service
// once PM-18176 removes the Recover2faComponent dependency.
/**
* Creates a master key from the provided master password and email.
* If a KdfConfig is provided, it will be used to generate the key.

View File

@@ -14,6 +14,7 @@ import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { CipherConfiguration } from "@bitwarden/common/auth/opaque/models/cipher-configuration";
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";
@@ -38,6 +39,10 @@ export class OpaqueLoginStrategyData implements LoginStrategyData {
/** The user's master key */
masterKey: MasterKey;
/* The user's OPAQUE cipher configuration which controls
the encryption schemes used during key derivation and key exchange */
cipherConfiguration: CipherConfiguration;
/**
* Tracks if the user needs to be forced to update their password
*/
@@ -52,10 +57,12 @@ export class OpaqueLoginStrategyData implements LoginStrategyData {
}
}
// TODO: link to RFC and give simple, brief explanation of the protocol
/**
*
* A login strategy that uses the ...
* A login strategy that uses the OPAQUE protocol for password authentication.
* OPAQUE (Oblivious Pseudorandom Function (OPRF)-based Password Authentication and Key Exchange)
* is a protocol that allows a client to authenticate to a server without revealing the password to the server.
* RFC: https://www.ietf.org/archive/id/draft-irtf-cfrg-opaque-03.html
*/
export class OpaqueLoginStrategy extends BaseLoginStrategy {
/** The email address of the user attempting to log in. */
@@ -81,19 +88,20 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy {
this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
}
// TODO: build OpaqueLoginCredentials
override async logIn(credentials: OpaqueLoginCredentials) {
const { email, masterPassword, twoFactor } = credentials;
const { email, masterPassword, kdfConfig, cipherConfiguration, twoFactor } = credentials;
const data = new OpaqueLoginStrategyData();
// TODO: we will still generate a master key here but we need to extract the prelogin call out of the makePreloginKey
// and simply rename it deriveMasterKey or something similar
data.userEnteredEmail = email;
// Even though we are completing OPAQUE authN and not logging in with password hash,
// we still need to hash the master password for logged in user verification scenarios.
data.masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey(
masterPassword,
email,
kdfConfig,
);
data.userEnteredEmail = email;
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
data.localMasterKeyHash = await this.keyService.hashMasterKey(
@@ -102,12 +110,10 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy {
HashPurpose.LocalAuthorization,
);
// const serverMasterKeyHash = await this.keyService.hashMasterKey(masterPassword, data.masterKey);
data.cipherConfiguration = cipherConfiguration;
// TODO: we must figure out how we will handle 2FA at some point.
data.tokenRequest = new OpaqueTokenRequest(
email,
undefined,
await this.buildTwoFactor(twoFactor, email),
await this.buildDeviceRequest(),
);
@@ -193,6 +199,7 @@ 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(

View File

@@ -44,6 +44,7 @@ export class PasswordLoginCredentials {
? new OpaqueLoginCredentials(
this.email,
this.masterPassword,
preLoginResponse.toKdfConfig(),
preLoginResponse.opaqueConfiguration,
)
: new PasswordHashLoginCredentials(
@@ -158,6 +159,7 @@ export class OpaqueLoginCredentials {
constructor(
public email: string,
public masterPassword: string,
public kdfConfig: KdfConfig,
public cipherConfiguration: CipherConfiguration,
public twoFactor?: TokenTwoFactorRequest,
) {}

View File

@@ -226,6 +226,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
// Password credentials may use the PasswordHashLoginStrategy or the OpaqueLoginStrategy
if (credentials.type === AuthenticationType.Password) {
const preLoginRequest = new PrePasswordLoginRequest(credentials.email);
// TODO: OPAQUE: we have to save off whether or not to enroll the user in OPAQUE based on the
// response from the pre-password-login request and execute the enroll in the password login strategy
const preLoginResponse =
await this.prePasswordLoginApiService.postPrePasswordLogin(preLoginRequest);
ownedCredentials = credentials.toSpecificLoginCredentials(preLoginResponse);

View File

@@ -6,10 +6,10 @@ import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
// TODO: we might have to support both login start and login finish requests within this?
// or, we could have separate OpaqueStartTokenRequest and OpaqueFinishTokenRequest classes
export class OpaqueTokenRequest extends TokenRequest {
constructor(
public email: string,
public masterPasswordHash: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
public newDeviceOtp?: string,
@@ -20,9 +20,9 @@ export class OpaqueTokenRequest extends TokenRequest {
toIdentityToken(clientId: ClientType) {
const obj = super.toIdentityToken(clientId);
// TODO: what grant type for OPAQUE?
obj.grant_type = "password";
obj.username = this.email;
obj.password = this.masterPasswordHash;
if (this.newDeviceOtp) {
obj.newDeviceOtp = this.newDeviceOtp;

View File

@@ -69,6 +69,9 @@ export class DefaultOpaqueService implements OpaqueService {
return registrationStartResponse.sessionId;
}
// 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,

View File

@@ -1,5 +1,7 @@
import { CipherConfiguration as CipherConfigurationSdk } from "@bitwarden/sdk-internal";
// TODO: add js docs to all types / classes here.
export type CipherSuite = OPAQUEKE3_RISTRETTO255_3DH_ARGON2ID13_SUITE;
export type OPAQUEKE3_RISTRETTO255_3DH_ARGON2ID13_SUITE =
"OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF";