-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PM-3797 Part 2] Create Account Recovery Service (#6667)
* create account recovery service * update legacy migration tests * declare account recovery service in migrate component * create account recovery module * remove changes to core organization module * use viewContainerRef to allow dependency injection on modal * fix imports
- Loading branch information
Showing
6 changed files
with
402 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
216 changes: 216 additions & 0 deletions
216
...-console/organizations/members/services/account-recovery/account-recovery.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CryptoService>; | ||
let encryptService: MockProxy<EncryptService>; | ||
let organizationService: MockProxy<OrganizationService>; | ||
let organizationUserService: MockProxy<OrganizationUserService>; | ||
let organizationApiService: MockProxy<OrganizationApiService>; | ||
let i18nService: MockProxy<I18nService>; | ||
|
||
beforeAll(() => { | ||
cryptoService = mock<CryptoService>(); | ||
encryptService = mock<EncryptService>(); | ||
organizationService = mock<OrganizationService>(); | ||
organizationUserService = mock<OrganizationUserService>(); | ||
organizationApiService = mock<OrganizationApiService>(); | ||
i18nService = mock<I18nService>(); | ||
|
||
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; | ||
} |
Oops, something went wrong.