mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[EC-271] Refactor CryptoService - move symmetric encryption to EncryptService (#3042)
* move decryptFromBytes, decryptToBytes, and encryptToBytes from CryptoService to EncryptService * leave redirects in CryptoService * combine encryptService decryptFromBytes and decryptToBytes methods * move parsing logic into EncArrayBuffer * add tests
This commit is contained in:
parent
88ee166e4d
commit
c90eb42ead
@ -1,6 +1,7 @@
|
||||
import * as fet from "node-fetch";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
import { Response } from "@bitwarden/node/cli/models/response";
|
||||
import { FileResponse } from "@bitwarden/node/cli/models/response/fileResponse";
|
||||
@ -24,8 +25,8 @@ export abstract class DownloadCommand {
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key);
|
||||
if (process.env.BW_SERVE === "true") {
|
||||
const res = new FileResponse(Buffer.from(decBuf), fileName);
|
||||
return Response.success(res);
|
||||
|
@ -2,6 +2,7 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
|
||||
export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
@ -63,10 +64,8 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decValue = await this.cryptoService().decryptFromBytes(
|
||||
Utils.fromB64ToArray(encValue).buffer,
|
||||
sessionKey
|
||||
);
|
||||
const encBuf = EncArrayBuffer.fromB64(encValue);
|
||||
const decValue = await this.cryptoService().decryptFromBytes(encBuf, sessionKey);
|
||||
if (decValue == null) {
|
||||
this.logService.info("Failed to decrypt.");
|
||||
return null;
|
||||
|
@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType";
|
||||
import { SendType } from "@bitwarden/common/enums/sendType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { SendAccess } from "@bitwarden/common/models/domain/sendAccess";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
import { SendAccessRequest } from "@bitwarden/common/models/request/sendAccessRequest";
|
||||
@ -109,8 +110,8 @@ export class AccessComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey);
|
||||
this.fileDownloadService.download({
|
||||
fileName: this.send.file.fileName,
|
||||
blobData: decBuf,
|
||||
|
@ -9,6 +9,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { Cipher } from "@bitwarden/common/models/domain/cipher";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/errorResponse";
|
||||
import { AttachmentView } from "@bitwarden/common/models/view/attachmentView";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||
@ -167,12 +168,12 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key);
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
@ -237,12 +238,12 @@ export class AttachmentsComponent implements OnInit {
|
||||
|
||||
try {
|
||||
// 2. Resave
|
||||
const buf = await response.arrayBuffer();
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key);
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
attachment.fileName,
|
||||
|
@ -27,6 +27,7 @@ import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { EventType } from "@bitwarden/common/enums/eventType";
|
||||
import { FieldType } from "@bitwarden/common/enums/fieldType";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/errorResponse";
|
||||
import { AttachmentView } from "@bitwarden/common/models/view/attachmentView";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||
@ -369,12 +370,12 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key);
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
|
76
libs/common/spec/domain/encArrayBuffer.spec.ts
Normal file
76
libs/common/spec/domain/encArrayBuffer.spec.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
|
||||
import { makeStaticByteArray } from "../utils";
|
||||
|
||||
describe("encArrayBuffer", () => {
|
||||
describe("parses the buffer", () => {
|
||||
test.each([
|
||||
[EncryptionType.AesCbc128_HmacSha256_B64, "AesCbc128_HmacSha256_B64"],
|
||||
[EncryptionType.AesCbc256_HmacSha256_B64, "AesCbc256_HmacSha256_B64"],
|
||||
])("with %c%s", (encType: EncryptionType) => {
|
||||
const iv = makeStaticByteArray(16, 10);
|
||||
const mac = makeStaticByteArray(32, 20);
|
||||
// We use the minimum data length of 1 to test the boundary of valid lengths
|
||||
const data = makeStaticByteArray(1, 100);
|
||||
|
||||
const array = new Uint8Array(1 + iv.byteLength + mac.byteLength + data.byteLength);
|
||||
array.set([encType]);
|
||||
array.set(iv, 1);
|
||||
array.set(mac, 1 + iv.byteLength);
|
||||
array.set(data, 1 + iv.byteLength + mac.byteLength);
|
||||
|
||||
const actual = new EncArrayBuffer(array.buffer);
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.macBytes).toEqualBuffer(mac);
|
||||
expect(actual.dataBytes).toEqualBuffer(data);
|
||||
});
|
||||
|
||||
it("with AesCbc256_B64", () => {
|
||||
const encType = EncryptionType.AesCbc256_B64;
|
||||
const iv = makeStaticByteArray(16, 10);
|
||||
// We use the minimum data length of 1 to test the boundary of valid lengths
|
||||
const data = makeStaticByteArray(1, 100);
|
||||
|
||||
const array = new Uint8Array(1 + iv.byteLength + data.byteLength);
|
||||
array.set([encType]);
|
||||
array.set(iv, 1);
|
||||
array.set(data, 1 + iv.byteLength);
|
||||
|
||||
const actual = new EncArrayBuffer(array.buffer);
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.dataBytes).toEqualBuffer(data);
|
||||
expect(actual.macBytes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("throws if the buffer has an invalid length", () => {
|
||||
test.each([
|
||||
[EncryptionType.AesCbc128_HmacSha256_B64, 50, "AesCbc128_HmacSha256_B64"],
|
||||
[EncryptionType.AesCbc256_HmacSha256_B64, 50, "AesCbc256_HmacSha256_B64"],
|
||||
[EncryptionType.AesCbc256_B64, 18, "AesCbc256_B64"],
|
||||
])("with %c%c%s", (encType: EncryptionType, minLength: number) => {
|
||||
// Generate invalid byte array
|
||||
// Minus 1 to leave room for the encType, minus 1 to make it invalid
|
||||
const invalidBytes = makeStaticByteArray(minLength - 2);
|
||||
|
||||
const invalidArray = new Uint8Array(1 + invalidBytes.buffer.byteLength);
|
||||
invalidArray.set([encType]);
|
||||
invalidArray.set(invalidBytes, 1);
|
||||
|
||||
expect(() => new EncArrayBuffer(invalidArray.buffer)).toThrow(
|
||||
"Error parsing encrypted ArrayBuffer"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't parse the buffer if the encryptionType is not supported", () => {
|
||||
// Starting at 9 implicitly gives us an invalid encType
|
||||
const bytes = makeStaticByteArray(50, 9);
|
||||
expect(() => new EncArrayBuffer(bytes)).toThrow("Error parsing encrypted ArrayBuffer");
|
||||
});
|
||||
});
|
25
libs/common/spec/matchers/toEqualBuffer.spec.ts
Normal file
25
libs/common/spec/matchers/toEqualBuffer.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { makeStaticByteArray } from "../utils";
|
||||
|
||||
describe("toEqualBuffer custom matcher", () => {
|
||||
it("matches identical ArrayBuffers", () => {
|
||||
const array = makeStaticByteArray(10);
|
||||
expect(array.buffer).toEqualBuffer(array.buffer);
|
||||
});
|
||||
|
||||
it("matches an identical ArrayBuffer and Uint8Array", () => {
|
||||
const array = makeStaticByteArray(10);
|
||||
expect(array.buffer).toEqualBuffer(array);
|
||||
});
|
||||
|
||||
it("doesn't match different ArrayBuffers", () => {
|
||||
const array1 = makeStaticByteArray(10);
|
||||
const array2 = makeStaticByteArray(10, 11);
|
||||
expect(array1.buffer).not.toEqualBuffer(array2.buffer);
|
||||
});
|
||||
|
||||
it("doesn't match a different ArrayBuffer and Uint8Array", () => {
|
||||
const array1 = makeStaticByteArray(10);
|
||||
const array2 = makeStaticByteArray(10, 11);
|
||||
expect(array1.buffer).not.toEqualBuffer(array2);
|
||||
});
|
||||
});
|
34
libs/common/spec/matchers/toEqualBuffer.ts
Normal file
34
libs/common/spec/matchers/toEqualBuffer.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* The inbuilt toEqual() matcher will always return TRUE when provided with 2 ArrayBuffers.
|
||||
* This is because an ArrayBuffer must be wrapped in a new Uint8Array to be accessible.
|
||||
* This custom matcher will automatically instantiate a new Uint8Array on the recieved value
|
||||
* (and optionally, the expected value) and then call toEqual() on the resulting Uint8Arrays.
|
||||
*/
|
||||
export const toEqualBuffer: jest.CustomMatcher = function (
|
||||
received: ArrayBuffer,
|
||||
expected: Uint8Array | ArrayBuffer
|
||||
) {
|
||||
received = new Uint8Array(received);
|
||||
|
||||
if (expected instanceof ArrayBuffer) {
|
||||
expected = new Uint8Array(expected);
|
||||
}
|
||||
|
||||
if (this.equals(received, expected)) {
|
||||
return {
|
||||
message: () => `expected
|
||||
${received}
|
||||
not to match
|
||||
${expected}`,
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: () => `expected
|
||||
${received}
|
||||
to match
|
||||
${expected}`,
|
||||
pass: false,
|
||||
};
|
||||
};
|
@ -8,7 +8,6 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { Cipher } from "@bitwarden/common/models/domain/cipher";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
@ -16,7 +15,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCry
|
||||
import { CipherService } from "@bitwarden/common/services/cipher.service";
|
||||
|
||||
const ENCRYPTED_TEXT = "This data has been encrypted";
|
||||
const ENCRYPTED_BYTES = new EncArrayBuffer(Utils.fromUtf8ToArray(ENCRYPTED_TEXT).buffer);
|
||||
const ENCRYPTED_BYTES = Substitute.for<EncArrayBuffer>();
|
||||
|
||||
describe("Cipher Service", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
|
38
libs/common/spec/services/crypto.service.spec.ts
Normal file
38
libs/common/spec/services/crypto.service.spec.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { CryptoService } from "@bitwarden/common/services/crypto.service";
|
||||
|
||||
describe("cryptoService", () => {
|
||||
let cryptoService: CryptoService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const encryptService = mock<AbstractEncryptService>();
|
||||
const platformUtilService = mock<PlatformUtilsService>();
|
||||
const logService = mock<LogService>();
|
||||
const stateService = mock<StateService>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(cryptoFunctionService);
|
||||
mockReset(encryptService);
|
||||
mockReset(platformUtilService);
|
||||
mockReset(logService);
|
||||
mockReset(stateService);
|
||||
|
||||
cryptoService = new CryptoService(
|
||||
cryptoFunctionService,
|
||||
encryptService,
|
||||
platformUtilService,
|
||||
logService,
|
||||
stateService
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(cryptoService).not.toBeFalsy();
|
||||
});
|
||||
});
|
163
libs/common/spec/services/encrypt.service.spec.ts
Normal file
163
libs/common/spec/services/encrypt.service.spec.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { mockReset, mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
|
||||
|
||||
import { makeStaticByteArray } from "../utils";
|
||||
|
||||
describe("EncryptService", () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<LogService>();
|
||||
|
||||
let encryptService: EncryptService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(cryptoFunctionService);
|
||||
mockReset(logService);
|
||||
|
||||
encryptService = new EncryptService(cryptoFunctionService, logService, true);
|
||||
});
|
||||
|
||||
describe("encryptToBytes", () => {
|
||||
const plainValue = makeStaticByteArray(16, 1);
|
||||
const iv = makeStaticByteArray(16, 30);
|
||||
const mac = makeStaticByteArray(32, 40);
|
||||
const encryptedData = makeStaticByteArray(20, 50);
|
||||
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.encryptToBytes(plainValue, null)).rejects.toThrow(
|
||||
"No encryption key"
|
||||
);
|
||||
});
|
||||
|
||||
describe("encrypts data", () => {
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService.randomBytes.calledWith(16).mockResolvedValueOnce(iv.buffer);
|
||||
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData.buffer);
|
||||
});
|
||||
|
||||
it("using a key which supports mac", async () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const encType = EncryptionType.AesCbc128_HmacSha256_B64;
|
||||
key.encType = encType;
|
||||
|
||||
key.macKey = makeStaticByteArray(16, 20);
|
||||
|
||||
cryptoFunctionService.hmac.mockResolvedValue(mac.buffer);
|
||||
|
||||
const actual = await encryptService.encryptToBytes(plainValue, key);
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.macBytes).toEqualBuffer(mac);
|
||||
expect(actual.dataBytes).toEqualBuffer(encryptedData);
|
||||
expect(actual.buffer.byteLength).toEqual(
|
||||
1 + iv.byteLength + mac.byteLength + encryptedData.byteLength
|
||||
);
|
||||
});
|
||||
|
||||
it("using a key which doesn't support mac", async () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const encType = EncryptionType.AesCbc256_B64;
|
||||
key.encType = encType;
|
||||
|
||||
key.macKey = null;
|
||||
|
||||
const actual = await encryptService.encryptToBytes(plainValue, key);
|
||||
|
||||
expect(cryptoFunctionService.hmac).not.toBeCalled();
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.macBytes).toBeNull();
|
||||
expect(actual.dataBytes).toEqualBuffer(encryptedData);
|
||||
expect(actual.buffer.byteLength).toEqual(1 + iv.byteLength + encryptedData.byteLength);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptToBytes", () => {
|
||||
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
|
||||
const computedMac = new Uint8Array(1).buffer;
|
||||
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, encType));
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService.hmac.mockResolvedValue(computedMac);
|
||||
});
|
||||
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.decryptToBytes(encBuffer, null)).rejects.toThrow(
|
||||
"No encryption key"
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if no encrypted value is provided", () => {
|
||||
return expect(encryptService.decryptToBytes(null, key)).rejects.toThrow(
|
||||
"Nothing provided for decryption"
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key", async () => {
|
||||
const decryptedBytes = makeStaticByteArray(10, 200).buffer;
|
||||
|
||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1).buffer);
|
||||
cryptoFunctionService.compare.mockResolvedValue(true);
|
||||
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);
|
||||
|
||||
const actual = await encryptService.decryptToBytes(encBuffer, key);
|
||||
|
||||
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
|
||||
expect.toEqualBuffer(encBuffer.dataBytes),
|
||||
expect.toEqualBuffer(encBuffer.ivBytes),
|
||||
expect.toEqualBuffer(key.encKey)
|
||||
);
|
||||
|
||||
expect(actual).toEqualBuffer(decryptedBytes);
|
||||
});
|
||||
|
||||
it("compares macs using CryptoFunctionService", async () => {
|
||||
const expectedMacData = new Uint8Array(
|
||||
encBuffer.ivBytes.byteLength + encBuffer.dataBytes.byteLength
|
||||
);
|
||||
expectedMacData.set(new Uint8Array(encBuffer.ivBytes));
|
||||
expectedMacData.set(new Uint8Array(encBuffer.dataBytes), encBuffer.ivBytes.byteLength);
|
||||
|
||||
await encryptService.decryptToBytes(encBuffer, key);
|
||||
|
||||
expect(cryptoFunctionService.hmac).toBeCalledWith(
|
||||
expect.toEqualBuffer(expectedMacData),
|
||||
key.macKey,
|
||||
"sha256"
|
||||
);
|
||||
|
||||
expect(cryptoFunctionService.compare).toBeCalledWith(
|
||||
expect.toEqualBuffer(encBuffer.macBytes),
|
||||
expect.toEqualBuffer(computedMac)
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null if macs don't match", async () => {
|
||||
cryptoFunctionService.compare.mockResolvedValue(false);
|
||||
|
||||
const actual = await encryptService.decryptToBytes(encBuffer, key);
|
||||
expect(cryptoFunctionService.compare).toHaveBeenCalled();
|
||||
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if encTypes don't match", async () => {
|
||||
key.encType = EncryptionType.AesCbc256_B64;
|
||||
cryptoFunctionService.compare.mockResolvedValue(true);
|
||||
|
||||
const actual = await encryptService.decryptToBytes(encBuffer, key);
|
||||
|
||||
expect(actual).toBeNull();
|
||||
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,27 @@
|
||||
import { webcrypto } from "crypto";
|
||||
|
||||
import { toEqualBuffer } from "./matchers/toEqualBuffer";
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: webcrypto,
|
||||
});
|
||||
|
||||
// Add custom matchers
|
||||
|
||||
expect.extend({
|
||||
toEqualBuffer: toEqualBuffer,
|
||||
});
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
toEqualBuffer(expected: Uint8Array | ArrayBuffer): R;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Expect extends CustomMatchers {}
|
||||
interface Matchers<R> extends CustomMatchers<R> {}
|
||||
interface InverseAsymmetricMatchers extends CustomMatchers {}
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
@ -28,10 +28,10 @@ export function mockEnc(s: string): EncString {
|
||||
return mock;
|
||||
}
|
||||
|
||||
export function makeStaticByteArray(length: number) {
|
||||
export function makeStaticByteArray(length: number, start = 0) {
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = i;
|
||||
arr[i] = start + i;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { IEncrypted } from "../interfaces/IEncrypted";
|
||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||
|
||||
export abstract class AbstractEncryptService {
|
||||
abstract encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string>;
|
||||
abstract encryptToBytes: (
|
||||
plainValue: ArrayBuffer,
|
||||
key?: SymmetricCryptoKey
|
||||
) => Promise<EncArrayBuffer>;
|
||||
abstract decryptToUtf8: (encString: EncString, key: SymmetricCryptoKey) => Promise<string>;
|
||||
abstract decryptToBytes: (encThing: IEncrypted, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export abstract class CryptoService {
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
|
||||
decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
randomNumber: (min: number, max: number) => Promise<number>;
|
||||
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
}
|
||||
|
8
libs/common/src/interfaces/IEncrypted.ts
Normal file
8
libs/common/src/interfaces/IEncrypted.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { EncryptionType } from "../enums/encryptionType";
|
||||
|
||||
export interface IEncrypted {
|
||||
encryptionType?: EncryptionType;
|
||||
dataBytes: ArrayBuffer;
|
||||
macBytes: ArrayBuffer;
|
||||
ivBytes: ArrayBuffer;
|
||||
}
|
@ -1,3 +1,73 @@
|
||||
export class EncArrayBuffer {
|
||||
constructor(public buffer: ArrayBuffer) {}
|
||||
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
|
||||
import { IEncrypted } from "@bitwarden/common/interfaces/IEncrypted";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
const ENC_TYPE_LENGTH = 1;
|
||||
const IV_LENGTH = 16;
|
||||
const MAC_LENGTH = 32;
|
||||
const MIN_DATA_LENGTH = 1;
|
||||
|
||||
export class EncArrayBuffer implements IEncrypted {
|
||||
readonly encryptionType: EncryptionType = null;
|
||||
readonly dataBytes: ArrayBuffer = null;
|
||||
readonly ivBytes: ArrayBuffer = null;
|
||||
readonly macBytes: ArrayBuffer = null;
|
||||
|
||||
constructor(readonly buffer: ArrayBuffer) {
|
||||
const encBytes = new Uint8Array(buffer);
|
||||
const encType = encBytes[0];
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64: {
|
||||
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH + MIN_DATA_LENGTH;
|
||||
if (encBytes.length < minimumLength) {
|
||||
this.throwDecryptionError();
|
||||
}
|
||||
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH).buffer;
|
||||
this.macBytes = encBytes.slice(
|
||||
ENC_TYPE_LENGTH + IV_LENGTH,
|
||||
ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH
|
||||
).buffer;
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH).buffer;
|
||||
break;
|
||||
}
|
||||
case EncryptionType.AesCbc256_B64: {
|
||||
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MIN_DATA_LENGTH;
|
||||
if (encBytes.length < minimumLength) {
|
||||
this.throwDecryptionError();
|
||||
}
|
||||
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH).buffer;
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH).buffer;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.throwDecryptionError();
|
||||
}
|
||||
|
||||
this.encryptionType = encType;
|
||||
}
|
||||
|
||||
private throwDecryptionError() {
|
||||
throw new Error(
|
||||
"Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format."
|
||||
);
|
||||
}
|
||||
|
||||
static async fromResponse(response: {
|
||||
arrayBuffer: () => Promise<ArrayBuffer>;
|
||||
}): Promise<EncArrayBuffer> {
|
||||
const buffer = await response.arrayBuffer();
|
||||
if (buffer == null) {
|
||||
throw new Error("Cannot create EncArrayBuffer from Response - Response is empty");
|
||||
}
|
||||
return new EncArrayBuffer(buffer);
|
||||
}
|
||||
|
||||
static fromB64(b64: string) {
|
||||
const buffer = Utils.fromB64ToArray(b64).buffer;
|
||||
return new EncArrayBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { IEncrypted } from "@bitwarden/common/interfaces/IEncrypted";
|
||||
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { EncryptionType } from "../../enums/encryptionType";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||
|
||||
export class EncString {
|
||||
export class EncString implements IEncrypted {
|
||||
encryptedString?: string;
|
||||
encryptionType?: EncryptionType;
|
||||
decryptedValue?: string;
|
||||
@ -119,4 +121,16 @@ export class EncString {
|
||||
}
|
||||
return this.decryptedValue;
|
||||
}
|
||||
|
||||
get ivBytes(): ArrayBuffer {
|
||||
return this.iv == null ? null : Utils.fromB64ToArray(this.iv).buffer;
|
||||
}
|
||||
|
||||
get macBytes(): ArrayBuffer {
|
||||
return this.mac == null ? null : Utils.fromB64ToArray(this.mac).buffer;
|
||||
}
|
||||
|
||||
get dataBytes(): ArrayBuffer {
|
||||
return this.data == null ? null : Utils.fromB64ToArray(this.data).buffer;
|
||||
}
|
||||
}
|
||||
|
@ -1058,8 +1058,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
throw Error("Failed to download attachment: " + attachmentResponse.status.toString());
|
||||
}
|
||||
|
||||
const buf = await attachmentResponse.arrayBuffer();
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, null);
|
||||
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, null);
|
||||
const key = await this.cryptoService.getOrgKey(organizationId);
|
||||
const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key);
|
||||
|
||||
|
@ -16,7 +16,6 @@ import { EEFLongWordList } from "../misc/wordlist";
|
||||
import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
|
||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { EncryptedObject } from "../models/domain/encryptedObject";
|
||||
import { BaseEncryptedOrganizationKey } from "../models/domain/encryptedOrganizationKey";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
|
||||
@ -513,33 +512,14 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return this.buildEncKey(key, encKey.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated June 22 2022: This method has been moved to encryptService.
|
||||
* All callers should use this service to grab the relevant key and use encryptService for encryption instead.
|
||||
* This method will be removed once all existing code has been refactored to use encryptService.
|
||||
*/
|
||||
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||
key = await this.getKeyForEncryption(key);
|
||||
|
||||
return await this.encryptService.encrypt(plainValue, key);
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
let macLen = 0;
|
||||
if (encValue.mac != null) {
|
||||
macLen = encValue.mac.byteLength;
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
||||
encBytes.set([encValue.key.encType]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
if (encValue.mac != null) {
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes.buffer);
|
||||
key = await this.getKeyForEncryption(key);
|
||||
return this.encryptService.encryptToBytes(plainValue, key);
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
|
||||
@ -608,15 +588,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
const iv = Utils.fromB64ToArray(encString.iv).buffer;
|
||||
const data = Utils.fromB64ToArray(encString.data).buffer;
|
||||
const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null;
|
||||
const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key);
|
||||
if (decipher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decipher;
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encString.encryptionType, keyForEnc);
|
||||
return this.encryptService.decryptToBytes(encString, theKey);
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
||||
@ -625,49 +599,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return await this.encryptService.decryptToUtf8(encString, key);
|
||||
}
|
||||
|
||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
if (encBuf == null) {
|
||||
throw new Error("no encBuf.");
|
||||
async decryptFromBytes(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
if (encBuffer == null) {
|
||||
throw new Error("No buffer provided for decryption.");
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(encBuf);
|
||||
const encType = encBytes[0];
|
||||
let ctBytes: Uint8Array = null;
|
||||
let ivBytes: Uint8Array = null;
|
||||
let macBytes: Uint8Array = null;
|
||||
key = await this.getKeyForEncryption(key);
|
||||
key = await this.resolveLegacyKey(encBuffer.encryptionType, key);
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if (encBytes.length <= 49) {
|
||||
// 1 + 16 + 32 + ctLength
|
||||
return null;
|
||||
}
|
||||
|
||||
ivBytes = encBytes.slice(1, 17);
|
||||
macBytes = encBytes.slice(17, 49);
|
||||
ctBytes = encBytes.slice(49);
|
||||
break;
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if (encBytes.length <= 17) {
|
||||
// 1 + 16 + ctLength
|
||||
return null;
|
||||
}
|
||||
|
||||
ivBytes = encBytes.slice(1, 17);
|
||||
ctBytes = encBytes.slice(17);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.aesDecryptToBytes(
|
||||
encType,
|
||||
ctBytes.buffer,
|
||||
ivBytes.buffer,
|
||||
macBytes != null ? macBytes.buffer : null,
|
||||
key
|
||||
);
|
||||
return this.encryptService.decryptToBytes(encBuffer, key);
|
||||
}
|
||||
|
||||
// EFForg/OpenWireless
|
||||
@ -722,7 +662,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
// ---HELPERS---
|
||||
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
@ -752,67 +693,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
: await this.stateService.getCryptoMasterKeyBiometric({ userId: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated June 22 2022: This method has been moved to encryptService.
|
||||
* All callers should use encryptService instead. This method will be removed once all existing code has been refactored to use encryptService.
|
||||
*/
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = await this.getKeyForEncryption(key);
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
|
||||
|
||||
if (obj.key.macKey != null) {
|
||||
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
|
||||
macData.set(new Uint8Array(obj.iv), 0);
|
||||
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
|
||||
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private async aesDecryptToBytes(
|
||||
encType: EncryptionType,
|
||||
data: ArrayBuffer,
|
||||
iv: ArrayBuffer,
|
||||
mac: ArrayBuffer,
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<ArrayBuffer> {
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encType, keyForEnc);
|
||||
|
||||
if (theKey.macKey != null && mac == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.encType !== encType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.macKey != null && mac != null) {
|
||||
const macData = new Uint8Array(iv.byteLength + data.byteLength);
|
||||
macData.set(new Uint8Array(iv), 0);
|
||||
macData.set(new Uint8Array(data), iv.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(
|
||||
macData.buffer,
|
||||
theKey.macKey,
|
||||
"sha256"
|
||||
);
|
||||
if (computedMac === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logService.error("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey);
|
||||
}
|
||||
|
||||
private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
if (key != null) {
|
||||
return key;
|
||||
|
@ -6,6 +6,8 @@ import { EncryptedObject } from "@bitwarden/common/models/domain/encryptedObject
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { IEncrypted } from "../interfaces/IEncrypted";
|
||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||
|
||||
export class EncryptService implements AbstractEncryptService {
|
||||
constructor(
|
||||
@ -16,7 +18,7 @@ export class EncryptService implements AbstractEncryptService {
|
||||
|
||||
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("no encryption key provided.");
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
if (plainValue == null) {
|
||||
@ -37,8 +39,34 @@ export class EncryptService implements AbstractEncryptService {
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
let macLen = 0;
|
||||
if (encValue.mac != null) {
|
||||
macLen = encValue.mac.byteLength;
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
||||
encBytes.set([encValue.key.encType]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
if (encValue.mac != null) {
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes.buffer);
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||
if (key?.macKey != null && encString?.mac == null) {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
if (key.macKey != null && encString?.mac == null) {
|
||||
this.logService.error("mac required.");
|
||||
return null;
|
||||
}
|
||||
@ -70,6 +98,52 @@ export class EncryptService implements AbstractEncryptService {
|
||||
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
||||
}
|
||||
|
||||
async decryptToBytes(encThing: IEncrypted, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
if (encThing == null) {
|
||||
throw new Error("Nothing provided for decryption.");
|
||||
}
|
||||
|
||||
if (key.macKey != null && encThing.macBytes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encThing.encryptionType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.macKey != null && encThing.macBytes != null) {
|
||||
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
|
||||
macData.set(new Uint8Array(encThing.ivBytes), 0);
|
||||
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(
|
||||
macData.buffer,
|
||||
key.macKey,
|
||||
"sha256"
|
||||
);
|
||||
if (computedMac === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logMacFailed("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
key.encKey
|
||||
);
|
||||
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = key;
|
||||
|
39
package-lock.json
generated
39
package-lock.json
generated
@ -145,6 +145,7 @@
|
||||
"husky": "^7.0.4",
|
||||
"jasmine-core": "^3.7.1",
|
||||
"jasmine-spec-reporter": "^7.0.0",
|
||||
"jest-mock-extended": "^2.0.6",
|
||||
"jest-preset-angular": "^10.1.0",
|
||||
"lint-staged": "^12.4.1",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
@ -29686,6 +29687,19 @@
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-mock-extended": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-2.0.6.tgz",
|
||||
"integrity": "sha512-KoDdjqwIp2phaOWB0hr4O+9HF7hIJx7O+Reefi3iGrNhUpzVkod9UozYTSanvbNvjFYIEH6noA2tIjc8IDpadw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ts-essentials": "^7.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0",
|
||||
"typescript": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-mock/node_modules/@jest/types": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
|
||||
@ -43038,6 +43052,15 @@
|
||||
"node": ">=6.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-essentials": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz",
|
||||
"integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"typescript": ">=3.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "27.1.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
|
||||
@ -68593,6 +68616,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest-mock-extended": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-2.0.6.tgz",
|
||||
"integrity": "sha512-KoDdjqwIp2phaOWB0hr4O+9HF7hIJx7O+Reefi3iGrNhUpzVkod9UozYTSanvbNvjFYIEH6noA2tIjc8IDpadw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ts-essentials": "^7.0.3"
|
||||
}
|
||||
},
|
||||
"jest-pnp-resolver": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||
@ -79023,6 +79055,13 @@
|
||||
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-essentials": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz",
|
||||
"integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"ts-jest": {
|
||||
"version": "27.1.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
|
||||
|
@ -18,7 +18,7 @@
|
||||
"lint:fix": "eslint . --fix",
|
||||
"prettier": "prettier --write .",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:watch": "jest --clearCache && jest --watch",
|
||||
"test:watch:all": "jest --watchAll",
|
||||
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
|
||||
"storybook": "npm run docs:json && start-storybook -p 6006",
|
||||
@ -106,6 +106,7 @@
|
||||
"husky": "^7.0.4",
|
||||
"jasmine-core": "^3.7.1",
|
||||
"jasmine-spec-reporter": "^7.0.0",
|
||||
"jest-mock-extended": "^2.0.6",
|
||||
"jest-preset-angular": "^10.1.0",
|
||||
"lint-staged": "^12.4.1",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
|
Loading…
Reference in New Issue
Block a user