mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-23 16:38:45 +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
|
||||
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";
|
||||
@ -10,6 +12,7 @@ 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";
|
||||
|
||||
@ -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 {
|
||||
@ -59,11 +64,21 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
|
||||
);
|
||||
this.currentState = "assertFailed";
|
||||
} else if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||
await this.router.navigate([this.forcePasswordResetRoute]);
|
||||
} else {
|
||||
await this.router.navigate([this.successRoute]);
|
||||
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]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate([this.successRoute]);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.validationService.showError(this.i18nService.t("invalidPasskeyPleaseTryAgain"));
|
||||
|
@ -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";
|
||||
@ -281,6 +283,10 @@ import {
|
||||
DefaultBiometricStateService,
|
||||
KdfConfigService,
|
||||
DefaultKdfConfigService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
DefaultUserAsymmetricKeysRegenerationService,
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
@ -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({
|
||||
|
@ -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";
|
||||
@ -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,
|
||||
@ -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();
|
||||
/**
|
||||
|
@ -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";
|
||||
@ -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";
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginSuccessHandlerService,
|
||||
PasswordLoginCredentials,
|
||||
RegisterRouteService,
|
||||
} 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 { 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,
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
|
@ -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";
|
||||
|
@ -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 "./register-route.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",
|
||||
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;
|
||||
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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-dynamic": "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",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "13.1.0",
|
||||
@ -4298,9 +4298,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/sdk-internal": {
|
||||
"version": "0.2.0-main.3",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.3.tgz",
|
||||
"integrity": "sha512-CYp98uaVMSFp6nr/QLw+Qw8ttnVtWark/bMpw59OhwMVhrCDKmpCgcR9G4oEdVO11IuFcYZieTBmtOEPhCpGaw==",
|
||||
"version": "0.2.0-main.38",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.38.tgz",
|
||||
"integrity": "sha512-bkN+BZC0YA4k0To8QiT33UTZX8peKDXud8Gzq3UHNPlU/vMSkP3Wn8q0GezzmYN3UNNIWXfreNCS0mJ+S51j/Q==",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"node_modules/@bitwarden/vault": {
|
||||
|
@ -154,7 +154,7 @@
|
||||
"@angular/platform-browser": "17.3.12",
|
||||
"@angular/platform-browser-dynamic": "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",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "13.1.0",
|
||||
|
Loading…
Reference in New Issue
Block a user