mirror of
https://github.com/bitwarden/browser.git
synced 2024-10-09 05:57:40 +02:00
28de9439be
* [deps] Autofill: Update prettier to v3 * prettier formatting updates --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
import { MockProxy } from "jest-mock-extended";
|
|
import mock from "jest-mock-extended/lib/Mock";
|
|
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
|
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response";
|
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { EncryptionType, KdfType } from "@bitwarden/common/platform/enums";
|
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
import {
|
|
UserKey,
|
|
SymmetricCryptoKey,
|
|
MasterKey,
|
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
|
|
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
|
|
import { EmergencyAccessType } from "../enums/emergency-access-type";
|
|
import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request";
|
|
import { EmergencyAccessUpdateRequest } from "../request/emergency-access-update.request";
|
|
import {
|
|
EmergencyAccessGranteeDetailsResponse,
|
|
EmergencyAccessTakeoverResponse,
|
|
} from "../response/emergency-access.response";
|
|
|
|
import { EmergencyAccessApiService } from "./emergency-access-api.service";
|
|
import { EmergencyAccessService } from "./emergency-access.service";
|
|
|
|
describe("EmergencyAccessService", () => {
|
|
let emergencyAccessApiService: MockProxy<EmergencyAccessApiService>;
|
|
let apiService: MockProxy<ApiService>;
|
|
let cryptoService: MockProxy<CryptoService>;
|
|
let encryptService: MockProxy<EncryptService>;
|
|
let cipherService: MockProxy<CipherService>;
|
|
let logService: MockProxy<LogService>;
|
|
let emergencyAccessService: EmergencyAccessService;
|
|
|
|
beforeAll(() => {
|
|
emergencyAccessApiService = mock<EmergencyAccessApiService>();
|
|
apiService = mock<ApiService>();
|
|
cryptoService = mock<CryptoService>();
|
|
encryptService = mock<EncryptService>();
|
|
cipherService = mock<CipherService>();
|
|
logService = mock<LogService>();
|
|
|
|
emergencyAccessService = new EmergencyAccessService(
|
|
emergencyAccessApiService,
|
|
apiService,
|
|
cryptoService,
|
|
encryptService,
|
|
cipherService,
|
|
logService,
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
describe("3 step setup process", () => {
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
describe("Step 1: invite", () => {
|
|
it("should post an emergency access invitation", async () => {
|
|
// Arrange
|
|
const email = "test@example.com";
|
|
const type = EmergencyAccessType.View;
|
|
const waitTimeDays = 5;
|
|
|
|
emergencyAccessApiService.postEmergencyAccessInvite.mockResolvedValueOnce();
|
|
|
|
// Act
|
|
await emergencyAccessService.invite(email, type, waitTimeDays);
|
|
|
|
// Assert
|
|
expect(emergencyAccessApiService.postEmergencyAccessInvite).toHaveBeenCalledWith({
|
|
email: email.trim(),
|
|
type: type,
|
|
waitTimeDays: waitTimeDays,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Step 2: accept", () => {
|
|
it("should post an emergency access accept request", async () => {
|
|
// Arrange
|
|
const id = "some-id";
|
|
const token = "some-token";
|
|
|
|
emergencyAccessApiService.postEmergencyAccessAccept.mockResolvedValueOnce();
|
|
|
|
// Act
|
|
await emergencyAccessService.accept(id, token);
|
|
|
|
// Assert
|
|
expect(emergencyAccessApiService.postEmergencyAccessAccept).toHaveBeenCalledWith(id, {
|
|
token: token,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Step 3: confirm", () => {
|
|
it("should post an emergency access confirmation", async () => {
|
|
// Arrange
|
|
const id = "some-id";
|
|
const granteeId = "grantee-id";
|
|
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
|
|
|
const mockPublicKeyB64 = "some-public-key-in-base64";
|
|
|
|
// const publicKey = Utils.fromB64ToArray(publicKeyB64);
|
|
|
|
const mockUserPublicKeyResponse = new UserKeyResponse({
|
|
UserId: granteeId,
|
|
PublicKey: mockPublicKeyB64,
|
|
});
|
|
|
|
const mockUserPublicKeyEncryptedUserKey = new EncString(
|
|
EncryptionType.AesCbc256_HmacSha256_B64,
|
|
"mockUserPublicKeyEncryptedUserKey",
|
|
);
|
|
|
|
cryptoService.getUserKey.mockResolvedValueOnce(mockUserKey);
|
|
apiService.getUserPublicKey.mockResolvedValueOnce(mockUserPublicKeyResponse);
|
|
|
|
cryptoService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
|
|
|
|
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
|
|
|
|
// Act
|
|
await emergencyAccessService.confirm(id, granteeId);
|
|
|
|
// Assert
|
|
expect(emergencyAccessApiService.postEmergencyAccessConfirm).toHaveBeenCalledWith(id, {
|
|
key: mockUserPublicKeyEncryptedUserKey.encryptedString,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("takeover", () => {
|
|
const mockId = "emergencyAccessId";
|
|
const mockEmail = "emergencyAccessEmail";
|
|
const mockName = "emergencyAccessName";
|
|
|
|
it("posts a new password when decryption succeeds", async () => {
|
|
// Arrange
|
|
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
|
|
keyEncrypted: "EncryptedKey",
|
|
kdf: KdfType.PBKDF2_SHA256,
|
|
kdfIterations: 500,
|
|
} as EmergencyAccessTakeoverResponse);
|
|
|
|
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
|
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
|
|
|
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
|
|
|
cryptoService.makeMasterKey.mockResolvedValueOnce(mockMasterKey);
|
|
|
|
const mockMasterKeyHash = "mockMasterKeyHash";
|
|
cryptoService.hashMasterKey.mockResolvedValueOnce(mockMasterKeyHash);
|
|
|
|
// must mock [UserKey, EncString] return from cryptoService.encryptUserKeyWithMasterKey
|
|
// where UserKey is the decrypted grantor user key
|
|
const mockMasterKeyEncryptedUserKey = new EncString(
|
|
EncryptionType.AesCbc256_HmacSha256_B64,
|
|
"mockMasterKeyEncryptedUserKey",
|
|
);
|
|
|
|
const mockUserKey = new SymmetricCryptoKey(mockDecryptedGrantorUserKey) as UserKey;
|
|
|
|
cryptoService.encryptUserKeyWithMasterKey.mockResolvedValueOnce([
|
|
mockUserKey,
|
|
mockMasterKeyEncryptedUserKey,
|
|
]);
|
|
|
|
const expectedEmergencyAccessPasswordRequest = new EmergencyAccessPasswordRequest();
|
|
expectedEmergencyAccessPasswordRequest.newMasterPasswordHash = mockMasterKeyHash;
|
|
expectedEmergencyAccessPasswordRequest.key = mockMasterKeyEncryptedUserKey.encryptedString;
|
|
|
|
// Act
|
|
await emergencyAccessService.takeover(mockId, mockEmail, mockName);
|
|
|
|
// Assert
|
|
expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith(
|
|
mockId,
|
|
expectedEmergencyAccessPasswordRequest,
|
|
);
|
|
});
|
|
|
|
it("should not post a new password if decryption fails", async () => {
|
|
cryptoService.rsaDecrypt.mockResolvedValueOnce(null);
|
|
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
|
|
keyEncrypted: "EncryptedKey",
|
|
kdf: KdfType.PBKDF2_SHA256,
|
|
kdfIterations: 500,
|
|
} as EmergencyAccessTakeoverResponse);
|
|
|
|
await expect(
|
|
emergencyAccessService.takeover(mockId, mockEmail, mockName),
|
|
).rejects.toThrowError("Failed to decrypt grantor key");
|
|
|
|
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("rotate", () => {
|
|
let mockUserKey: UserKey;
|
|
const allowedStatuses = [
|
|
EmergencyAccessStatusType.Confirmed,
|
|
EmergencyAccessStatusType.RecoveryInitiated,
|
|
EmergencyAccessStatusType.RecoveryApproved,
|
|
];
|
|
|
|
const mockEmergencyAccess = {
|
|
data: [
|
|
createMockEmergencyAccess("0", "EA 0", EmergencyAccessStatusType.Invited),
|
|
createMockEmergencyAccess("1", "EA 1", EmergencyAccessStatusType.Accepted),
|
|
createMockEmergencyAccess("2", "EA 2", EmergencyAccessStatusType.Confirmed),
|
|
createMockEmergencyAccess("3", "EA 3", EmergencyAccessStatusType.RecoveryInitiated),
|
|
createMockEmergencyAccess("4", "EA 4", EmergencyAccessStatusType.RecoveryApproved),
|
|
],
|
|
} as ListResponse<EmergencyAccessGranteeDetailsResponse>;
|
|
|
|
beforeEach(() => {
|
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
|
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
|
|
|
emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess);
|
|
apiService.getUserPublicKey.mockResolvedValue({
|
|
userId: "mockUserId",
|
|
publicKey: "mockPublicKey",
|
|
} as UserKeyResponse);
|
|
|
|
cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
|
|
return Promise.resolve(
|
|
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("Only updates emergency accesses with allowed statuses", async () => {
|
|
await emergencyAccessService.rotate(mockUserKey);
|
|
|
|
let expectedCallCount = 0;
|
|
|
|
mockEmergencyAccess.data.forEach((emergencyAccess) => {
|
|
if (allowedStatuses.includes(emergencyAccess.status)) {
|
|
expect(emergencyAccessApiService.putEmergencyAccess).toHaveBeenCalledWith(
|
|
emergencyAccess.id,
|
|
expect.any(EmergencyAccessUpdateRequest),
|
|
);
|
|
expectedCallCount++;
|
|
} else {
|
|
expect(emergencyAccessApiService.putEmergencyAccess).not.toHaveBeenCalledWith(
|
|
emergencyAccess.id,
|
|
expect.any(EmergencyAccessUpdateRequest),
|
|
);
|
|
}
|
|
});
|
|
expect(emergencyAccessApiService.putEmergencyAccess).toHaveBeenCalledTimes(expectedCallCount);
|
|
});
|
|
});
|
|
});
|
|
|
|
function createMockEmergencyAccess(
|
|
id: string,
|
|
name: string,
|
|
status: EmergencyAccessStatusType,
|
|
): EmergencyAccessGranteeDetailsResponse {
|
|
const emergencyAccess = new EmergencyAccessGranteeDetailsResponse({});
|
|
emergencyAccess.id = id;
|
|
emergencyAccess.name = name;
|
|
emergencyAccess.type = 0;
|
|
emergencyAccess.status = status;
|
|
return emergencyAccess;
|
|
}
|