mirror of
https://github.com/bitwarden/browser
synced 2026-02-27 10:03:23 +00:00
Merge main
This commit is contained in:
@@ -10,7 +10,9 @@ import {
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
@@ -209,6 +211,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
constructor(
|
||||
private auditService: AuditService,
|
||||
private cipherService: CipherService,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
@@ -312,7 +315,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
}
|
||||
|
||||
if (!this.email) {
|
||||
throw new Error("Email is required to create master key.");
|
||||
throw new Error("Email not found.");
|
||||
}
|
||||
|
||||
// 1. Determine kdfConfig
|
||||
@@ -320,13 +323,13 @@ export class InputPasswordComponent implements OnInit {
|
||||
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
} else {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
throw new Error("userId not found.");
|
||||
}
|
||||
this.kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
|
||||
}
|
||||
|
||||
if (this.kdfConfig == null) {
|
||||
throw new Error("KdfConfig is required to create master key.");
|
||||
throw new Error("KdfConfig not found.");
|
||||
}
|
||||
|
||||
const salt =
|
||||
@@ -334,7 +337,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
? await firstValueFrom(this.masterPasswordService.saltForUser$(this.userId))
|
||||
: this.masterPasswordService.emailToSalt(this.email);
|
||||
if (salt == null) {
|
||||
throw new Error("Salt is required to create master key.");
|
||||
throw new Error("Salt not found.");
|
||||
}
|
||||
|
||||
// 2. Verify current password is correct (if necessary)
|
||||
@@ -361,6 +364,41 @@ export class InputPasswordComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
// When you unwind the flag in PM-28143, also remove the ConfigService if it is un-used.
|
||||
const newApisWithInputPasswordFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword,
|
||||
);
|
||||
|
||||
if (newApisWithInputPasswordFlagEnabled) {
|
||||
// 4. Build a PasswordInputResult object
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
kdfConfig: this.kdfConfig,
|
||||
salt,
|
||||
newPasswordHint,
|
||||
newApisWithInputPasswordFlagEnabled, // To be removed in PM-28143
|
||||
};
|
||||
|
||||
if (
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
passwordInputResult.currentPassword = currentPassword;
|
||||
}
|
||||
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value;
|
||||
}
|
||||
|
||||
// 5. Emit and return PasswordInputResult object
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
return passwordInputResult;
|
||||
}
|
||||
|
||||
/*******************************************************************
|
||||
* The following code (within this `try`) to be removed in PM-28143
|
||||
*******************************************************************/
|
||||
|
||||
// 4. Create cryptographic keys and build a PasswordInputResult object
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
newPassword,
|
||||
|
||||
@@ -6,14 +6,12 @@ import * as stories from "./input-password.stories.ts";
|
||||
|
||||
# InputPassword Component
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials.
|
||||
Specifically, it does the following:
|
||||
The `InputPasswordComponent` allows a user to enter a new master password for the purpose of setting
|
||||
an initial password or changing an existing password. Specifically, it does the following:
|
||||
|
||||
1. Displays form fields in the UI
|
||||
2. Validates form fields
|
||||
3. Generates cryptographic properties based on the form inputs (e.g. `newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.)
|
||||
4. Emits the generated properties to the parent component
|
||||
3. Emits values to the parent component
|
||||
|
||||
The `InputPasswordComponent` is central to our set/change password flows, allowing us to keep our
|
||||
form UI and validation logic consistent. As such, it is intended for re-use in different set/change
|
||||
@@ -30,7 +28,6 @@ those values as needed.
|
||||
- [The InputPasswordFlow](#the-inputpasswordflow)
|
||||
- [Use Cases](#use-cases)
|
||||
- [HTML - Form Fields](#html---form-fields)
|
||||
- [TypeScript - Credential Generation](#typescript---credential-generation)
|
||||
- [Difference between SetInitialPasswordAccountRegistration and SetInitialPasswordAuthedUser](#difference-between-setinitialpasswordaccountregistration-and-setinitialpasswordautheduser)
|
||||
- [Validation](#validation)
|
||||
- [Submit Logic](#submit-logic)
|
||||
@@ -44,20 +41,20 @@ those values as needed.
|
||||
**Required**
|
||||
|
||||
- `flow` - the parent component must provide an `InputPasswordFlow`, which is used to determine
|
||||
which form input elements will be displayed in the UI and which cryptographic keys will be created
|
||||
and emitted. [Click here](#the-inputpasswordflow) to learn more about the different
|
||||
`InputPasswordFlow` options.
|
||||
which form input elements will be displayed in the UI and which values will be emitted.
|
||||
[Click here](#the-inputpasswordflow) to learn more about the different `InputPasswordFlow`
|
||||
options.
|
||||
|
||||
**Optional (sometimes)**
|
||||
|
||||
These two `@Inputs` are optional on some flows, but required on others. Therefore these `@Inputs`
|
||||
are not marked as `{ required: true }`, but there _is_ component logic that ensures (requires) that
|
||||
the `email` and/or `userId` is present in certain flows, while not present in other flows.
|
||||
These `@Inputs` are optional on some flows, but required on others. Therefore these `@Inputs` are
|
||||
not marked as `{ required: true }`, but there _is_ component logic that ensures (requires) that the
|
||||
`email` and/or `userId` is present in certain flows, while not present in other flows.
|
||||
|
||||
- `email` - allows the `InputPasswordComponent` to generate a master key
|
||||
- `email` - allows the `InputPasswordComponent` to use the email as a salt (if needed)
|
||||
- `userId` - allows the `InputPasswordComponent` to do things like get the user's `kdfConfig`,
|
||||
verify that a current password is correct, and perform validation prior to user key rotation on
|
||||
the parent
|
||||
verify that a current password is correct, and perform validation prior to user key rotation (if
|
||||
selected) on the parent
|
||||
|
||||
**Optional**
|
||||
|
||||
@@ -87,8 +84,7 @@ These `@Inputs` are truly optional.
|
||||
## The `InputPasswordFlow`
|
||||
|
||||
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
||||
credential generation logic of the component. It is important for the dev to understand when to use
|
||||
each flow.
|
||||
logic of the component. It is important for the dev to understand when to use each flow.
|
||||
|
||||
### Use Cases
|
||||
|
||||
@@ -106,8 +102,9 @@ Used in scenarios where we do have an existing and authed user, and thus an acti
|
||||
|
||||
- A "just-in-time" (JIT) provisioned user joins a master password (MP) encryption org and must set
|
||||
their initial password
|
||||
- A "just-in-time" (JIT) provisioned user joins a trusted device encryption (TDE) org with a
|
||||
starting role that requires them to have/set their initial password
|
||||
- A "just-in-time" (JIT) provisioned user joins a trusted device encryption (TDE) org with the reset
|
||||
password permission ("manage account recovery") from the start, which requires them to have/set
|
||||
their initial password
|
||||
- A note on JIT provisioned user flows:
|
||||
- Even though a JIT provisioned user is a brand-new user who was “just” created, we consider
|
||||
them to be an “existing authed user” _from the perspective of the set-password flow_. This is
|
||||
@@ -117,8 +114,9 @@ Used in scenarios where we do have an existing and authed user, and thus an acti
|
||||
registration when a user reaches the `/finish-signup` or `/trial-initiation` page to set their
|
||||
initial password, their account does not yet exist in the database, and will only be created
|
||||
once they set an initial password.
|
||||
- An existing user in a TDE org logs in after the org admin upgraded the user to a role that now
|
||||
requires them to have/set their initial password
|
||||
- An existing user in a TDE org logs in after an org admin upgraded the user to have the reset
|
||||
password persmission ("manage account recovery"), which now requires the user to have/set their
|
||||
initial password
|
||||
- An existing user logs in after their org admin offboarded the org from TDE, and the user must now
|
||||
have/set their initial password<br /><br />
|
||||
|
||||
@@ -126,7 +124,7 @@ Used in scenarios where we do have an existing and authed user, and thus an acti
|
||||
|
||||
Used in scenarios where we simply want to offer the user the ability to change their password:
|
||||
|
||||
- User clicks an org email invite link an logs in with their password which does not meet the org's
|
||||
- User clicks an org email invite link and logs in with their password which does not meet the org's
|
||||
policy requirements
|
||||
- User logs in with password that does not meet the org's policy requirements
|
||||
- User logs in after their password was reset via Account Recovery (and now they must change their
|
||||
@@ -156,26 +154,10 @@ which form field UI elements get displayed.
|
||||
|
||||
<br />
|
||||
|
||||
### TypeScript - Credential Generation
|
||||
|
||||
- **`SetInitialPasswordAccountRegistration`** and **`SetInitialPasswordAuthedUser`**
|
||||
- These flows involve a user setting their password for the first time. Therefore on submit the
|
||||
component will only generate new credentials (`newMasterKey`) and not current credentials
|
||||
(`currentMasterKey`).<br /><br />
|
||||
- **`ChangePassword`** and **`ChangePasswordWithOptionalUserKeyRotation`**
|
||||
- These flows both require the user to enter a current password along with a new password.
|
||||
Therefore on submit the component will generate current credentials (`currentMasterKey`) along
|
||||
with new credentials (`newMasterKey`).<br /><br />
|
||||
- **`ChangePasswordDelegation`**
|
||||
- This flow does not generate any credentials, but simply validates the new password and emits it
|
||||
up to the parent.
|
||||
|
||||
<br />
|
||||
|
||||
### Difference between `SetInitialPasswordAccountRegistration` and `SetInitialPasswordAuthedUser`
|
||||
|
||||
These two flows are similar in that they display the same form fields and only generate new
|
||||
credentials, but we need to keep them separate for the following reasons:
|
||||
These two flows are similar in that they display the same form fields, but we need to keep them
|
||||
separate for the following reasons:
|
||||
|
||||
- `SetInitialPasswordAccountRegistration` involves scenarios where we have no existing user, and
|
||||
**thus NO active account `userId`**:
|
||||
@@ -183,7 +165,7 @@ credentials, but we need to keep them separate for the following reasons:
|
||||
and **thus an active account `userId`**:
|
||||
|
||||
The presence or absence of an active account `userId` is important because it determines how we get
|
||||
the correct `kdfConfig` prior to key generation:
|
||||
the correct `kdfConfig`:
|
||||
|
||||
- If there is no `userId` passed down from the parent, we default to `DEFAULT_KDF_CONFIG`
|
||||
- If there is a `userId` passed down from the parent, we get the `kdfConfig` from state using the
|
||||
@@ -223,25 +205,16 @@ When the form is submitted, the `InputPasswordComponent` does the following in o
|
||||
checkbox)
|
||||
- Checks that the new password adheres to any enforced master password policies that were
|
||||
optionally passed down by the parent
|
||||
2. Uses the form inputs to create cryptographic properties (`newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.)
|
||||
3. Emits those cryptographic properties up to the parent (along with other values defined in
|
||||
`PasswordInputResult`) to be used by the parent as needed.
|
||||
2. Emits values up to the parent (along with other values defined in `PasswordInputResult`) to be
|
||||
used by the parent as needed.
|
||||
|
||||
```typescript
|
||||
export interface PasswordInputResult {
|
||||
currentPassword?: string;
|
||||
currentMasterKey?: MasterKey;
|
||||
currentServerMasterKeyHash?: string;
|
||||
currentLocalMasterKeyHash?: string;
|
||||
|
||||
newPassword: string;
|
||||
newPasswordHint?: string;
|
||||
newMasterKey?: MasterKey;
|
||||
newServerMasterKeyHash?: string;
|
||||
newLocalMasterKeyHash?: string;
|
||||
|
||||
kdfConfig?: KdfConfig;
|
||||
salt?: MasterPasswordSalt;
|
||||
newPasswordHint?: string;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -59,6 +60,13 @@ export default {
|
||||
getAllDecrypted: () => Promise.resolve([]),
|
||||
},
|
||||
},
|
||||
// Can remove ConfigService from component and stories in PM-28143 (if it is no longer used)
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
getFeatureFlag: () => false, // default to false since flag does not effect UI
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: {
|
||||
|
||||
@@ -10,6 +10,20 @@ export interface PasswordInputResult {
|
||||
newPasswordHint?: string;
|
||||
rotateUserKey?: boolean;
|
||||
|
||||
/**
|
||||
* Temporary property that persists the flag state through the entire set/change password process.
|
||||
* This allows flows to consume this value instead of re-checking the flag state via ConfigService themselves.
|
||||
*
|
||||
* The ChangePasswordDelegation flows (Emergency Access Takeover and Account Recovery), however, only ever
|
||||
* require a raw newPassword from the InputPasswordComponent regardless of whether the flag is on or off.
|
||||
* Flagging for those 2 flows will be done via the ConfigService in their respective services.
|
||||
*
|
||||
* To be removed in PM-28143
|
||||
*/
|
||||
newApisWithInputPasswordFlagEnabled?: boolean;
|
||||
|
||||
// The deprecated properties below will be removed in PM-28143: https://bitwarden.atlassian.net/browse/PM-28143
|
||||
|
||||
/** @deprecated This low-level cryptographic state will be removed. It will be replaced by high level calls to masterpassword service, in the consumers of this interface. */
|
||||
currentMasterKey?: MasterKey;
|
||||
/** @deprecated */
|
||||
|
||||
@@ -278,13 +278,6 @@ describe("LoginDecryptionOptionsComponent", () => {
|
||||
const expectedUserKey = new SymmetricCryptoKey(new Uint8Array(mockUserKeyBytes));
|
||||
|
||||
// Verify keys were set
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockUserId);
|
||||
expect(keyService.setSignedPublicKey).toHaveBeenCalledWith(mockSignedPublicKey, mockUserId);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith(mockSigningKey, mockUserId);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
mockSecurityState,
|
||||
mockUserId,
|
||||
);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
V2: {
|
||||
|
||||
@@ -34,11 +34,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import {
|
||||
SignedPublicKey,
|
||||
SignedSecurityState,
|
||||
WrappedSigningKey,
|
||||
} from "@bitwarden/common/key-management/types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -322,23 +317,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
register_result.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
// Legacy individual states
|
||||
await this.keyService.setPrivateKey(
|
||||
register_result.account_cryptographic_state.V2.private_key,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setSignedPublicKey(
|
||||
register_result.account_cryptographic_state.V2.signed_public_key as SignedPublicKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserSigningKey(
|
||||
register_result.account_cryptographic_state.V2.signing_key as WrappedSigningKey,
|
||||
userId,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
register_result.account_cryptographic_state.V2.security_state as SignedSecurityState,
|
||||
userId,
|
||||
);
|
||||
|
||||
// TDE unlock
|
||||
await this.deviceTrustService.setDeviceKey(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { TwoFactorTimeoutIcon } from "@bitwarden/assets/svg";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ButtonModule, IconModule } from "@bitwarden/components";
|
||||
import { ButtonModule, SvgModule } from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
* RegistrationLinkExpiredComponentData
|
||||
@@ -24,7 +24,7 @@ export interface RegistrationLinkExpiredComponentData {
|
||||
@Component({
|
||||
selector: "auth-registration-link-expired",
|
||||
templateUrl: "./registration-link-expired.component.html",
|
||||
imports: [CommonModule, JslibModule, RouterModule, IconModule, ButtonModule],
|
||||
imports: [CommonModule, JslibModule, RouterModule, SvgModule, ButtonModule],
|
||||
})
|
||||
export class RegistrationLinkExpiredComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ButtonModule,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@@ -54,7 +54,7 @@ const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record<Region, boolean> = {
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
RegistrationEnvSelectorComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -11,30 +11,30 @@
|
||||
[ngSwitch]="provider.type"
|
||||
class="tw-w-16 md:tw-w-20 tw-mr-2 sm:tw-mr-4"
|
||||
>
|
||||
<bit-icon
|
||||
<bit-svg
|
||||
*ngSwitchCase="TwoFactorProviderType.Authenticator"
|
||||
[icon]="Icons.TwoFactorAuthAuthenticatorIcon"
|
||||
></bit-icon>
|
||||
<bit-icon
|
||||
[content]="Icons.TwoFactorAuthAuthenticatorIcon"
|
||||
></bit-svg>
|
||||
<bit-svg
|
||||
*ngSwitchCase="TwoFactorProviderType.Email"
|
||||
[icon]="Icons.TwoFactorAuthEmailIcon"
|
||||
></bit-icon>
|
||||
<bit-icon
|
||||
[content]="Icons.TwoFactorAuthEmailIcon"
|
||||
></bit-svg>
|
||||
<bit-svg
|
||||
*ngSwitchCase="TwoFactorProviderType.Duo"
|
||||
[icon]="Icons.TwoFactorAuthDuoIcon"
|
||||
></bit-icon>
|
||||
<bit-icon
|
||||
[content]="Icons.TwoFactorAuthDuoIcon"
|
||||
></bit-svg>
|
||||
<bit-svg
|
||||
*ngSwitchCase="TwoFactorProviderType.Yubikey"
|
||||
[icon]="Icons.TwoFactorAuthYubicoIcon"
|
||||
></bit-icon>
|
||||
<bit-icon
|
||||
[content]="Icons.TwoFactorAuthYubicoIcon"
|
||||
></bit-svg>
|
||||
<bit-svg
|
||||
*ngSwitchCase="TwoFactorProviderType.OrganizationDuo"
|
||||
[icon]="Icons.TwoFactorAuthDuoIcon"
|
||||
></bit-icon>
|
||||
<bit-icon
|
||||
[content]="Icons.TwoFactorAuthDuoIcon"
|
||||
></bit-svg>
|
||||
<bit-svg
|
||||
*ngSwitchCase="TwoFactorProviderType.WebAuthn"
|
||||
[icon]="Icons.TwoFactorAuthWebAuthnIcon"
|
||||
></bit-icon>
|
||||
[content]="Icons.TwoFactorAuthWebAuthnIcon"
|
||||
></bit-svg>
|
||||
</div>
|
||||
{{ provider.name }}
|
||||
<ng-container slot="secondary"> {{ provider.description }} </ng-container>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ItemModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
@@ -39,7 +39,7 @@ export type TwoFactorOptionsDialogResult = {
|
||||
ButtonModule,
|
||||
TypographyModule,
|
||||
ItemModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
],
|
||||
providers: [],
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
>
|
||||
<div class="tw-flex tw-flex-col tw-items-center">
|
||||
<div class="tw-size-16 tw-content-center tw-mb-4">
|
||||
<bit-icon [icon]="Icons.UserVerificationBiometricsIcon"></bit-icon>
|
||||
<bit-svg [content]="Icons.UserVerificationBiometricsIcon"></bit-svg>
|
||||
</div>
|
||||
<p class="tw-font-medium tw-mb-1">{{ "verifyWithBiometrics" | i18n }}</p>
|
||||
<div *ngIf="!biometricsVerificationFailed">
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
CalloutModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@@ -64,7 +64,7 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
|
||||
@@ -180,7 +180,10 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId);
|
||||
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
mockUserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
|
||||
@@ -207,7 +210,10 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
mockUserId,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// trustDeviceIfRequired should be called
|
||||
expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled();
|
||||
|
||||
@@ -120,20 +120,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
@@ -38,15 +37,12 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import {
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PasswordStrengthService,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
@@ -109,6 +105,12 @@ export function identityTokenResponseFactory(
|
||||
token_type: "Bearer",
|
||||
MasterPasswordPolicy: masterPasswordPolicyResponse,
|
||||
UserDecryptionOptions: userDecryptionOptions || defaultUserDecryptionOptionsServerResponse,
|
||||
AccountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: privateKey,
|
||||
publicKey: "PUBLIC_KEY",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,19 +203,7 @@ describe("LoginStrategy", () => {
|
||||
});
|
||||
|
||||
describe("base class", () => {
|
||||
const userKeyBytesLength = 64;
|
||||
const masterKeyBytesLength = 64;
|
||||
let userKey: UserKey;
|
||||
let masterKey: MasterKey;
|
||||
|
||||
beforeEach(() => {
|
||||
userKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(userKeyBytesLength).buffer as CsprngArray,
|
||||
) as UserKey;
|
||||
masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(masterKeyBytesLength).buffer as CsprngArray,
|
||||
) as MasterKey;
|
||||
|
||||
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
|
||||
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
|
||||
mockVaultTimeoutAction,
|
||||
@@ -335,39 +325,6 @@ describe("LoginStrategy", () => {
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("makes a new public and private key for an old account", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
keyService.userKey$.mockReturnValue(new BehaviorSubject<UserKey>(userKey).asObservable());
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
// User symmetric key must be set before the new RSA keypair is generated
|
||||
expect(keyService.setUserKey).toHaveBeenCalled();
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalled();
|
||||
expect(keyService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
keyService.makeKeyPair.mock.invocationCallOrder[0],
|
||||
);
|
||||
|
||||
expect(apiService.postAccountKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if userKey is CoseEncrypt0 (V2 encryption) in createKeyPairForOldAccount", async () => {
|
||||
keyService.userKey$.mockReturnValue(
|
||||
new BehaviorSubject<UserKey>({
|
||||
inner: () => ({ type: 7 }),
|
||||
} as unknown as UserKey).asObservable(),
|
||||
);
|
||||
await expect(passwordLoginStrategy["createKeyPairForOldAccount"](userId)).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Two-factor authentication", () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
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";
|
||||
@@ -33,7 +32,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -265,7 +263,7 @@ export abstract class LoginStrategy {
|
||||
|
||||
await this.setMasterKey(response, userId);
|
||||
await this.setUserKey(response, userId);
|
||||
await this.setPrivateKey(response, userId);
|
||||
await this.setAccountCryptographicState(response, userId);
|
||||
|
||||
// This needs to run after the keys are set because it checks for the existence of the encrypted private key
|
||||
await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
|
||||
@@ -283,7 +281,10 @@ export abstract class LoginStrategy {
|
||||
|
||||
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
protected abstract setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void>;
|
||||
|
||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||
// check on password logins
|
||||
@@ -315,28 +316,6 @@ export abstract class LoginStrategy {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async createKeyPairForOldAccount(userId: UserId) {
|
||||
try {
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
if (userKey === null) {
|
||||
throw new Error("User key is null when creating key pair for old account");
|
||||
}
|
||||
|
||||
if (userKey.inner().type == EncryptionType.CoseEncrypt0) {
|
||||
throw new Error("Cannot create key pair for account on V2 encryption");
|
||||
}
|
||||
|
||||
const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey);
|
||||
if (!privateKey.encryptedString) {
|
||||
throw new Error("Failed to create encrypted private key");
|
||||
}
|
||||
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
|
||||
return privateKey.encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the response from the server when a 2FA is required.
|
||||
* It clears any existing 2FA token, as it's no longer valid, and sets up the necessary data for the 2FA process.
|
||||
|
||||
@@ -216,7 +216,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
userId,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when there are no requirements", async () => {
|
||||
|
||||
@@ -148,20 +148,14 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
|
||||
|
||||
@@ -196,13 +196,14 @@ describe("SsoLoginStrategy", () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
tokenResponse.privateKey = null;
|
||||
tokenResponse.accountKeysResponseModel = null;
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setUserKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets master key encrypted user key for existing SSO users", async () => {
|
||||
|
||||
@@ -335,7 +335,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
@@ -345,20 +345,6 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
|
||||
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
|
||||
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
|
||||
// and so we don't want them falling into the createKeyPairForOldAccount flow
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
} else if (tokenResponse.privateKey) {
|
||||
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
|
||||
// This is just existing TDE users or a TDE offboarder on an untrusted device
|
||||
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
|
||||
@@ -188,7 +188,10 @@ describe("UserApiLoginStrategy", () => {
|
||||
tokenResponse.key,
|
||||
userId,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
||||
|
||||
@@ -79,20 +79,14 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Overridden to save client ID and secret to token service
|
||||
|
||||
@@ -175,6 +175,8 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
WebAuthnPrfOption: {
|
||||
EncryptedPrivateKey: mockEncPrfPrivateKey,
|
||||
EncryptedUserKey: mockEncUserKey,
|
||||
CredentialId: "mockCredentialId",
|
||||
Transports: ["usb", "nfc"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -261,7 +263,10 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
mockPrfPrivateKey,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: idTokenResponse.privateKey } },
|
||||
userId,
|
||||
);
|
||||
|
||||
// Master key and private key should not be set
|
||||
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
|
||||
|
||||
@@ -72,14 +72,15 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
const userDecryptionOptions = idTokenResponse?.userDecryptionOptions;
|
||||
|
||||
if (userDecryptionOptions?.webAuthnPrfOption) {
|
||||
const webAuthnPrfOption = idTokenResponse.userDecryptionOptions?.webAuthnPrfOption;
|
||||
|
||||
const credentials = this.cache.value.credentials;
|
||||
|
||||
// confirm we still have the prf key
|
||||
if (!credentials.prfKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webAuthnPrfOption = userDecryptionOptions.webAuthnPrfOption;
|
||||
|
||||
// decrypt prf encrypted private key
|
||||
const privateKey = await this.encryptService.unwrapDecapsulationKey(
|
||||
webAuthnPrfOption.encryptedPrivateKey,
|
||||
@@ -98,20 +99,14 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Jsonify } from "type-fest";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response";
|
||||
import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response";
|
||||
import { WebAuthnPrfDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/webauthn-prf-decryption-option.response";
|
||||
|
||||
/**
|
||||
* Key Connector decryption options. Intended to be sent to the client for use after authentication.
|
||||
@@ -45,6 +46,61 @@ export class KeyConnectorUserDecryptionOption {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted device decryption options. Intended to be sent to the client for use after authentication.
|
||||
* @see {@link UserDecryptionOptions}
|
||||
*/
|
||||
/**
|
||||
* WebAuthn PRF decryption options. Intended to be sent to the client for use after authentication.
|
||||
* @see {@link UserDecryptionOptions}
|
||||
*/
|
||||
export class WebAuthnPrfUserDecryptionOption {
|
||||
/** The encrypted private key that can be decrypted with the PRF key. */
|
||||
encryptedPrivateKey: string;
|
||||
/** The encrypted user key that can be decrypted with the private key. */
|
||||
encryptedUserKey: string;
|
||||
/** The credential ID for this WebAuthn PRF credential. */
|
||||
credentialId: string;
|
||||
/** The transports supported by this credential. */
|
||||
transports: string[];
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the WebAuthnPrfUserDecryptionOption from a response object.
|
||||
* @param response The WebAuthn PRF user decryption option response object.
|
||||
* @returns A new instance of the WebAuthnPrfUserDecryptionOption or undefined if `response` is nullish.
|
||||
*/
|
||||
static fromResponse(
|
||||
response: WebAuthnPrfDecryptionOptionResponse,
|
||||
): WebAuthnPrfUserDecryptionOption | undefined {
|
||||
if (response == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (!response.encryptedPrivateKey || !response.encryptedUserKey) {
|
||||
return undefined;
|
||||
}
|
||||
const options = new WebAuthnPrfUserDecryptionOption();
|
||||
options.encryptedPrivateKey = response.encryptedPrivateKey.encryptedString;
|
||||
options.encryptedUserKey = response.encryptedUserKey.encryptedString;
|
||||
options.credentialId = response.credentialId;
|
||||
options.transports = response.transports || [];
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of a WebAuthnPrfUserDecryptionOption from a JSON object.
|
||||
* @param obj JSON object to deserialize.
|
||||
* @returns A new instance of the WebAuthnPrfUserDecryptionOption or undefined if `obj` is nullish.
|
||||
*/
|
||||
static fromJSON(
|
||||
obj: Jsonify<WebAuthnPrfUserDecryptionOption>,
|
||||
): WebAuthnPrfUserDecryptionOption | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.assign(new WebAuthnPrfUserDecryptionOption(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted device decryption options. Intended to be sent to the client for use after authentication.
|
||||
* @see {@link UserDecryptionOptions}
|
||||
@@ -104,6 +160,8 @@ export class UserDecryptionOptions {
|
||||
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
|
||||
/** {@link KeyConnectorUserDecryptionOption} */
|
||||
keyConnectorOption?: KeyConnectorUserDecryptionOption;
|
||||
/** Array of {@link WebAuthnPrfUserDecryptionOption} */
|
||||
webAuthnPrfOptions?: WebAuthnPrfUserDecryptionOption[];
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the UserDecryptionOptions from a response object.
|
||||
@@ -134,6 +192,18 @@ export class UserDecryptionOptions {
|
||||
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromResponse(
|
||||
responseOptions.keyConnectorOption,
|
||||
);
|
||||
|
||||
// The IdTokenResponse only returns a single WebAuthn PRF option to support immediate unlock after logging in
|
||||
// with the same PRF passkey.
|
||||
// Since our domain model supports multiple WebAuthn PRF options, we convert the single option into an array.
|
||||
if (responseOptions.webAuthnPrfOption) {
|
||||
const option = WebAuthnPrfUserDecryptionOption.fromResponse(
|
||||
responseOptions.webAuthnPrfOption,
|
||||
);
|
||||
if (option) {
|
||||
decryptionOptions.webAuthnPrfOptions = [option];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"User Decryption Options are required for client initialization. userDecryptionOptions is missing in response.",
|
||||
@@ -158,6 +228,12 @@ export class UserDecryptionOptions {
|
||||
obj?.keyConnectorOption,
|
||||
);
|
||||
|
||||
if (obj?.webAuthnPrfOptions && Array.isArray(obj.webAuthnPrfOptions)) {
|
||||
decryptionOptions.webAuthnPrfOptions = obj.webAuthnPrfOptions
|
||||
.map((option) => WebAuthnPrfUserDecryptionOption.fromJSON(option))
|
||||
.filter((option) => option !== undefined);
|
||||
}
|
||||
|
||||
return decryptionOptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +495,12 @@ describe("LoginStrategyService", () => {
|
||||
KdfParallelism: 1,
|
||||
Key: "KEY",
|
||||
PrivateKey: "PRIVATE_KEY",
|
||||
AccountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: "PRIVATE_KEY",
|
||||
publicKey: "PUBLIC_KEY",
|
||||
},
|
||||
},
|
||||
access_token: "ACCESS_TOKEN",
|
||||
expires_in: 3600,
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
@@ -562,6 +568,12 @@ describe("LoginStrategyService", () => {
|
||||
KdfParallelism: 1,
|
||||
Key: "KEY",
|
||||
PrivateKey: "PRIVATE_KEY",
|
||||
AccountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: "PRIVATE_KEY",
|
||||
publicKey: "PUBLIC_KEY",
|
||||
},
|
||||
},
|
||||
access_token: "ACCESS_TOKEN",
|
||||
expires_in: 3600,
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
@@ -627,6 +639,12 @@ describe("LoginStrategyService", () => {
|
||||
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1,
|
||||
Key: "KEY",
|
||||
PrivateKey: "PRIVATE_KEY",
|
||||
AccountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: "PRIVATE_KEY",
|
||||
publicKey: "PUBLIC_KEY",
|
||||
},
|
||||
},
|
||||
access_token: "ACCESS_TOKEN",
|
||||
expires_in: 3600,
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
@@ -690,6 +708,12 @@ describe("LoginStrategyService", () => {
|
||||
KdfParallelism: 1,
|
||||
Key: "KEY",
|
||||
PrivateKey: "PRIVATE_KEY",
|
||||
AccountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: "PRIVATE_KEY",
|
||||
publicKey: "PUBLIC_KEY",
|
||||
},
|
||||
},
|
||||
access_token: "ACCESS_TOKEN",
|
||||
expires_in: 3600,
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
|
||||
Reference in New Issue
Block a user