Skip to content

Commit

Permalink
[PM-12700] Add private key regeneration process (#11829)
Browse files Browse the repository at this point in the history
* add user asymmetric key api service

* Add user asymmetric key regen service

* add feature flag

* Add LoginSuccessHandlerService

* add loginSuccessHandlerService to BaseLoginViaWebAuthnComponent

* Only run loginSuccessHandlerService if webAuthn is used for vault decryption.

* Updates for TS strict

* bump SDK version

* swap to combineLatest

* Update abstractions
  • Loading branch information
Thomas-Avery authored Dec 16, 2024
1 parent c628f54 commit 971c157
Show file tree
Hide file tree
Showing 20 changed files with 628 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";

import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { KeyService } from "@bitwarden/key-management";

export type State = "assert" | "assertFailed";

Expand All @@ -26,6 +29,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
private logService: LogService,
private validationService: ValidationService,
private i18nService: I18nService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private keyService: KeyService,
) {}

ngOnInit(): void {
Expand Down Expand Up @@ -59,11 +64,21 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
);
this.currentState = "assertFailed";
} else if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
return;
}

// Only run loginSuccessHandlerService if webAuthn is used for vault decryption.
const userKey = await firstValueFrom(this.keyService.userKey$(authResult.userId));
if (userKey) {
await this.loginSuccessHandlerService.run(authResult.userId);
}

if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
await this.router.navigate([this.forcePasswordResetRoute]);
} else {
await this.router.navigate([this.successRoute]);
return;
}

await this.router.navigate([this.successRoute]);
} catch (error) {
if (error instanceof ErrorResponse) {
this.validationService.showError(this.i18nService.t("invalidPasskeyPleaseTryAgain"));
Expand Down
29 changes: 29 additions & 0 deletions libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
RegisterRouteService,
AuthRequestApiService,
DefaultAuthRequestApiService,
DefaultLoginSuccessHandlerService,
LoginSuccessHandlerService,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
Expand Down Expand Up @@ -281,6 +283,10 @@ import {
DefaultBiometricStateService,
KdfConfigService,
DefaultKdfConfigService,
UserAsymmetricKeysRegenerationService,
DefaultUserAsymmetricKeysRegenerationService,
UserAsymmetricKeysRegenerationApiService,
DefaultUserAsymmetricKeysRegenerationApiService,
} from "@bitwarden/key-management";
import { PasswordRepromptService } from "@bitwarden/vault";
import {
Expand Down Expand Up @@ -1395,6 +1401,29 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultLoginDecryptionOptionsService,
deps: [MessagingServiceAbstraction],
}),
safeProvider({
provide: UserAsymmetricKeysRegenerationApiService,
useClass: DefaultUserAsymmetricKeysRegenerationApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: UserAsymmetricKeysRegenerationService,
useClass: DefaultUserAsymmetricKeysRegenerationService,
deps: [
KeyServiceAbstraction,
CipherServiceAbstraction,
UserAsymmetricKeysRegenerationApiService,
LogService,
SdkService,
ApiServiceAbstraction,
ConfigService,
],
}),
safeProvider({
provide: LoginSuccessHandlerService,
useClass: DefaultLoginSuccessHandlerService,
deps: [SyncService, UserAsymmetricKeysRegenerationService],
}),
];

