diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 1d50bd2cfc7..efaa1720f16 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -11,24 +11,17 @@ import { Subject, takeUntil } from "rxjs"; import zxcvbn from "zxcvbn"; import { PasswordStrengthComponent } from "@bitwarden/angular/shared/components/password-strength/password-strength.component"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { - SymmetricCryptoKey, - UserKey, -} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { AccountRecoveryService } from "../services/account-recovery/account-recovery.service"; + @Component({ selector: "app-reset-password", templateUrl: "reset-password.component.html", @@ -50,13 +43,12 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( + private accountRecoveryService: AccountRecoveryService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private passwordGenerationService: PasswordGenerationServiceAbstraction, private policyService: PolicyService, - private cryptoService: CryptoService, private logService: LogService, - private organizationUserService: OrganizationUserService, private dialogService: DialogService, ) {} @@ -151,64 +143,13 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { } } - // Get user Information (kdf type, kdf iterations, resetPasswordKey, private key) and change password try { - this.formPromise = this.organizationUserService - .getOrganizationUserResetPasswordDetails(this.organizationId, this.id) - .then(async (response) => { - if (response == null) { - throw new Error(this.i18nService.t("resetPasswordDetailsError")); - } - - const kdfType = response.kdf; - const kdfIterations = response.kdfIterations; - const kdfMemory = response.kdfMemory; - const kdfParallelism = response.kdfParallelism; - const resetPasswordKey = response.resetPasswordKey; - const encryptedPrivateKey = response.encryptedPrivateKey; - - // Decrypt Organization's encrypted Private Key with org key - const orgSymKey = await this.cryptoService.getOrgKey(this.organizationId); - const decPrivateKey = await this.cryptoService.decryptToBytes( - new EncString(encryptedPrivateKey), - orgSymKey, - ); - - // Decrypt User's Reset Password Key to get UserKey - const decValue = await this.cryptoService.rsaDecrypt(resetPasswordKey, decPrivateKey); - const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; - - // Create new master key and hash new password - const newMasterKey = await this.cryptoService.makeMasterKey( - this.newPassword, - this.email.trim().toLowerCase(), - kdfType, - new KdfConfig(kdfIterations, kdfMemory, kdfParallelism), - ); - const newMasterKeyHash = await this.cryptoService.hashMasterKey( - this.newPassword, - newMasterKey, - ); - - // Create new encrypted user key for the User - const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey( - newMasterKey, - existingUserKey, - ); - - // Create request - const request = new OrganizationUserResetPasswordRequest(); - request.key = newUserKey[1].encryptedString; - request.newMasterPasswordHash = newMasterKeyHash; - - // Change user's password - return this.organizationUserService.putOrganizationUserResetPassword( - this.organizationId, - this.id, - request, - ); - }); - + this.formPromise = this.accountRecoveryService.resetMasterPassword( + this.newPassword, + this.email, + this.id, + this.organizationId, + ); await this.formPromise; this.platformUtilsService.showToast( "success", @@ -219,6 +160,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { } catch (e) { this.logService.error(e); } + this.formPromise = null; } getStrengthResult(result: zxcvbn.ZXCVBNResult) { diff --git a/apps/web/src/app/admin-console/organizations/members/services/account-recovery/account-recovery.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/account-recovery/account-recovery.service.spec.ts new file mode 100644 index 00000000000..161c6020767 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/account-recovery/account-recovery.service.spec.ts @@ -0,0 +1,216 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; +import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncryptionType, KdfType } from "@bitwarden/common/platform/enums"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + MasterKey, + OrgKey, + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; + +import { AccountRecoveryService } from "./account-recovery.service"; + +describe("AccountRecoveryService", () => { + let sut: AccountRecoveryService; + + let cryptoService: MockProxy; + let encryptService: MockProxy; + let organizationService: MockProxy; + let organizationUserService: MockProxy; + let organizationApiService: MockProxy; + let i18nService: MockProxy; + + beforeAll(() => { + cryptoService = mock(); + encryptService = mock(); + organizationService = mock(); + organizationUserService = mock(); + organizationApiService = mock(); + i18nService = mock(); + + sut = new AccountRecoveryService( + cryptoService, + encryptService, + organizationService, + organizationUserService, + organizationApiService, + i18nService, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should be created", () => { + expect(sut).toBeTruthy(); + }); + + describe("getRecoveryKey", () => { + const mockOrgId = "test-org-id"; + + beforeEach(() => { + organizationApiService.getKeys.mockResolvedValue( + new OrganizationKeysResponse({ + privateKey: "test-private-key", + publicKey: "test-public-key", + }), + ); + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + cryptoService.getUserKey.mockResolvedValue(mockUserKey); + + cryptoService.rsaEncrypt.mockResolvedValue( + new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"), + ); + }); + + it("should return an encrypted user key", async () => { + const encryptedString = await sut.buildRecoveryKey(mockOrgId); + expect(encryptedString).toBeDefined(); + }); + + it("should only use the user key from memory if one is not provided", async () => { + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + await sut.buildRecoveryKey(mockOrgId, mockUserKey); + + expect(cryptoService.getUserKey).not.toHaveBeenCalled(); + }); + + it("should throw an error if the organization keys are null", async () => { + organizationApiService.getKeys.mockResolvedValue(null); + await expect(sut.buildRecoveryKey(mockOrgId)).rejects.toThrow(); + }); + + it("should throw an error if the user key can't be found", async () => { + cryptoService.getUserKey.mockResolvedValue(null); + await expect(sut.buildRecoveryKey(mockOrgId)).rejects.toThrow(); + }); + + it("should rsa encrypt the user key", async () => { + await sut.buildRecoveryKey(mockOrgId); + + expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything()); + }); + }); + + describe("resetMasterPassword", () => { + const mockNewMP = "new-password"; + const mockEmail = "test@example.com"; + const mockOrgUserId = "test-org-user-id"; + const mockOrgId = "test-org-id"; + + beforeEach(() => { + organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue( + new OrganizationUserResetPasswordDetailsResponse({ + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: 5000, + resetPasswordKey: "test-reset-password-key", + encryptedPrivateKey: "test-encrypted-private-key", + }), + ); + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey; + cryptoService.getOrgKey.mockResolvedValue(mockOrgKey); + encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes); + + cryptoService.rsaDecrypt.mockResolvedValue(mockRandomBytes); + const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey; + cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey); + cryptoService.hashMasterKey.mockResolvedValue("test-master-key-hash"); + + const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + cryptoService.encryptUserKeyWithMasterKey.mockResolvedValue([ + mockUserKey, + new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "test-encrypted-user-key"), + ]); + }); + + it("should reset the user's master password", async () => { + await sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId); + expect(organizationUserService.putOrganizationUserResetPassword).toHaveBeenCalled(); + }); + + it("should throw an error if the user details are null", async () => { + organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null); + await expect( + sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), + ).rejects.toThrow(); + }); + + it("should throw an error if the org key is null", async () => { + cryptoService.getOrgKey.mockResolvedValue(null); + await expect( + sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), + ).rejects.toThrow(); + }); + }); + + describe("rotate", () => { + beforeEach(() => { + organizationService.getAll.mockResolvedValue([ + createOrganization("1", "org1"), + createOrganization("2", "org2"), + ]); + organizationApiService.getKeys.mockResolvedValue( + new OrganizationKeysResponse({ + privateKey: "test-private-key", + publicKey: "test-public-key", + }), + ); + cryptoService.rsaEncrypt.mockResolvedValue( + new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"), + ); + }); + + it("should rotate all of the user's recovery key", async () => { + organizationApiService.getKeys.mockResolvedValue( + new OrganizationKeysResponse({ + privateKey: "test-private-key", + publicKey: "test-public-key", + }), + ); + cryptoService.rsaEncrypt.mockResolvedValue( + new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"), + ); + organizationService.getAll.mockResolvedValue([ + createOrganization("1", "org1"), + createOrganization("2", "org2"), + ]); + + await sut.rotate( + new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, + "test-master-password-hash", + ); + + expect( + organizationUserService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledTimes(2); + }); + }); +}); + +function createOrganization(id: string, name: string) { + const org = new Organization(); + org.id = id; + org.name = name; + org.identifier = name; + org.isMember = true; + org.resetPasswordEnrolled = true; + return org; +} diff --git a/apps/web/src/app/admin-console/organizations/members/services/account-recovery/account-recovery.service.ts b/apps/web/src/app/admin-console/organizations/members/services/account-recovery/account-recovery.service.ts new file mode 100644 index 00000000000..ade098b4df2 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/account-recovery/account-recovery.service.ts @@ -0,0 +1,155 @@ +import { Injectable } from "@angular/core"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { + OrganizationUserResetPasswordEnrollmentRequest, + OrganizationUserResetPasswordRequest, +} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +@Injectable({ + providedIn: "root", +}) +export class AccountRecoveryService { + constructor( + private cryptoService: CryptoService, + private encryptService: EncryptService, + private organizationService: OrganizationService, + private organizationUserService: OrganizationUserService, + private organizationApiService: OrganizationApiServiceAbstraction, + private i18nService: I18nService, + ) {} + + /** + * Returns the user key encrypted by the organization's public key. + * Intended for use in enrollment + * @param orgId desired organization + */ + async buildRecoveryKey(orgId: string, userKey?: UserKey): Promise { + // Retrieve Public Key + const orgKeys = await this.organizationApiService.getKeys(orgId); + if (orgKeys == null) { + throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); + } + + const publicKey = Utils.fromB64ToArray(orgKeys.publicKey); + + // RSA Encrypt user key with organization's public key + userKey ??= await this.cryptoService.getUserKey(); + if (userKey == null) { + throw new Error("No user key found"); + } + const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); + + return encryptedKey.encryptedString; + } + + /** + * Sets a user's master password through account recovery. + * Intended for organization admins + * @param newMasterPassword user's new master password + * @param email user's email + * @param orgUserId organization user's id + * @param orgId organization id + */ + async resetMasterPassword( + newMasterPassword: string, + email: string, + orgUserId: string, + orgId: string, + ): Promise { + const response = await this.organizationUserService.getOrganizationUserResetPasswordDetails( + orgId, + orgUserId, + ); + + if (response == null) { + throw new Error(this.i18nService.t("resetPasswordDetailsError")); + } + + // Decrypt Organization's encrypted Private Key with org key + const orgSymKey = await this.cryptoService.getOrgKey(orgId); + if (orgSymKey == null) { + throw new Error("No org key found"); + } + const decPrivateKey = await this.encryptService.decryptToBytes( + new EncString(response.encryptedPrivateKey), + orgSymKey, + ); + + // Decrypt User's Reset Password Key to get UserKey + const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey); + const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; + + // Create new master key and hash new password + const newMasterKey = await this.cryptoService.makeMasterKey( + newMasterPassword, + email.trim().toLowerCase(), + response.kdf, + new KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism), + ); + const newMasterKeyHash = await this.cryptoService.hashMasterKey( + newMasterPassword, + newMasterKey, + ); + + // Create new encrypted user key for the User + const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey( + newMasterKey, + existingUserKey, + ); + + // Create request + const request = new OrganizationUserResetPasswordRequest(); + request.key = newUserKey[1].encryptedString; + request.newMasterPasswordHash = newMasterKeyHash; + + // Change user's password + await this.organizationUserService.putOrganizationUserResetPassword(orgId, orgUserId, request); + } + + /** + * Rotates the user's recovery key for all enrolled organizations. + * @param newUserKey the new user key + * @param masterPasswordHash the user's master password hash (required for user verification) + */ + async rotate(newUserKey: UserKey, masterPasswordHash: string): Promise { + const allOrgs = await this.organizationService.getAll(); + + for (const org of allOrgs) { + // If not already enrolled, skip + if (!org.resetPasswordEnrolled) { + continue; + } + + try { + // Re-enroll - encrypt user key with organization public key + const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey); + + // Create/Execute request + const request = new OrganizationUserResetPasswordEnrollmentRequest(); + request.resetPasswordKey = encryptedKey; + request.masterPasswordHash = masterPasswordHash; + + await this.organizationUserService.putOrganizationUserResetPasswordEnrollment( + org.id, + org.userId, + request, + ); + } catch (e) { + // If enrollment fails, continue to next org + } + } + } +} diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index 09ba827459c..0bb91846534 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -2,20 +2,19 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { Verification } from "@bitwarden/common/auth/types/verification"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { AccountRecoveryService } from "../members/services/account-recovery/account-recovery.service"; + interface EnrollMasterPasswordResetData { organization: Organization; } @@ -34,21 +33,18 @@ export class EnrollMasterPasswordReset { constructor( private dialogRef: DialogRef, @Inject(DIALOG_DATA) protected data: EnrollMasterPasswordResetData, + private accountRecoveryService: AccountRecoveryService, private userVerificationService: UserVerificationService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private cryptoService: CryptoService, private syncService: SyncService, private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, ) { this.organization = data.organization; } submit = async () => { - let toastStringRef = "withdrawPasswordResetSuccess"; - try { await this.userVerificationService .buildRequest( @@ -56,25 +52,10 @@ export class EnrollMasterPasswordReset { OrganizationUserResetPasswordEnrollmentRequest, ) .then(async (request) => { - // Set variables - let keyString: string = null; - - // Retrieve Public Key - const orgKeys = await this.organizationApiService.getKeys(this.organization.id); - if (orgKeys == null) { - throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); - } - - const publicKey = Utils.fromB64ToArray(orgKeys.publicKey); - - // RSA Encrypt user's encKey.key with organization public key - const userKey = await this.cryptoService.getUserKey(); - const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); - keyString = encryptedKey.encryptedString; - toastStringRef = "enrollPasswordResetSuccess"; - // Create request and execute enrollment - request.resetPasswordKey = keyString; + request.resetPasswordKey = await this.accountRecoveryService.buildRecoveryKey( + this.organization.id, + ); await this.organizationUserService.putOrganizationUserResetPasswordEnrollment( this.organization.id, this.organization.userId, @@ -83,7 +64,11 @@ export class EnrollMasterPasswordReset { await this.syncService.fullSync(true); }); - this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef)); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("enrollPasswordResetSuccess"), + ); this.dialogRef.close(); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts index 45ed5b78c98..26b1e44f995 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts @@ -2,12 +2,6 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; -import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -29,6 +23,7 @@ import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { AccountRecoveryService } from "../../admin-console/organizations/members/services/account-recovery/account-recovery.service"; import { EmergencyAccessService } from "../emergency-access"; import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service"; @@ -36,10 +31,8 @@ import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption. describe("migrateFromLegacyEncryptionService", () => { let migrateFromLegacyEncryptionService: MigrateFromLegacyEncryptionService; - const organizationService = mock(); - const organizationApiService = mock(); - const organizationUserService = mock(); const emergencyAccessService = mock(); + const accountRecoveryService = mock(); const apiService = mock(); const encryptService = mock(); const cryptoService = mock(); @@ -55,10 +48,8 @@ describe("migrateFromLegacyEncryptionService", () => { jest.clearAllMocks(); migrateFromLegacyEncryptionService = new MigrateFromLegacyEncryptionService( - organizationService, - organizationApiService, - organizationUserService, emergencyAccessService, + accountRecoveryService, apiService, cryptoService, encryptService, @@ -211,68 +202,6 @@ describe("migrateFromLegacyEncryptionService", () => { expect(emergencyAccessService.rotate).toHaveBeenCalled(); }); }); - - describe("updateAllAdminRecoveryKeys", () => { - let mockMasterPassword: string; - let mockUserKey: UserKey; - - beforeEach(() => { - mockMasterPassword = "mockMasterPassword"; - - const mockRandomBytes = new Uint8Array(64) as CsprngArray; - mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; - - organizationService.getAll.mockResolvedValue([ - createOrganization("1", "Org 1", true), - createOrganization("2", "Org 2", true), - createOrganization("3", "Org 3", false), - createOrganization("4", "Org 4", false), - ]); - - organizationApiService.getKeys.mockImplementation((orgId) => { - return Promise.resolve({ - publicKey: orgId + "mockPublicKey", - privateKey: orgId + "mockPrivateKey", - } as OrganizationKeysResponse); - }); - }); - - it("Only updates organizations that are enrolled in admin recovery", async () => { - await migrateFromLegacyEncryptionService.updateAllAdminRecoveryKeys( - mockMasterPassword, - mockUserKey, - ); - - expect( - organizationUserService.putOrganizationUserResetPasswordEnrollment, - ).toHaveBeenCalledWith( - "1", - expect.any(String), - expect.any(OrganizationUserResetPasswordEnrollmentRequest), - ); - expect( - organizationUserService.putOrganizationUserResetPasswordEnrollment, - ).toHaveBeenCalledWith( - "2", - expect.any(String), - expect.any(OrganizationUserResetPasswordEnrollmentRequest), - ); - expect( - organizationUserService.putOrganizationUserResetPasswordEnrollment, - ).not.toHaveBeenCalledWith( - "3", - expect.any(String), - expect.any(OrganizationUserResetPasswordEnrollmentRequest), - ); - expect( - organizationUserService.putOrganizationUserResetPasswordEnrollment, - ).not.toHaveBeenCalledWith( - "4", - expect.any(String), - expect.any(OrganizationUserResetPasswordEnrollmentRequest), - ); - }); - }); }); function createMockFolder(id: string, name: string): FolderView { @@ -295,12 +224,3 @@ function createMockSend(id: string, name: string): Send { send.name = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, name); return send; } - -function createOrganization(id: string, name: string, resetPasswordEnrolled: boolean) { - const org = new Organization(); - org.id = id; - org.name = name; - org.resetPasswordEnrolled = resetPasswordEnrolled; - org.userId = "mockUserID"; - return org; -} diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts index b4dac039c23..31d5e420272 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts @@ -2,15 +2,10 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; @@ -21,16 +16,15 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request"; import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; +import { AccountRecoveryService } from "../../admin-console/organizations/members/services/account-recovery/account-recovery.service"; import { EmergencyAccessService } from "../emergency-access"; // TODO: PM-3797 - This service should be expanded and used for user key rotations in change-password.component.ts @Injectable() export class MigrateFromLegacyEncryptionService { constructor( - private organizationService: OrganizationService, - private organizationApiService: OrganizationApiServiceAbstraction, - private organizationUserService: OrganizationUserService, private emergencyAccessService: EmergencyAccessService, + private accountRecoveryService: AccountRecoveryService, private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, @@ -112,35 +106,11 @@ export class MigrateFromLegacyEncryptionService { * @param newUserKey The new user key */ async updateAllAdminRecoveryKeys(masterPassword: string, newUserKey: UserKey) { - const allOrgs = await this.organizationService.getAll(); - - for (const org of allOrgs) { - // If not already enrolled, skip - if (!org.resetPasswordEnrolled) { - continue; - } - - // Retrieve public key - const response = await this.organizationApiService.getKeys(org.id); - const publicKey = Utils.fromB64ToArray(response?.publicKey); - - // Re-enroll - encrypt user key with organization public key - const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey); - - // Create/Execute request - const request = new OrganizationUserResetPasswordEnrollmentRequest(); - request.resetPasswordKey = encryptedKey.encryptedString; - request.masterPasswordHash = await this.cryptoService.hashMasterKey( - masterPassword, - await this.cryptoService.getOrDeriveMasterKey(masterPassword), - ); - - await this.organizationUserService.putOrganizationUserResetPasswordEnrollment( - org.id, - org.userId, - request, - ); - } + const masterPasswordHash = await this.cryptoService.hashMasterKey( + masterPassword, + await this.cryptoService.getOrDeriveMasterKey(masterPassword), + ); + await this.accountRecoveryService.rotate(newUserKey, masterPasswordHash); } private async encryptPrivateKey(newUserKey: UserKey): Promise {