1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-05 18:47:52 +01:00

[PM-3143] Trusted device encryption: Refactor reset enroll service (#5869)

* create new reset enrollment service

* refactor: login decryption options according to TODO

* feat: add tests

* PM-3143 - Add override to overriden methods

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
This commit is contained in:
Andreas Coroiu 2023-07-21 18:52:06 +02:00 committed by GitHub
parent 04232fe94a
commit 41ceaddfc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 202 deletions

View File

@ -1,62 +1,20 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
@Component({
selector: "browser-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
constructor(
protected formBuilder: FormBuilder,
protected devicesService: DevicesServiceAbstraction,
protected stateService: StateService,
protected router: Router,
protected activatedRoute: ActivatedRoute,
protected messagingService: MessagingService,
protected tokenService: TokenService,
protected loginService: LoginService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected apiService: ApiService,
protected i18nService: I18nService,
protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService
) {
super(
formBuilder,
devicesService,
stateService,
router,
activatedRoute,
messagingService,
tokenService,
loginService,
organizationApiService,
cryptoService,
organizationUserService,
apiService,
i18nService,
validationService,
deviceTrustCryptoService,
platformUtilsService
);
override async createUser(): Promise<void> {
try {
await super.createUser();
BrowserApi.closeBitwardenExtensionTab();
} catch (error) {
this.validationService.showError(error);
}
}
}

View File

@ -1,62 +1,18 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@Component({
selector: "desktop-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
constructor(
protected formBuilder: FormBuilder,
protected devicesService: DevicesServiceAbstraction,
protected stateService: StateService,
protected router: Router,
protected activatedRoute: ActivatedRoute,
protected messagingService: MessagingService,
protected tokenService: TokenService,
protected loginService: LoginService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected apiService: ApiService,
protected i18nService: I18nService,
protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService
) {
super(
formBuilder,
devicesService,
stateService,
router,
activatedRoute,
messagingService,
tokenService,
loginService,
organizationApiService,
cryptoService,
organizationUserService,
apiService,
i18nService,
validationService,
deviceTrustCryptoService,
platformUtilsService
);
override async createUser(): Promise<void> {
try {
await super.createUser();
await this.router.navigate(["/vault"]);
} catch (error) {
this.validationService.showError(error);
}
}
}

View File

@ -88,7 +88,7 @@
buttonType="primary"
block
class="tw-mb-6"
[bitAction]="createUser"
[bitAction]="createUserAction"
>
{{ "continue" | i18n }}
</button>

View File

@ -1,61 +1,21 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@Component({
selector: "web-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
constructor(
protected formBuilder: FormBuilder,
protected devicesService: DevicesServiceAbstraction,
protected stateService: StateService,
protected router: Router,
protected activatedRoute: ActivatedRoute,
protected messagingService: MessagingService,
protected tokenService: TokenService,
protected loginService: LoginService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected apiService: ApiService,
protected i18nService: I18nService,
protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService
) {
super(
formBuilder,
devicesService,
stateService,
router,
activatedRoute,
messagingService,
tokenService,
loginService,
organizationApiService,
cryptoService,
organizationUserService,
apiService,
i18nService,
validationService,
deviceTrustCryptoService,
platformUtilsService
);
override async createUser(): Promise<void> {
try {
await super.createUser();
await this.router.navigate(["/vault"]);
} catch (error) {
this.validationService.showError(error);
}
}
createUserAction = async (): Promise<void> => {
return this.createUser();
};
}

View File

@ -16,10 +16,10 @@ import {
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -28,10 +28,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
enum State {
NewUser,
@ -88,7 +85,8 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected i18nService: I18nService,
protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService
protected platformUtilsService: PlatformUtilsService,
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction
) {}
async ngOnInit() {
@ -240,7 +238,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
this.router.navigate(["/lock"]);
}
createUser = async () => {
async createUser() {
if (this.data.state !== State.NewUser) {
return;
}
@ -248,11 +246,11 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
// this.loading to support clients without async-actions-support
this.loading = true;
try {
const { userKey, publicKey, privateKey } = await this.cryptoService.initAccount();
const { publicKey, privateKey } = await this.cryptoService.initAccount();
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
await this.apiService.postAccountKeys(keysRequest);
await this.passwordResetEnroll(userKey, publicKey, privateKey);
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
if (this.rememberDeviceForm.value.rememberDevice) {
await this.deviceTrustCryptoService.trustDevice();
@ -262,46 +260,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
} finally {
this.loading = false;
}
};
passwordResetEnroll = async (userKey: UserKey, publicKey: string, privateKey: EncString) => {
if (this.data.state !== State.NewUser) {
return;
}
// this.loading to support clients without async-actions-support
this.loading = true;
try {
const orgKeyResponse = await this.organizationApiService.getKeys(this.data.organizationId);
if (orgKeyResponse == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
// RSA Encrypt user's userKey.key with organization public key
const userId = await this.stateService.getUserId();
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey.buffer);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
this.data.organizationId,
userId,
resetRequest
);
// TODO: On browser this should close the window. But since we might extract
// this logic into a service I'm gonna leaves this as-is untill that
// refactor is done
await this.router.navigate(["/vault"]);
} catch (error) {
this.validationService.showError(error);
} finally {
this.loading = false;
}
};
}
logOut() {
this.loading = true; // to avoid an awkward delay in browser extension

View File

@ -45,6 +45,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
@ -56,6 +57,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
@ -595,6 +597,17 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
useClass: OrganizationUserServiceImplementation,
deps: [ApiServiceAbstraction],
},
{
provide: PasswordResetEnrollmentServiceAbstraction,
useClass: PasswordResetEnrollmentServiceImplementation,
deps: [
OrganizationApiServiceAbstraction,
StateServiceAbstraction,
CryptoServiceAbstraction,
OrganizationUserService,
I18nServiceAbstraction,
],
},
{
provide: ProviderServiceAbstraction,
useClass: ProviderService,

View File

@ -0,0 +1,21 @@
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
export abstract class PasswordResetEnrollmentServiceAbstraction {
/**
* Enroll current user in password reset
* @param organizationId - Organization in which to enroll the user
* @returns Promise that resolves when the user is enrolled
* @throws Error if the action fails
*/
abstract enroll(organizationId: string): Promise<void>;
/**
* Enroll user in password reset
* @param organizationId - Organization in which to enroll the user
* @param userId - User to enroll
* @param userKey - User's symmetric key
* @returns Promise that resolves when the user is enrolled
* @throws Error if the action fails
*/
abstract enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
}

View File

@ -0,0 +1,90 @@
import { mock, MockProxy } from "jest-mock-extended";
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
describe("PasswordResetEnrollmentServiceImplementation", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let stateService: MockProxy<StateService>;
let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let i18nService: MockProxy<I18nService>;
let service: PasswordResetEnrollmentServiceImplementation;
beforeEach(() => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
stateService = mock<StateService>();
cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>();
i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService,
stateService,
cryptoService,
organizationUserService,
i18nService
);
});
describe("enroll", () => {
it("should throw an error if the organization keys are not found", async () => {
organizationApiService.getKeys.mockResolvedValue(null);
i18nService.t.mockReturnValue("resetPasswordOrgKeysError");
const result = () => service.enroll("orgId");
await expect(result).rejects.toThrowError("resetPasswordOrgKeysError");
});
it("should enroll the user when no user id or key is provided", async () => {
const orgKeyResponse = {
publicKey: "publicKey",
privateKey: "privateKey",
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
stateService.getUserId.mockResolvedValue("userId");
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId");
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"orgId",
"userId",
expect.objectContaining({
resetPasswordKey: encryptedKey.encryptedString,
})
);
});
it("should enroll the user when a user id and key is provided", async () => {
const orgKeyResponse = {
publicKey: "publicKey",
privateKey: "privateKey",
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId", "userId", { key: "key" } as any);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"orgId",
"userId",
expect.objectContaining({
resetPasswordKey: encryptedKey.encryptedString,
})
);
});
});
});

View File

@ -0,0 +1,46 @@
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
export class PasswordResetEnrollmentServiceImplementation
implements PasswordResetEnrollmentServiceAbstraction
{
constructor(
protected organizationApiService: OrganizationApiServiceAbstraction,
protected stateService: StateService,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected i18nService: I18nService
) {}
async enroll(organizationId: string): Promise<void>;
async enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise<void> {
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
if (orgKeyResponse == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
userId = userId ?? (await this.stateService.getUserId());
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey.buffer);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
organizationId,
userId,
resetRequest
);
}
}