@NgModule({
Expand Down
9 changes: 8 additions & 1 deletion libs/auth/src/angular/lock/lock.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import {
IconButtonModule,
ToastService,
} from "@bitwarden/components";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import {
KeyService,
BiometricStateService,
UserAsymmetricKeysRegenerationService,
} from "@bitwarden/key-management";

import { PinServiceAbstraction } from "../../common/abstractions";
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
Expand Down Expand Up @@ -139,6 +143,7 @@ export class LockV2Component implements OnInit, OnDestroy {
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,

private lockComponentService: LockComponentService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
Expand Down Expand Up @@ -532,6 +537,8 @@ export class LockV2Component implements OnInit, OnDestroy {
// Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service.
await this.syncService.fullSync(false);

await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(this.activeAccount.id);

if (this.clientType === "browser") {
const previousUrl = this.lockComponentService.getPreviousUrl();
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AuthRequestServiceAbstraction,
LoginEmailServiceAbstraction,
LoginStrategyServiceAbstraction,
LoginSuccessHandlerService,
} from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
Expand All @@ -34,7 +35,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";

Expand Down Expand Up @@ -88,9 +88,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private router: Router,
private syncService: SyncService,
private toastService: ToastService,
private validationService: ValidationService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
) {
this.clientType = this.platformUtilsService.getClientType();

Expand Down Expand Up @@ -485,7 +485,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);

await this.handleSuccessfulLoginNavigation();
await this.handleSuccessfulLoginNavigation(userId);
}

/**
Expand Down Expand Up @@ -555,17 +555,17 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
await this.router.navigate(["update-temp-password"]);
} else {
await this.handleSuccessfulLoginNavigation();
await this.handleSuccessfulLoginNavigation(loginResponse.userId);
}
}

private async handleSuccessfulLoginNavigation() {
private async handleSuccessfulLoginNavigation(userId: UserId) {
if (this.flow === Flow.StandardAuthRequest) {
// Only need to set remembered email on standard login with auth req flow
await this.loginEmailService.saveEmailSettings();
}

await this.syncService.fullSync(true);
await this.loginSuccessHandlerService.run(userId);
await this.router.navigate(["vault"]);
}
}
6 changes: 3 additions & 3 deletions libs/auth/src/angular/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
LoginEmailServiceAbstraction,
LoginStrategyServiceAbstraction,
LoginSuccessHandlerService,
PasswordLoginCredentials,
RegisterRouteService,
} from "@bitwarden/auth/common";
Expand All @@ -31,7 +32,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import {
AsyncActionsModule,
Expand Down Expand Up @@ -127,11 +127,11 @@ export class LoginComponent implements OnInit, OnDestroy {
private policyService: InternalPolicyService,
private registerRouteService: RegisterRouteService,
private router: Router,
private syncService: SyncService,
private toastService: ToastService,
private logService: LogService,
private validationService: ValidationService,
private configService: ConfigService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
) {
this.clientType = this.platformUtilsService.getClientType();
}
Expand Down Expand Up @@ -280,7 +280,7 @@ export class LoginComponent implements OnInit, OnDestroy {
return;
}

await this.syncService.fullSync(true);
await this.loginSuccessHandlerService.run(authResult.userId);

if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
this.loginEmailService.clearValues();
Expand Down
1 change: 1 addition & 0 deletions libs/auth/src/common/abstractions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction";
export * from "./auth-request.service.abstraction";
export * from "./login-approval-component.service.abstraction";
export * from "./login-success-handler.service";
10 changes: 10 additions & 0 deletions libs/auth/src/common/abstractions/login-success-handler.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { UserId } from "@bitwarden/common/types/guid";

export abstract class LoginSuccessHandlerService {
/**
* Runs any service calls required after a successful login.
* Service calls that should be included in this method are only those required to be awaited after successful login.
* @param userId The user id.
*/
abstract run(userId: UserId): Promise<void>;
}
1 change: 1 addition & 0 deletions libs/auth/src/common/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./auth-request/auth-request.service";
export * from "./auth-request/auth-request-api.service";
export * from "./register-route.service";
export * from "./accounts/lock.service";
export * from "./login-success-handler/default-login-success-handler.service";
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management";

import { LoginSuccessHandlerService } from "../../abstractions/login-success-handler.service";

export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerService {
constructor(
private syncService: SyncService,
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
) {}
async run(userId: UserId): Promise<void> {
await this.syncService.fullSync(true);
await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId);
}
}
2 changes: 2 additions & 0 deletions libs/common/src/enums/feature-flag.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum FeatureFlag {
MacOsNativeCredentialSync = "macos-native-credential-sync",
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
PM12443RemovePagingLogic = "pm-12443-remove-paging-logic",
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
}

export type AllowedFeatureFlagTypes = boolean | number | string;
Expand Down Expand Up @@ -94,6 +95,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
[FeatureFlag.PM12443RemovePagingLogic]: FALSE,
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
Expand Down
2 changes: 2 additions & 0 deletions libs/key-management/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export {
export { KdfConfigService } from "./abstractions/kdf-config.service";
export { DefaultKdfConfigService } from "./kdf-config.service";
export { KdfType } from "./enums/kdf-type.enum";

export * from "./user-asymmetric-key-regeneration";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";

export abstract class UserAsymmetricKeysRegenerationApiService {
abstract regenerateUserAsymmetricKeys(
userPublicKey: string,
userKeyEncryptedUserPrivateKey: EncString,
): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { UserId } from "@bitwarden/common/types/guid";

export abstract class UserAsymmetricKeysRegenerationService {
/**
* Attempts to regenerate the user's asymmetric keys if they are invalid.
* Requires the PrivateKeyRegeneration feature flag to be enabled if not the method will do nothing.
* @param userId The user id.
*/
abstract regenerateIfNeeded(userId: UserId): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { UserAsymmetricKeysRegenerationService } from "./abstractions/user-asymmetric-key-regeneration.service";
export { DefaultUserAsymmetricKeysRegenerationService } from "./services/default-user-asymmetric-key-regeneration.service";

export { UserAsymmetricKeysRegenerationApiService } from "./abstractions/user-asymmetric-key-regeneration-api.service";
export { DefaultUserAsymmetricKeysRegenerationApiService } from "./services/default-user-asymmetric-key-regeneration-api.service";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";

export class KeyRegenerationRequest {
userPublicKey: string;
userKeyEncryptedUserPrivateKey: EncString;

constructor(userPublicKey: string, userKeyEncryptedUserPrivateKey: EncString) {
this.userPublicKey = userPublicKey;
this.userKeyEncryptedUserPrivateKey = userKeyEncryptedUserPrivateKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";

import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service";
import { KeyRegenerationRequest } from "../models/requests/key-regeneration.request";

export class DefaultUserAsymmetricKeysRegenerationApiService
implements UserAsymmetricKeysRegenerationApiService
{
constructor(private apiService: ApiService) {}

async regenerateUserAsymmetricKeys(
userPublicKey: string,
userKeyEncryptedUserPrivateKey: EncString,
): Promise<void> {
const request: KeyRegenerationRequest = {
userPublicKey,
userKeyEncryptedUserPrivateKey,
};

await this.apiService.send(
"POST",
"/accounts/key-management/regenerate-keys",
request,
true,
true,
);
}
}
Loading

0 comments on commit 971c157

Please sign in to comment.