[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:
Thomas Rittson 2022-07-26 11:40:32 +10:00 committed by GitHub
parent 88ee166e4d
commit c90eb42ead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 615 additions and 161 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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,

View 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");
});
});

View 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);
});
});

View 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,
};
};

View File

@ -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>;

View 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();
});
});

View 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();
});
});
});

View File

@ -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 */

View File

@ -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;
}

View File

@ -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>;
}

View File

@ -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>;
}

View File

@ -0,0 +1,8 @@
import { EncryptionType } from "../enums/encryptionType";
export interface IEncrypted {
encryptionType?: EncryptionType;
dataBytes: ArrayBuffer;
macBytes: ArrayBuffer;
ivBytes: ArrayBuffer;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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
View File

@ -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",

View File

@ -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",