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:
@@ -1508,7 +1508,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: OpaqueKeyExchangeService,
|
||||
useClass: DefaultOpaqueKeyExchangeService,
|
||||
deps: [OpaqueKeyExchangeApiService, SdkService],
|
||||
deps: [OpaqueKeyExchangeApiService, SdkService, EncryptService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordApiServiceAbstraction,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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">;
|
||||
|
||||
Reference in New Issue
Block a user