mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-19 15:57:42 +01:00
[PM-12700] Add private key regeneration process (#11829)
* 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
This commit is contained in:
parent
c628f541d1
commit
971c157f56
@ -2,7 +2,9 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, OnInit } from "@angular/core";
|
import { Directive, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
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 { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||||
@ -10,6 +12,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
export type State = "assert" | "assertFailed";
|
export type State = "assert" | "assertFailed";
|
||||||
|
|
||||||
@ -26,6 +29,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||||
|
private keyService: KeyService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -59,11 +64,21 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
|||||||
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
|
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
|
||||||
);
|
);
|
||||||
this.currentState = "assertFailed";
|
this.currentState = "assertFailed";
|
||||||
} else if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
return;
|
||||||
await this.router.navigate([this.forcePasswordResetRoute]);
|
|
||||||
} else {
|
|
||||||
await this.router.navigate([this.successRoute]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate([this.successRoute]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ErrorResponse) {
|
if (error instanceof ErrorResponse) {
|
||||||
this.validationService.showError(this.i18nService.t("invalidPasskeyPleaseTryAgain"));
|
this.validationService.showError(this.i18nService.t("invalidPasskeyPleaseTryAgain"));
|
||||||
|
@ -37,6 +37,8 @@ import {
|
|||||||
RegisterRouteService,
|
RegisterRouteService,
|
||||||
AuthRequestApiService,
|
AuthRequestApiService,
|
||||||
DefaultAuthRequestApiService,
|
DefaultAuthRequestApiService,
|
||||||
|
DefaultLoginSuccessHandlerService,
|
||||||
|
LoginSuccessHandlerService,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||||
@ -281,6 +283,10 @@ import {
|
|||||||
DefaultBiometricStateService,
|
DefaultBiometricStateService,
|
||||||
KdfConfigService,
|
KdfConfigService,
|
||||||
DefaultKdfConfigService,
|
DefaultKdfConfigService,
|
||||||
|
UserAsymmetricKeysRegenerationService,
|
||||||
|
DefaultUserAsymmetricKeysRegenerationService,
|
||||||
|
UserAsymmetricKeysRegenerationApiService,
|
||||||
|
DefaultUserAsymmetricKeysRegenerationApiService,
|
||||||
} from "@bitwarden/key-management";
|
} from "@bitwarden/key-management";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
import {
|
import {
|
||||||
@ -1395,6 +1401,29 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultLoginDecryptionOptionsService,
|
useClass: DefaultLoginDecryptionOptionsService,
|
||||||
deps: [MessagingServiceAbstraction],
|
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({
|
@NgModule({
|
||||||
|
@ -37,7 +37,11 @@ import {
|
|||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
ToastService,
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
import {
|
||||||
|
KeyService,
|
||||||
|
BiometricStateService,
|
||||||
|
UserAsymmetricKeysRegenerationService,
|
||||||
|
} from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { PinServiceAbstraction } from "../../common/abstractions";
|
import { PinServiceAbstraction } from "../../common/abstractions";
|
||||||
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
||||||
@ -139,6 +143,7 @@ export class LockV2Component implements OnInit, OnDestroy {
|
|||||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
|
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||||
|
|
||||||
private lockComponentService: LockComponentService,
|
private lockComponentService: LockComponentService,
|
||||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||||
@ -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.
|
// 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.syncService.fullSync(false);
|
||||||
|
|
||||||
|
await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(this.activeAccount.id);
|
||||||
|
|
||||||
if (this.clientType === "browser") {
|
if (this.clientType === "browser") {
|
||||||
const previousUrl = this.lockComponentService.getPreviousUrl();
|
const previousUrl = this.lockComponentService.getPreviousUrl();
|
||||||
/**
|
/**
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
|
LoginSuccessHandlerService,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||||
@ -34,7 +35,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
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 { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
|
|
||||||
@ -88,9 +88,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private syncService: SyncService,
|
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
|
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||||
) {
|
) {
|
||||||
this.clientType = this.platformUtilsService.getClientType();
|
this.clientType = this.platformUtilsService.getClientType();
|
||||||
|
|
||||||
@ -485,7 +485,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
||||||
|
|
||||||
await this.handleSuccessfulLoginNavigation();
|
await this.handleSuccessfulLoginNavigation(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -555,17 +555,17 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
|
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||||
await this.router.navigate(["update-temp-password"]);
|
await this.router.navigate(["update-temp-password"]);
|
||||||
} else {
|
} else {
|
||||||
await this.handleSuccessfulLoginNavigation();
|
await this.handleSuccessfulLoginNavigation(loginResponse.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSuccessfulLoginNavigation() {
|
private async handleSuccessfulLoginNavigation(userId: UserId) {
|
||||||
if (this.flow === Flow.StandardAuthRequest) {
|
if (this.flow === Flow.StandardAuthRequest) {
|
||||||
// Only need to set remembered email on standard login with auth req flow
|
// Only need to set remembered email on standard login with auth req flow
|
||||||
await this.loginEmailService.saveEmailSettings();
|
await this.loginEmailService.saveEmailSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.syncService.fullSync(true);
|
await this.loginSuccessHandlerService.run(userId);
|
||||||
await this.router.navigate(["vault"]);
|
await this.router.navigate(["vault"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|||||||
import {
|
import {
|
||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
|
LoginSuccessHandlerService,
|
||||||
PasswordLoginCredentials,
|
PasswordLoginCredentials,
|
||||||
RegisterRouteService,
|
RegisterRouteService,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
@ -31,7 +32,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import {
|
import {
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
@ -127,11 +127,11 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
private policyService: InternalPolicyService,
|
private policyService: InternalPolicyService,
|
||||||
private registerRouteService: RegisterRouteService,
|
private registerRouteService: RegisterRouteService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private syncService: SyncService,
|
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||||
) {
|
) {
|
||||||
this.clientType = this.platformUtilsService.getClientType();
|
this.clientType = this.platformUtilsService.getClientType();
|
||||||
}
|
}
|
||||||
@ -280,7 +280,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.syncService.fullSync(true);
|
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||||
|
|
||||||
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||||
this.loginEmailService.clearValues();
|
this.loginEmailService.clearValues();
|
||||||
|
@ -5,3 +5,4 @@ export * from "./login-strategy.service";
|
|||||||
export * from "./user-decryption-options.service.abstraction";
|
export * from "./user-decryption-options.service.abstraction";
|
||||||
export * from "./auth-request.service.abstraction";
|
export * from "./auth-request.service.abstraction";
|
||||||
export * from "./login-approval-component.service.abstraction";
|
export * from "./login-approval-component.service.abstraction";
|
||||||
|
export * from "./login-success-handler.service";
|
||||||
|
@ -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>;
|
||||||
|
}
|
@ -6,3 +6,4 @@ export * from "./auth-request/auth-request.service";
|
|||||||
export * from "./auth-request/auth-request-api.service";
|
export * from "./auth-request/auth-request-api.service";
|
||||||
export * from "./register-route.service";
|
export * from "./register-route.service";
|
||||||
export * from "./accounts/lock.service";
|
export * from "./accounts/lock.service";
|
||||||
|
export * from "./login-success-handler/default-login-success-handler.service";
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,7 @@ export enum FeatureFlag {
|
|||||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||||
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
|
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
|
||||||
PM12443RemovePagingLogic = "pm-12443-remove-paging-logic",
|
PM12443RemovePagingLogic = "pm-12443-remove-paging-logic",
|
||||||
|
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||||
@ -94,6 +95,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||||
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
|
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
|
||||||
[FeatureFlag.PM12443RemovePagingLogic]: FALSE,
|
[FeatureFlag.PM12443RemovePagingLogic]: FALSE,
|
||||||
|
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||||
|
|
||||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||||
|
@ -17,3 +17,5 @@ export {
|
|||||||
export { KdfConfigService } from "./abstractions/kdf-config.service";
|
export { KdfConfigService } from "./abstractions/kdf-config.service";
|
||||||
export { DefaultKdfConfigService } from "./kdf-config.service";
|
export { DefaultKdfConfigService } from "./kdf-config.service";
|
||||||
export { KdfType } from "./enums/kdf-type.enum";
|
export { KdfType } from "./enums/kdf-type.enum";
|
||||||
|
|
||||||
|
export * from "./user-asymmetric-key-regeneration";
|
||||||
|
@ -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>;
|
||||||
|
}
|
@ -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>;
|
||||||
|
}
|
@ -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";
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,306 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of, throwError } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
|
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
|
import { makeStaticByteArray, mockEnc } from "@bitwarden/common/spec";
|
||||||
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
|
import { BitwardenClient, VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { KeyService } from "../../abstractions/key.service";
|
||||||
|
import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service";
|
||||||
|
|
||||||
|
import { DefaultUserAsymmetricKeysRegenerationService } from "./default-user-asymmetric-key-regeneration.service";
|
||||||
|
|
||||||
|
function setupVerificationResponse(
|
||||||
|
mockVerificationResponse: VerifyAsymmetricKeysResponse,
|
||||||
|
sdkService: MockProxy<SdkService>,
|
||||||
|
) {
|
||||||
|
const mockKeyPairResponse = {
|
||||||
|
userPublicKey: "userPublicKey",
|
||||||
|
userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey",
|
||||||
|
};
|
||||||
|
|
||||||
|
sdkService.client$ = of({
|
||||||
|
crypto: () => ({
|
||||||
|
verify_asymmetric_keys: jest.fn().mockReturnValue(mockVerificationResponse),
|
||||||
|
make_key_pair: jest.fn().mockReturnValue(mockKeyPairResponse),
|
||||||
|
}),
|
||||||
|
free: jest.fn(),
|
||||||
|
echo: jest.fn(),
|
||||||
|
version: jest.fn(),
|
||||||
|
throw: jest.fn(),
|
||||||
|
catch: jest.fn(),
|
||||||
|
} as unknown as BitwardenClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupUserKeyValidation(
|
||||||
|
cipherService: MockProxy<CipherService>,
|
||||||
|
keyService: MockProxy<KeyService>,
|
||||||
|
encryptService: MockProxy<EncryptService>,
|
||||||
|
) {
|
||||||
|
const cipher = new Cipher();
|
||||||
|
cipher.id = "id";
|
||||||
|
cipher.edit = true;
|
||||||
|
cipher.viewPassword = true;
|
||||||
|
cipher.favorite = false;
|
||||||
|
cipher.name = mockEnc("EncryptedString");
|
||||||
|
cipher.notes = mockEnc("EncryptedString");
|
||||||
|
cipher.key = mockEnc("EncKey");
|
||||||
|
cipherService.getAll.mockResolvedValue([cipher]);
|
||||||
|
encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64));
|
||||||
|
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("regenerateIfNeeded", () => {
|
||||||
|
let sut: DefaultUserAsymmetricKeysRegenerationService;
|
||||||
|
const userId = "userId" as UserId;
|
||||||
|
|
||||||
|
let keyService: MockProxy<KeyService>;
|
||||||
|
let cipherService: MockProxy<CipherService>;
|
||||||
|
let userAsymmetricKeysRegenerationApiService: MockProxy<UserAsymmetricKeysRegenerationApiService>;
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
let sdkService: MockProxy<SdkService>;
|
||||||
|
let apiService: MockProxy<ApiService>;
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
|
let encryptService: MockProxy<EncryptService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
keyService = mock<KeyService>();
|
||||||
|
cipherService = mock<CipherService>();
|
||||||
|
userAsymmetricKeysRegenerationApiService = mock<UserAsymmetricKeysRegenerationApiService>();
|
||||||
|
logService = mock<LogService>();
|
||||||
|
sdkService = mock<SdkService>();
|
||||||
|
apiService = mock<ApiService>();
|
||||||
|
configService = mock<ConfigService>();
|
||||||
|
encryptService = mock<EncryptService>();
|
||||||
|
|
||||||
|
sut = new DefaultUserAsymmetricKeysRegenerationService(
|
||||||
|
keyService,
|
||||||
|
cipherService,
|
||||||
|
userAsymmetricKeysRegenerationApiService,
|
||||||
|
logService,
|
||||||
|
sdkService,
|
||||||
|
apiService,
|
||||||
|
configService,
|
||||||
|
);
|
||||||
|
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
|
const mockEncryptedString = new SymmetricCryptoKey(
|
||||||
|
mockRandomBytes,
|
||||||
|
).toString() as EncryptedString;
|
||||||
|
const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||||
|
keyService.userKey$.mockReturnValue(of(mockUserKey));
|
||||||
|
keyService.userEncryptedPrivateKey$.mockReturnValue(of(mockEncryptedString));
|
||||||
|
apiService.getUserPublicKey.mockResolvedValue({
|
||||||
|
userId: "userId",
|
||||||
|
publicKey: "publicKey",
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call regeneration code when feature flag is off", async () => {
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(keyService.userKey$).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not regenerate when top level error is thrown", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: true,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
keyService.userKey$.mockReturnValue(throwError(() => new Error("error")));
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not regenerate when private key is decryptable and valid", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: true,
|
||||||
|
validPrivateKey: true,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should regenerate when private key is decryptable and invalid", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: true,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set private key on known API error", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: true,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys.mockRejectedValue(
|
||||||
|
new Error("Key regeneration not supported for this user."),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set private key on unknown API error", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: true,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys.mockRejectedValue(
|
||||||
|
new Error("error"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should regenerate when private key is not decryptable and user key is valid", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: false,
|
||||||
|
validPrivateKey: true,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
setupUserKeyValidation(cipherService, keyService, encryptService);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not regenerate when private key is not decryptable and user key is invalid", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: false,
|
||||||
|
validPrivateKey: true,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
setupUserKeyValidation(cipherService, keyService, encryptService);
|
||||||
|
encryptService.decryptToBytes.mockRejectedValue(new Error("error"));
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not regenerate when private key is not decryptable and no ciphers to check", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: false,
|
||||||
|
validPrivateKey: true,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
cipherService.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should regenerate when private key is not decryptable and invalid and user key is valid", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: false,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
setupUserKeyValidation(cipherService, keyService, encryptService);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not regenerate when private key is not decryptable and invalid and user key is invalid", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: false,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
setupUserKeyValidation(cipherService, keyService, encryptService);
|
||||||
|
encryptService.decryptToBytes.mockRejectedValue(new Error("error"));
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not regenerate when private key is not decryptable and invalid and no ciphers to check", async () => {
|
||||||
|
const mockVerificationResponse: VerifyAsymmetricKeysResponse = {
|
||||||
|
privateKeyDecryptable: false,
|
||||||
|
validPrivateKey: false,
|
||||||
|
};
|
||||||
|
setupVerificationResponse(mockVerificationResponse, sdkService);
|
||||||
|
cipherService.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await sut.regenerateIfNeeded(userId);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,158 @@
|
|||||||
|
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
|
||||||
|
import { KeyService } from "../../abstractions/key.service";
|
||||||
|
import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service";
|
||||||
|
import { UserAsymmetricKeysRegenerationService } from "../abstractions/user-asymmetric-key-regeneration.service";
|
||||||
|
|
||||||
|
export class DefaultUserAsymmetricKeysRegenerationService
|
||||||
|
implements UserAsymmetricKeysRegenerationService
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private keyService: KeyService,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private userAsymmetricKeysRegenerationApiService: UserAsymmetricKeysRegenerationApiService,
|
||||||
|
private logService: LogService,
|
||||||
|
private sdkService: SdkService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async regenerateIfNeeded(userId: UserId): Promise<void> {
|
||||||
|
try {
|
||||||
|
const privateKeyRegenerationFlag = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PrivateKeyRegeneration,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (privateKeyRegenerationFlag) {
|
||||||
|
const shouldRegenerate = await this.shouldRegenerate(userId);
|
||||||
|
if (shouldRegenerate) {
|
||||||
|
await this.regenerateUserAsymmetricKeys(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logService.error(
|
||||||
|
"[UserAsymmetricKeyRegeneration] An error occurred: " +
|
||||||
|
error +
|
||||||
|
" Skipping regeneration for the user.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async shouldRegenerate(userId: UserId): Promise<boolean> {
|
||||||
|
const [userKey, userKeyEncryptedPrivateKey, publicKeyResponse] = await firstValueFrom(
|
||||||
|
combineLatest([
|
||||||
|
this.keyService.userKey$(userId),
|
||||||
|
this.keyService.userEncryptedPrivateKey$(userId),
|
||||||
|
this.apiService.getUserPublicKey(userId),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const verificationResponse = await firstValueFrom(
|
||||||
|
this.sdkService.client$.pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (sdk === undefined) {
|
||||||
|
throw new Error("SDK is undefined");
|
||||||
|
}
|
||||||
|
return sdk.crypto().verify_asymmetric_keys({
|
||||||
|
userKey: userKey.keyB64,
|
||||||
|
userPublicKey: publicKeyResponse.publicKey,
|
||||||
|
userKeyEncryptedPrivateKey: userKeyEncryptedPrivateKey,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verificationResponse.privateKeyDecryptable) {
|
||||||
|
if (verificationResponse.validPrivateKey) {
|
||||||
|
// The private key is decryptable and valid. Should not regenerate.
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// The private key is decryptable but not valid so we should regenerate it.
|
||||||
|
this.logService.info(
|
||||||
|
"[UserAsymmetricKeyRegeneration] User's private key is decryptable but not a valid key, attempting regeneration.",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The private isn't decryptable, check to see if we can decrypt something with the userKey.
|
||||||
|
const userKeyCanDecrypt = await this.userKeyCanDecrypt(userKey);
|
||||||
|
if (userKeyCanDecrypt) {
|
||||||
|
this.logService.info(
|
||||||
|
"[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, attempting regeneration.",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logService.warning(
|
||||||
|
"[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, but unable to determine User Symmetric Key validity, skipping regeneration.",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async regenerateUserAsymmetricKeys(userId: UserId): Promise<void> {
|
||||||
|
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||||
|
const makeKeyPairResponse = await firstValueFrom(
|
||||||
|
this.sdkService.client$.pipe(
|
||||||
|
map((sdk) => {
|
||||||
|
if (sdk === undefined) {
|
||||||
|
throw new Error("SDK is undefined");
|
||||||
|
}
|
||||||
|
return sdk.crypto().make_key_pair(userKey.keyB64);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys(
|
||||||
|
makeKeyPairResponse.userPublicKey,
|
||||||
|
new EncString(makeKeyPairResponse.userKeyEncryptedPrivateKey),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.message === "Key regeneration not supported for this user.") {
|
||||||
|
this.logService.info(
|
||||||
|
"[UserAsymmetricKeyRegeneration] Regeneration not supported for this user at this time.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logService.error(
|
||||||
|
"[UserAsymmetricKeyRegeneration] Regeneration error when submitting the request to the server: " +
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.keyService.setPrivateKey(makeKeyPairResponse.userKeyEncryptedPrivateKey, userId);
|
||||||
|
this.logService.info(
|
||||||
|
"[UserAsymmetricKeyRegeneration] User's asymmetric keys successfully regenerated.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async userKeyCanDecrypt(userKey: UserKey): Promise<boolean> {
|
||||||
|
const ciphers = await this.cipherService.getAll();
|
||||||
|
const cipher = ciphers.find((cipher) => cipher.organizationId == null);
|
||||||
|
|
||||||
|
if (cipher != null) {
|
||||||
|
try {
|
||||||
|
await cipher.decrypt(userKey);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logService.error(
|
||||||
|
"[UserAsymmetricKeyRegeneration] User Symmetric Key validation error: " + error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -24,7 +24,7 @@
|
|||||||
"@angular/platform-browser": "17.3.12",
|
"@angular/platform-browser": "17.3.12",
|
||||||
"@angular/platform-browser-dynamic": "17.3.12",
|
"@angular/platform-browser-dynamic": "17.3.12",
|
||||||
"@angular/router": "17.3.12",
|
"@angular/router": "17.3.12",
|
||||||
"@bitwarden/sdk-internal": "0.2.0-main.3",
|
"@bitwarden/sdk-internal": "0.2.0-main.38",
|
||||||
"@electron/fuses": "1.8.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@koa/multer": "3.0.2",
|
"@koa/multer": "3.0.2",
|
||||||
"@koa/router": "13.1.0",
|
"@koa/router": "13.1.0",
|
||||||
@ -4298,9 +4298,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@bitwarden/sdk-internal": {
|
"node_modules/@bitwarden/sdk-internal": {
|
||||||
"version": "0.2.0-main.3",
|
"version": "0.2.0-main.38",
|
||||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.3.tgz",
|
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.38.tgz",
|
||||||
"integrity": "sha512-CYp98uaVMSFp6nr/QLw+Qw8ttnVtWark/bMpw59OhwMVhrCDKmpCgcR9G4oEdVO11IuFcYZieTBmtOEPhCpGaw==",
|
"integrity": "sha512-bkN+BZC0YA4k0To8QiT33UTZX8peKDXud8Gzq3UHNPlU/vMSkP3Wn8q0GezzmYN3UNNIWXfreNCS0mJ+S51j/Q==",
|
||||||
"license": "GPL-3.0"
|
"license": "GPL-3.0"
|
||||||
},
|
},
|
||||||
"node_modules/@bitwarden/vault": {
|
"node_modules/@bitwarden/vault": {
|
||||||
|
@ -154,7 +154,7 @@
|
|||||||
"@angular/platform-browser": "17.3.12",
|
"@angular/platform-browser": "17.3.12",
|
||||||
"@angular/platform-browser-dynamic": "17.3.12",
|
"@angular/platform-browser-dynamic": "17.3.12",
|
||||||
"@angular/router": "17.3.12",
|
"@angular/router": "17.3.12",
|
||||||
"@bitwarden/sdk-internal": "0.2.0-main.3",
|
"@bitwarden/sdk-internal": "0.2.0-main.38",
|
||||||
"@electron/fuses": "1.8.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@koa/multer": "3.0.2",
|
"@koa/multer": "3.0.2",
|
||||||
"@koa/router": "13.1.0",
|
"@koa/router": "13.1.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user