mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
Innovation/opaque registration integration into password strategy (#13884)
* WIP on registration and all todos * Fix naming after merge * Initial draft of register method * Override processTokenResponse * remove premature todo * Password Login Strategy - (1) adjust comment (2) fix import * ChangePassword - update logic to use default argon config * Password Login Strategy - (1) Wire up saving MP to cache (2) Add null checking to registration * DefaultOpaqueKeyExchangeSvc - (1) Update naming (2) Add null param error handling --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { Argon2KdfConfig, KdfConfigService, KdfType, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service";
|
||||
|
||||
@@ -222,24 +222,27 @@ export class ChangePasswordComponent
|
||||
return this.updateKey();
|
||||
});
|
||||
} else {
|
||||
// PBKDF2 is not recommended for opaque, so force use of Argon2 with default params if the user is using PBKDF2.
|
||||
const userConfiguredKdf = await this.kdfConfigService.getKdfConfig();
|
||||
const opaqueKdf =
|
||||
userConfiguredKdf.kdfType === KdfType.Argon2id
|
||||
? userConfiguredKdf
|
||||
: new Argon2KdfConfig();
|
||||
|
||||
const sessionId = await this.opaqueKeyExchangeService.register(
|
||||
this.masterPassword,
|
||||
newUserKey[0],
|
||||
{
|
||||
memory: 256 * 1024,
|
||||
iterations: 3,
|
||||
parallelism: 4,
|
||||
},
|
||||
opaqueKdf,
|
||||
);
|
||||
request.opaqueSessionId = sessionId;
|
||||
this.formPromise = this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
// TODO: remove this test code
|
||||
await this.opaqueKeyExchangeService.register(this.masterPassword, newUserKey[0], {
|
||||
algorithm: "argon2id",
|
||||
parameters: { memory: 256 * 1024, iterations: 3, parallelism: 4 },
|
||||
});
|
||||
const userConfiguredKdf = await this.kdfConfigService.getKdfConfig();
|
||||
const opaqueKdf =
|
||||
userConfiguredKdf.kdfType === KdfType.Argon2id ? userConfiguredKdf : new Argon2KdfConfig();
|
||||
await this.opaqueKeyExchangeService.register(this.masterPassword, newUserKey[0], opaqueKdf);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -477,6 +477,8 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
TaskSchedulerService,
|
||||
PrePasswordLoginApiService,
|
||||
ConfigService,
|
||||
OpaqueKeyExchangeService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -98,7 +98,7 @@ export abstract class BaseLoginStrategy {
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
protected KdfConfigService: KdfConfigService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
@@ -246,7 +246,7 @@ export abstract class BaseLoginStrategy {
|
||||
tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token.
|
||||
);
|
||||
|
||||
await this.KdfConfigService.setKdfConfig(
|
||||
await this.kdfConfigService.setKdfConfig(
|
||||
userId as UserId,
|
||||
tokenResponse.kdf === KdfType.PBKDF2_SHA256
|
||||
? new PBKDF2KdfConfig(tokenResponse.kdfIterations)
|
||||
|
||||
@@ -13,11 +13,15 @@ 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 { OpaqueKeyExchangeService } from "@bitwarden/common/auth/opaque/opaque-key-exchange.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
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 { Argon2KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordHashLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
@@ -32,6 +36,12 @@ export class PasswordLoginStrategyData implements LoginStrategyData {
|
||||
userEnteredEmail: string;
|
||||
/** If 2fa is required, token is returned to bypass captcha */
|
||||
captchaBypassToken?: string;
|
||||
|
||||
// TODO: we need to get a security audit as to whether or not this is safe to do. It is only used in memory
|
||||
// for the duration of the login process, but it is still a security risk - especially in 2FA and new device verification
|
||||
/** The user's master password */
|
||||
masterPassword: string;
|
||||
|
||||
/** The local version of the user's master key hash */
|
||||
localMasterKeyHash: string;
|
||||
/** The user's master key */
|
||||
@@ -65,6 +75,8 @@ export class PasswordLoginStrategy extends BaseLoginStrategy {
|
||||
data: PasswordLoginStrategyData,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private configService: ConfigService,
|
||||
private opaqueKeyExchangeService: OpaqueKeyExchangeService,
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
@@ -82,6 +94,7 @@ export class PasswordLoginStrategy extends BaseLoginStrategy {
|
||||
|
||||
const data = new PasswordLoginStrategyData();
|
||||
|
||||
data.masterPassword = masterPassword;
|
||||
data.masterKey = await this.makePrePasswordLoginMasterKey(masterPassword, email, kdfConfig);
|
||||
data.userEnteredEmail = email;
|
||||
|
||||
@@ -138,6 +151,23 @@ export class PasswordLoginStrategy extends BaseLoginStrategy {
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
}
|
||||
|
||||
return authResult;
|
||||
}
|
||||
|
||||
protected override async processTokenResponse(
|
||||
response: IdentityTokenResponse,
|
||||
): Promise<AuthResult> {
|
||||
const authResult = await super.processTokenResponse(response);
|
||||
|
||||
const opaqueKeyExchangeFeatureFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.OpaqueKeyExchange,
|
||||
);
|
||||
if (opaqueKeyExchangeFeatureFlagEnabled) {
|
||||
// Register the user for opaque password key exchange
|
||||
await this.registerUserForOpaqueKeyExchange(authResult.userId);
|
||||
}
|
||||
|
||||
return authResult;
|
||||
}
|
||||
|
||||
@@ -246,4 +276,34 @@ export class PasswordLoginStrategy extends BaseLoginStrategy {
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers password using users with the OPAQUE key exchange protocol which is a more secure password
|
||||
* authN protocol which prevents the server from ever knowing anything about the user's password.
|
||||
*/
|
||||
private async registerUserForOpaqueKeyExchange(userId: UserId) {
|
||||
const masterPassword = this.cache.value?.masterPassword;
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
// PBKDF2 is not recommended for opaque, so force use of Argon2 with default params if the user is using PBKDF2.
|
||||
const userConfiguredKdf = await this.kdfConfigService.getKdfConfig();
|
||||
|
||||
if (!masterPassword || !userKey || !userConfiguredKdf) {
|
||||
this.logService.error(
|
||||
`Unable to register user for OPAQUE key exchange due to missing data. MasterPassword exists: ${!!masterPassword}; UserKey exists ${!!userKey}; KdfConfig exists: ${!!userConfiguredKdf}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const opaqueKdf =
|
||||
userConfiguredKdf.kdfType === KdfType.Argon2id ? userConfiguredKdf : new Argon2KdfConfig();
|
||||
|
||||
try {
|
||||
await this.opaqueKeyExchangeService.register(masterPassword, userKey, opaqueKdf);
|
||||
} catch (error) {
|
||||
// If this process fails for any reason, we don't want to stop the login process
|
||||
// so just log the error and continue.
|
||||
this.logService.error(`Failed to register user for OPAQUE key exchange: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { PrePasswordLoginRequest } from "@bitwarden/common/auth/models/request/pre-password-login.request";
|
||||
import { OpaqueKeyExchangeService } from "@bitwarden/common/auth/opaque/opaque-key-exchange.service";
|
||||
import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -27,6 +28,7 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -131,6 +133,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected taskSchedulerService: TaskSchedulerService,
|
||||
protected prePasswordLoginApiService: PrePasswordLoginApiService,
|
||||
protected configService: ConfigService,
|
||||
protected opaqueKeyExchangeService: OpaqueKeyExchangeService,
|
||||
) {
|
||||
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
|
||||
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
|
||||
@@ -226,8 +230,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
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);
|
||||
@@ -396,6 +398,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
data?.password ?? new PasswordLoginStrategyData(),
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this.configService,
|
||||
this.opaqueKeyExchangeService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.Sso:
|
||||
|
||||
@@ -25,25 +25,31 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService
|
||||
async register(
|
||||
masterPassword: string,
|
||||
userKey: UserKey,
|
||||
ksfParameters: Argon2IdParameters,
|
||||
keyStretchingFuncArgon2Params: Argon2IdParameters, // TODO: eval if we can use KdfConfig existing type
|
||||
): Promise<OpaqueSessionId> {
|
||||
const config = new CipherConfiguration(ksfParameters);
|
||||
if (!masterPassword || !userKey || !keyStretchingFuncArgon2Params) {
|
||||
throw new Error(
|
||||
`Unable to register user with missing parameters. masterPassword exists: ${!!masterPassword}, userKey exists: ${!!userKey}, keyStretchingFuncArgon2Params exists: ${!!keyStretchingFuncArgon2Params}`,
|
||||
);
|
||||
}
|
||||
|
||||
const cipherConfig = new CipherConfiguration(keyStretchingFuncArgon2Params);
|
||||
const cryptoClient = (await firstValueFrom(this.sdkService.client$)).crypto();
|
||||
|
||||
const registrationStart = cryptoClient.opaque_register_start(
|
||||
masterPassword,
|
||||
config.toSdkConfig(),
|
||||
cipherConfig.toSdkConfig(),
|
||||
);
|
||||
const registrationStartResponse = await this.opaqueKeyExchangeApiService.registrationStart(
|
||||
new RegistrationStartRequest(
|
||||
Utils.fromBufferToB64(registrationStart.registration_request),
|
||||
config,
|
||||
cipherConfig,
|
||||
),
|
||||
);
|
||||
|
||||
const registrationFinish = cryptoClient.opaque_register_finish(
|
||||
masterPassword,
|
||||
config.toSdkConfig(),
|
||||
cipherConfig.toSdkConfig(),
|
||||
Utils.fromB64ToArray(registrationStartResponse.registrationResponse),
|
||||
registrationStart.state,
|
||||
);
|
||||
@@ -75,19 +81,25 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService
|
||||
async login(
|
||||
email: string,
|
||||
masterPassword: string,
|
||||
ksfConfig: Argon2IdParameters,
|
||||
keyStretchingFuncArgon2Params: Argon2IdParameters,
|
||||
): Promise<Uint8Array> {
|
||||
const config = new CipherConfiguration(ksfConfig);
|
||||
if (!email || !masterPassword || !keyStretchingFuncArgon2Params) {
|
||||
throw new Error(
|
||||
`Unable to log in user with missing parameters. email exists: ${!!email}; masterPassword exists: ${!!masterPassword}; keyStretchingFuncArgon2Params exists: ${!!keyStretchingFuncArgon2Params}`,
|
||||
);
|
||||
}
|
||||
|
||||
const cipherConfig = new CipherConfiguration(keyStretchingFuncArgon2Params);
|
||||
const cryptoClient = (await firstValueFrom(this.sdkService.client$)).crypto();
|
||||
|
||||
const loginStart = cryptoClient.opaque_login_start(masterPassword, config.toSdkConfig());
|
||||
const loginStart = cryptoClient.opaque_login_start(masterPassword, cipherConfig.toSdkConfig());
|
||||
const loginStartResponse = await this.opaqueKeyExchangeApiService.loginStart(
|
||||
new LoginStartRequest(email, Utils.fromBufferToB64(loginStart.credential_request)),
|
||||
);
|
||||
|
||||
const loginFinish = cryptoClient.opaque_login_finish(
|
||||
masterPassword,
|
||||
config.toSdkConfig(),
|
||||
cipherConfig.toSdkConfig(),
|
||||
Utils.fromB64ToArray(loginStartResponse.credentialResponse),
|
||||
loginStart.state,
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ export class CipherConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: eval if we can just use KdfConfig existing type
|
||||
export type Argon2IdParameters = {
|
||||
// Memory in KiB
|
||||
memory: number;
|
||||
|
||||
Reference in New Issue
Block a user