mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-18 01:41:27 +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 * as fet from "node-fetch";
|
||||||
|
|
||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
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 { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
import { Response } from "@bitwarden/node/cli/models/response";
|
import { Response } from "@bitwarden/node/cli/models/response";
|
||||||
import { FileResponse } from "@bitwarden/node/cli/models/response/fileResponse";
|
import { FileResponse } from "@bitwarden/node/cli/models/response/fileResponse";
|
||||||
@ -24,8 +25,8 @@ export abstract class DownloadCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buf = await response.arrayBuffer();
|
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key);
|
||||||
if (process.env.BW_SERVE === "true") {
|
if (process.env.BW_SERVE === "true") {
|
||||||
const res = new FileResponse(Buffer.from(decBuf), fileName);
|
const res = new FileResponse(Buffer.from(decBuf), fileName);
|
||||||
return Response.success(res);
|
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 { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
export class NodeEnvSecureStorageService implements AbstractStorageService {
|
export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||||
@ -63,10 +64,8 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decValue = await this.cryptoService().decryptFromBytes(
|
const encBuf = EncArrayBuffer.fromB64(encValue);
|
||||||
Utils.fromB64ToArray(encValue).buffer,
|
const decValue = await this.cryptoService().decryptFromBytes(encBuf, sessionKey);
|
||||||
sessionKey
|
|
||||||
);
|
|
||||||
if (decValue == null) {
|
if (decValue == null) {
|
||||||
this.logService.info("Failed to decrypt.");
|
this.logService.info("Failed to decrypt.");
|
||||||
return null;
|
return null;
|
||||||
|
@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
|||||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType";
|
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType";
|
||||||
import { SendType } from "@bitwarden/common/enums/sendType";
|
import { SendType } from "@bitwarden/common/enums/sendType";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||||
import { SendAccess } from "@bitwarden/common/models/domain/sendAccess";
|
import { SendAccess } from "@bitwarden/common/models/domain/sendAccess";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
import { SendAccessRequest } from "@bitwarden/common/models/request/sendAccessRequest";
|
import { SendAccessRequest } from "@bitwarden/common/models/request/sendAccessRequest";
|
||||||
@ -109,8 +110,8 @@ export class AccessComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buf = await response.arrayBuffer();
|
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
|
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey);
|
||||||
this.fileDownloadService.download({
|
this.fileDownloadService.download({
|
||||||
fileName: this.send.file.fileName,
|
fileName: this.send.file.fileName,
|
||||||
blobData: decBuf,
|
blobData: decBuf,
|
||||||
|
@ -9,6 +9,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { Cipher } from "@bitwarden/common/models/domain/cipher";
|
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 { ErrorResponse } from "@bitwarden/common/models/response/errorResponse";
|
||||||
import { AttachmentView } from "@bitwarden/common/models/view/attachmentView";
|
import { AttachmentView } from "@bitwarden/common/models/view/attachmentView";
|
||||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
@ -167,12 +168,12 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buf = await response.arrayBuffer();
|
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||||
const key =
|
const key =
|
||||||
attachment.key != null
|
attachment.key != null
|
||||||
? attachment.key
|
? attachment.key
|
||||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
: 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({
|
this.fileDownloadService.download({
|
||||||
fileName: attachment.fileName,
|
fileName: attachment.fileName,
|
||||||
blobData: decBuf,
|
blobData: decBuf,
|
||||||
@ -237,12 +238,12 @@ export class AttachmentsComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. Resave
|
// 2. Resave
|
||||||
const buf = await response.arrayBuffer();
|
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||||
const key =
|
const key =
|
||||||
attachment.key != null
|
attachment.key != null
|
||||||
? attachment.key
|
? attachment.key
|
||||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
: 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 = await this.cipherService.saveAttachmentRawWithServer(
|
||||||
this.cipherDomain,
|
this.cipherDomain,
|
||||||
attachment.fileName,
|
attachment.fileName,
|
||||||
|
@ -27,6 +27,7 @@ import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
|
|||||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||||
import { EventType } from "@bitwarden/common/enums/eventType";
|
import { EventType } from "@bitwarden/common/enums/eventType";
|
||||||
import { FieldType } from "@bitwarden/common/enums/fieldType";
|
import { FieldType } from "@bitwarden/common/enums/fieldType";
|
||||||
|
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/errorResponse";
|
import { ErrorResponse } from "@bitwarden/common/models/response/errorResponse";
|
||||||
import { AttachmentView } from "@bitwarden/common/models/view/attachmentView";
|
import { AttachmentView } from "@bitwarden/common/models/view/attachmentView";
|
||||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||||
@ -369,12 +370,12 @@ export class ViewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buf = await response.arrayBuffer();
|
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||||
const key =
|
const key =
|
||||||
attachment.key != null
|
attachment.key != null
|
||||||
? attachment.key
|
? attachment.key
|
||||||
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
: 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({
|
this.fileDownloadService.download({
|
||||||
fileName: attachment.fileName,
|
fileName: attachment.fileName,
|
||||||
blobData: decBuf,
|
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 { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { StateService } from "@bitwarden/common/abstractions/state.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 { Cipher } from "@bitwarden/common/models/domain/cipher";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
import { EncArrayBuffer } from "@bitwarden/common/models/domain/encArrayBuffer";
|
||||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
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";
|
import { CipherService } from "@bitwarden/common/services/cipher.service";
|
||||||
|
|
||||||
const ENCRYPTED_TEXT = "This data has been encrypted";
|
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", () => {
|
describe("Cipher Service", () => {
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
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 { webcrypto } from "crypto";
|
||||||
|
|
||||||
|
import { toEqualBuffer } from "./matchers/toEqualBuffer";
|
||||||
|
|
||||||
Object.defineProperty(window, "crypto", {
|
Object.defineProperty(window, "crypto", {
|
||||||
value: webcrypto,
|
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;
|
return mock;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeStaticByteArray(length: number) {
|
export function makeStaticByteArray(length: number, start = 0) {
|
||||||
const arr = new Uint8Array(length);
|
const arr = new Uint8Array(length);
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
arr[i] = i;
|
arr[i] = start + i;
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
|
import { IEncrypted } from "../interfaces/IEncrypted";
|
||||||
|
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||||
|
|
||||||
export abstract class AbstractEncryptService {
|
export abstract class AbstractEncryptService {
|
||||||
abstract encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString>;
|
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>;
|
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||||
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
|
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>;
|
randomNumber: (min: number, max: number) => Promise<number>;
|
||||||
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
|
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 {
|
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
|
||||||
constructor(public buffer: ArrayBuffer) {}
|
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 { CryptoService } from "../../abstractions/crypto.service";
|
||||||
import { EncryptionType } from "../../enums/encryptionType";
|
import { EncryptionType } from "../../enums/encryptionType";
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||||
|
|
||||||
export class EncString {
|
export class EncString implements IEncrypted {
|
||||||
encryptedString?: string;
|
encryptedString?: string;
|
||||||
encryptionType?: EncryptionType;
|
encryptionType?: EncryptionType;
|
||||||
decryptedValue?: string;
|
decryptedValue?: string;
|
||||||
@ -119,4 +121,16 @@ export class EncString {
|
|||||||
}
|
}
|
||||||
return this.decryptedValue;
|
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());
|
throw Error("Failed to download attachment: " + attachmentResponse.status.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const buf = await attachmentResponse.arrayBuffer();
|
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
|
||||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, null);
|
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, null);
|
||||||
const key = await this.cryptoService.getOrgKey(organizationId);
|
const key = await this.cryptoService.getOrgKey(organizationId);
|
||||||
const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key);
|
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 { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
|
||||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||||
import { EncString } from "../models/domain/encString";
|
import { EncString } from "../models/domain/encString";
|
||||||
import { EncryptedObject } from "../models/domain/encryptedObject";
|
|
||||||
import { BaseEncryptedOrganizationKey } from "../models/domain/encryptedOrganizationKey";
|
import { BaseEncryptedOrganizationKey } from "../models/domain/encryptedOrganizationKey";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||||
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
|
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
|
||||||
@ -513,33 +512,14 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return this.buildEncKey(key, encKey.key);
|
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> {
|
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||||
key = await this.getKeyForEncryption(key);
|
key = await this.getKeyForEncryption(key);
|
||||||
|
|
||||||
return await this.encryptService.encrypt(plainValue, key);
|
return await this.encryptService.encrypt(plainValue, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||||
const encValue = await this.aesEncrypt(plainValue, key);
|
key = await this.getKeyForEncryption(key);
|
||||||
let macLen = 0;
|
return this.encryptService.encryptToBytes(plainValue, key);
|
||||||
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 rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
|
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> {
|
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||||
const iv = Utils.fromB64ToArray(encString.iv).buffer;
|
const keyForEnc = await this.getKeyForEncryption(key);
|
||||||
const data = Utils.fromB64ToArray(encString.data).buffer;
|
const theKey = await this.resolveLegacyKey(encString.encryptionType, keyForEnc);
|
||||||
const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null;
|
return this.encryptService.decryptToBytes(encString, theKey);
|
||||||
const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key);
|
|
||||||
if (decipher == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return decipher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
||||||
@ -625,49 +599,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return await this.encryptService.decryptToUtf8(encString, key);
|
return await this.encryptService.decryptToUtf8(encString, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
async decryptFromBytes(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||||
if (encBuf == null) {
|
if (encBuffer == null) {
|
||||||
throw new Error("no encBuf.");
|
throw new Error("No buffer provided for decryption.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const encBytes = new Uint8Array(encBuf);
|
key = await this.getKeyForEncryption(key);
|
||||||
const encType = encBytes[0];
|
key = await this.resolveLegacyKey(encBuffer.encryptionType, key);
|
||||||
let ctBytes: Uint8Array = null;
|
|
||||||
let ivBytes: Uint8Array = null;
|
|
||||||
let macBytes: Uint8Array = null;
|
|
||||||
|
|
||||||
switch (encType) {
|
return this.encryptService.decryptToBytes(encBuffer, key);
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EFForg/OpenWireless
|
// EFForg/OpenWireless
|
||||||
@ -722,7 +662,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// ---HELPERS---
|
||||||
|
|
||||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||||
@ -752,67 +693,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
: await this.stateService.getCryptoMasterKeyBiometric({ userId: userId });
|
: 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> {
|
private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
return key;
|
return key;
|
||||||
|
@ -6,6 +6,8 @@ import { EncryptedObject } from "@bitwarden/common/models/domain/encryptedObject
|
|||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||||
|
import { IEncrypted } from "../interfaces/IEncrypted";
|
||||||
|
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||||
|
|
||||||
export class EncryptService implements AbstractEncryptService {
|
export class EncryptService implements AbstractEncryptService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -16,7 +18,7 @@ export class EncryptService implements AbstractEncryptService {
|
|||||||
|
|
||||||
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
|
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
throw new Error("no encryption key provided.");
|
throw new Error("No encryption key provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plainValue == null) {
|
if (plainValue == null) {
|
||||||
@ -37,8 +39,34 @@ export class EncryptService implements AbstractEncryptService {
|
|||||||
return new EncString(encObj.key.encType, data, iv, mac);
|
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> {
|
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.");
|
this.logService.error("mac required.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -70,6 +98,52 @@ export class EncryptService implements AbstractEncryptService {
|
|||||||
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
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> {
|
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||||
const obj = new EncryptedObject();
|
const obj = new EncryptedObject();
|
||||||
obj.key = key;
|
obj.key = key;
|
||||||
|
39
package-lock.json
generated
39
package-lock.json
generated
@ -145,6 +145,7 @@
|
|||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jasmine-core": "^3.7.1",
|
"jasmine-core": "^3.7.1",
|
||||||
"jasmine-spec-reporter": "^7.0.0",
|
"jasmine-spec-reporter": "^7.0.0",
|
||||||
|
"jest-mock-extended": "^2.0.6",
|
||||||
"jest-preset-angular": "^10.1.0",
|
"jest-preset-angular": "^10.1.0",
|
||||||
"lint-staged": "^12.4.1",
|
"lint-staged": "^12.4.1",
|
||||||
"mini-css-extract-plugin": "^2.4.5",
|
"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": "^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": {
|
"node_modules/jest-mock/node_modules/@jest/types": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
|
||||||
@ -43038,6 +43052,15 @@
|
|||||||
"node": ">=6.10"
|
"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": {
|
"node_modules/ts-jest": {
|
||||||
"version": "27.1.5",
|
"version": "27.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
|
"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": {
|
"jest-pnp-resolver": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
"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==",
|
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
|
||||||
"dev": true
|
"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": {
|
"ts-jest": {
|
||||||
"version": "27.1.5",
|
"version": "27.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"prettier": "prettier --write .",
|
"prettier": "prettier --write .",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --clearCache && jest --watch",
|
||||||
"test:watch:all": "jest --watchAll",
|
"test:watch:all": "jest --watchAll",
|
||||||
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
|
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
|
||||||
"storybook": "npm run docs:json && start-storybook -p 6006",
|
"storybook": "npm run docs:json && start-storybook -p 6006",
|
||||||
@ -106,6 +106,7 @@
|
|||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jasmine-core": "^3.7.1",
|
"jasmine-core": "^3.7.1",
|
||||||
"jasmine-spec-reporter": "^7.0.0",
|
"jasmine-spec-reporter": "^7.0.0",
|
||||||
|
"jest-mock-extended": "^2.0.6",
|
||||||
"jest-preset-angular": "^10.1.0",
|
"jest-preset-angular": "^10.1.0",
|
||||||
"lint-staged": "^12.4.1",
|
"lint-staged": "^12.4.1",
|
||||||
"mini-css-extract-plugin": "^2.4.5",
|
"mini-css-extract-plugin": "^2.4.5",
|
||||||
|
Loading…
Reference in New Issue
Block a user