1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 13:40:06 +00:00

Refactor symmetric keys and increase test coverage

This commit is contained in:
Bernd Schoolmann
2025-02-26 15:43:31 +01:00
parent e2af7c625c
commit 8cfe331a00
5 changed files with 444 additions and 180 deletions

View File

@@ -13,7 +13,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
Aes256CbcHmacKey,
SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { EncryptService } from "../abstractions/encrypt.service";
@@ -40,11 +43,16 @@ export class EncryptServiceImplementation implements EncryptService {
plainBuf = plainValue;
}
const encObj = await this.aesEncrypt(plainBuf, key);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
return new EncString(encObj.key.encType, data, iv, mac);
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encObj = await this.aesEncrypt(plainBuf, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = Utils.fromBufferToB64(encObj.mac);
return new EncString(innerKey.type, data, iv, mac);
} else {
throw new Error(`Encrypt is not supported for keys of type ${innerKey.type}`);
}
}
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
@@ -52,21 +60,21 @@ export class EncryptServiceImplementation implements EncryptService {
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) {
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encValue = await this.aesEncrypt(plainValue, innerKey);
const macLen = encValue.mac.length;
const encBytes = new Uint8Array(
1 + encValue.iv.byteLength + macLen + encValue.data.byteLength,
);
encBytes.set([innerKey.type]);
encBytes.set(new Uint8Array(encValue.iv), 1);
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);
} else {
throw new Error(`Encrypt is not supported for keys of type ${innerKey.type}`);
}
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
return new EncArrayBuffer(encBytes);
}
async decryptToUtf8(
@@ -78,36 +86,25 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No key provided for decryption.");
}
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encString?.mac == null) {
this.logService.error(
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType),
"Decrypt context: " + decryptContext,
);
return null;
}
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
if (encString.encryptionType !== EncryptionType.AesCbc256_HmacSha256_B64) {
this.logDecryptError(
"Key encryption type does not match payload encryption type",
key.encType,
encString.encryptionType,
decryptContext,
);
return null;
}
if (key.encType !== encString.encryptionType) {
this.logService.error(
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType),
"Decrypt context: " + decryptContext,
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
key,
);
return null;
}
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
key,
);
if (fastParams.macKey != null && fastParams.mac != null) {
const computedMac = await this.cryptoFunctionService.hmacFast(
fastParams.macData,
fastParams.macKey,
@@ -116,18 +113,41 @@ export class EncryptServiceImplementation implements EncryptService {
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
this.logMacFailed(
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType) +
" Decrypt context: " +
decryptContext,
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
key.encType,
encString.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
if (encString.encryptionType !== EncryptionType.AesCbc256_B64) {
this.logDecryptError(
"Key encryption type does not match payload encryption type",
key.encType,
encString.encryptionType,
decryptContext,
);
return null;
}
}
return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams });
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
null,
key,
);
return await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
} else {
throw new Error(`Unsupported encryption type`);
}
}
async decryptToBytes(
@@ -143,72 +163,60 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("Nothing provided for decryption.");
}
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encThing.macBytes == null) {
this.logService.error(
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
const inner = key.inner();
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
if (
encThing.encryptionType !== EncryptionType.AesCbc256_HmacSha256_B64 ||
encThing.macBytes === null
) {
this.logDecryptError(
"Encryption key type mismatch",
key.encType,
encThing.encryptionType,
decryptContext,
);
return null;
}
);
return null;
}
if (key.encType !== encThing.encryptionType) {
this.logService.error(
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
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, key.macKey, "sha256");
if (computedMac === null) {
this.logMacFailed(
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
"MAC comparison failed. Key or payload has changed.",
key.encType,
encThing.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
"cbc",
);
} else if (inner.type === EncryptionType.AesCbc256_B64) {
if (encThing.encryptionType !== EncryptionType.AesCbc256_B64) {
this.logDecryptError(
"Encryption key type mismatch",
key.encType,
encThing.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
"cbc",
);
}
const result = await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
"cbc",
);
return result ?? null;
}
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
@@ -273,25 +281,38 @@ export class EncryptServiceImplementation implements EncryptService {
return Utils.fromBufferToB64(hashArray);
}
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.key = key;
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
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, obj.key.macKey, "sha256");
}
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, key.authenticationKey, "sha256");
return obj;
}
private logMacFailed(msg: string) {
private logDecryptError(
msg: string,
keyEncType: EncryptionType,
dataEncType: EncryptionType,
decryptContext: string,
) {
this.logService.error(
`[Encrypt service] ${msg} Key type ${encryptionTypeName(keyEncType)} Payload type ${encryptionTypeName(dataEncType)} Decrypt context: ${decryptContext}`,
);
}
private logMacFailed(
msg: string,
keyEncType: EncryptionType,
dataEncType: EncryptionType,
decryptContext: string,
) {
if (this.logMacFailures) {
this.logService.error(msg);
this.logDecryptError(msg, keyEncType, dataEncType, decryptContext);
}
}
}

View File

@@ -37,12 +37,29 @@ describe("EncryptService", () => {
const actual = await encryptService.encrypt(null, key);
expect(actual).toBeNull();
});
it("throws for AesCbc256_B64", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
await expect(encryptService.encrypt("data", key)).rejects.toThrow(
"Encrypt is not supported for keys of type 0",
);
});
it("creates an EncString for AesCbc256_HmacSha256_B64", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data";
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(32));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
const result = await encryptService.encrypt(plainValue, key);
expect(cryptoFunctionService.aesEncrypt).toHaveBeenCalled();
expect(cryptoFunctionService.hmac).toHaveBeenCalled();
expect(Utils.fromB64ToArray(result.data).length).toEqual(32);
expect(Utils.fromB64ToArray(result.iv).length).toEqual(16);
expect(Utils.fromB64ToArray(result.mac).length).toEqual(32);
});
});
describe("encryptToBytes", () => {
const plainValue = makeStaticByteArray(16, 1);
const iv = makeStaticByteArray(16, 30);
const encryptedData = makeStaticByteArray(20, 50);
it("throws if no key is provided", () => {
return expect(encryptService.encryptToBytes(plainValue, null)).rejects.toThrow(
@@ -50,35 +67,40 @@ describe("EncryptService", () => {
);
});
describe("encrypts data", () => {
beforeEach(() => {
cryptoFunctionService.randomBytes.calledWith(16).mockResolvedValueOnce(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData);
});
it("throws for an AesCbc256_B64 key", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 100));
key.encType = EncryptionType.AesCbc256_B64;
it("using a key which doesn't support mac", async () => {
const key = mock<SymmetricCryptoKey>();
const encType = EncryptionType.AesCbc256_B64;
key.encType = encType;
await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow(
"Encrypt is not supported for keys of type 0",
);
});
key.macKey = null;
it("encrypts data with provided key and returns correct encbuffer", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const iv = makeStaticByteArray(16, 80);
const mac = makeStaticByteArray(32, 100);
const cipherText = makeStaticByteArray(20, 150);
cryptoFunctionService.randomBytes.mockResolvedValue(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
cryptoFunctionService.hmac.mockResolvedValue(mac);
const actual = await encryptService.encryptToBytes(plainValue, key);
const actual = await encryptService.encryptToBytes(plainValue, key);
const expectedBytes = new Uint8Array(
1 + iv.byteLength + mac.byteLength + cipherText.byteLength,
);
expectedBytes.set([EncryptionType.AesCbc256_HmacSha256_B64]);
expectedBytes.set(iv, 1);
expectedBytes.set(mac, 1 + iv.byteLength);
expectedBytes.set(cipherText, 1 + iv.byteLength + mac.byteLength);
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);
});
expect(actual.buffer).toEqualBuffer(expectedBytes);
});
});
describe("decryptToBytes", () => {
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100));
const computedMac = new Uint8Array(1);
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, encType));
@@ -98,7 +120,28 @@ describe("EncryptService", () => {
);
});
it("decrypts data with provided key", async () => {
it("decrypts data with provided key for AesCbc256Hmac", async () => {
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
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),
"cbc",
);
expect(actual).toEqualBuffer(decryptedBytes);
});
it("decrypts data with provided key for AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
@@ -147,8 +190,8 @@ describe("EncryptService", () => {
expect(actual).toBeNull();
});
it("returns null if encTypes don't match", async () => {
key.encType = EncryptionType.AesCbc256_B64;
it("returns null if key is AesCbc256 but encbuffer is AesCbc256_HMAC", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
cryptoFunctionService.compare.mockResolvedValue(true);
const actual = await encryptService.decryptToBytes(encBuffer, key);
@@ -156,6 +199,16 @@ describe("EncryptService", () => {
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
it("returns null if key is AesCbc256_HMAC but encbuffer is AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
cryptoFunctionService.compare.mockResolvedValue(true);
const buffer = new EncArrayBuffer(makeStaticByteArray(200, EncryptionType.AesCbc256_B64));
const actual = await encryptService.decryptToBytes(buffer, key);
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
});
describe("decryptToUtf8", () => {
@@ -164,17 +217,74 @@ describe("EncryptService", () => {
"No key provided for decryption.",
);
});
it("returns null if key is mac key but encstring has no mac", async () => {
const key = new SymmetricCryptoKey(
makeStaticByteArray(64, 0),
EncryptionType.AesCbc256_HmacSha256_B64,
);
it("decrypts data with provided key for AesCbc256_HmacSha256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(true);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(cryptoFunctionService.compareFast).toHaveBeenCalled();
});
it("decrypts data with provided key for AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(true);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(cryptoFunctionService.compareFast).not.toHaveBeenCalled();
});
it("returns null if key is AesCbc256_HMAC but encstring is AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
it("returns null if key is AesCbc256 but encstring is AesCbc256_HMAC", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
it("returns null if macs don't match", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(false);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
});
});
describe("rsa", () => {
@@ -248,4 +358,31 @@ describe("EncryptService", () => {
});
});
});
describe("hash", () => {
it("hashes a string and returns b64", async () => {
cryptoFunctionService.hash.mockResolvedValue(Uint8Array.from([1, 2, 3]));
expect(await encryptService.hash("test", "sha256")).toEqual("AQID");
expect(cryptoFunctionService.hash).toHaveBeenCalledWith("test", "sha256");
});
});
describe("decryptItems", () => {
it("returns empty array if no items are provided", async () => {
const key = mock<SymmetricCryptoKey>();
const actual = await encryptService.decryptItems(null, key);
expect(actual).toEqual([]);
});
it("returns items decrypted with provided key", async () => {
const key = mock<SymmetricCryptoKey>();
const decryptable = {
decrypt: jest.fn().mockResolvedValue("decrypted"),
};
const items = [decryptable];
const actual = await encryptService.decryptItems(items as any, key);
expect(actual).toEqual(["decrypted"]);
expect(decryptable.decrypt).toHaveBeenCalledWith(key);
});
});
});

View File

@@ -1,10 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export class EncryptedObject {
iv: Uint8Array;
data: Uint8Array;
mac: Uint8Array;
key: SymmetricCryptoKey;
}

View File

@@ -1,4 +1,6 @@
import { makeStaticByteArray } from "../../../../spec";
import { EncryptionType } from "../../enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
@@ -19,10 +21,15 @@ describe("SymmetricCryptoKey", () => {
expect(cryptoKey).toEqual({
encKey: key,
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 0,
encType: EncryptionType.AesCbc256_B64,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: null,
macKeyB64: null,
innerKey: {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
},
});
});
@@ -33,12 +40,17 @@ describe("SymmetricCryptoKey", () => {
expect(cryptoKey).toEqual({
encKey: key.slice(0, 32),
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 2,
encType: EncryptionType.AesCbc256_HmacSha256_B64,
key: key,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
macKey: key.slice(32, 64),
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
innerKey: {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
authenticationKey: key.slice(32),
},
});
});
@@ -47,7 +59,7 @@ describe("SymmetricCryptoKey", () => {
new SymmetricCryptoKey(makeStaticByteArray(30));
};
expect(t).toThrowError("Unable to determine encType.");
expect(t).toThrowError(`Unsupported encType/key length 30`);
});
});
@@ -67,4 +79,39 @@ describe("SymmetricCryptoKey", () => {
expect(actual).toEqual(expected);
expect(actual).toBeInstanceOf(SymmetricCryptoKey);
});
it("inner returns inner key", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const actual = key.inner();
expect(actual).toEqual({
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.encKey,
authenticationKey: key.macKey,
});
});
it("toEncoded returns encoded key for AesCbc256_B64", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const actual = key.toEncoded();
expect(actual).toEqual(key.encKey);
});
it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => {
const keyBytes = makeStaticByteArray(64);
const key = new SymmetricCryptoKey(keyBytes);
const actual = key.toEncoded();
expect(actual).toEqual(keyBytes);
});
it("toBase64 returns base64 encoded key", () => {
const keyBytes = makeStaticByteArray(64);
const keyB64 = Utils.fromBufferToB64(keyBytes);
const key = new SymmetricCryptoKey(keyBytes);
const actual = key.toBase64();
expect(actual).toEqual(keyB64);
});
});

View File

@@ -5,7 +5,25 @@ import { Jsonify } from "type-fest";
import { Utils } from "../../../platform/misc/utils";
import { EncryptionType } from "../../enums";
export type Aes256CbcHmacKey = {
type: EncryptionType.AesCbc256_HmacSha256_B64;
encryptionKey: Uint8Array;
authenticationKey: Uint8Array;
};
export type Aes256CbcKey = {
type: EncryptionType.AesCbc256_B64;
encryptionKey: Uint8Array;
};
/**
* A symmetric crypto key represents a symmetric key usable for symmetric encryption and decryption operations.
* The specific algorithm used is private to the key, and should only be exposed to encrypt service implementations.
* This can be done via `inner()`.
*/
export class SymmetricCryptoKey {
private innerKey: Aes256CbcHmacKey | Aes256CbcKey;
key: Uint8Array;
encKey: Uint8Array;
macKey?: Uint8Array;
@@ -17,38 +35,45 @@ export class SymmetricCryptoKey {
meta: any;
constructor(key: Uint8Array, encType?: EncryptionType) {
/**
* @param key The key in one of the permitted serialization formats
*/
constructor(key: Uint8Array) {
if (key == null) {
throw new Error("Must provide key");
}
if (encType == null) {
if (key.byteLength === 32) {
encType = EncryptionType.AesCbc256_B64;
} else if (key.byteLength === 64) {
encType = EncryptionType.AesCbc256_HmacSha256_B64;
} else {
throw new Error("Unable to determine encType.");
}
}
if (key.byteLength === 32) {
this.innerKey = {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
};
this.encType = EncryptionType.AesCbc256_B64;
this.key = key;
this.keyB64 = Utils.fromBufferToB64(this.key);
this.key = key;
this.encType = encType;
if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) {
this.encKey = key;
this.macKey = null;
} else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) {
this.encKey = key.slice(0, 32);
this.macKey = key.slice(32, 64);
} else {
throw new Error("Unsupported encType/key length.");
}
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
if (this.macKey != null) {
this.macKey = null;
this.macKeyB64 = null;
} else if (key.byteLength === 64) {
this.innerKey = {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
authenticationKey: key.slice(32),
};
this.encType = EncryptionType.AesCbc256_HmacSha256_B64;
this.key = key;
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKey = key.slice(0, 32);
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
this.macKey = key.slice(32);
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
} else {
throw new Error(`Unsupported encType/key length ${key.byteLength}`);
}
}
@@ -57,6 +82,45 @@ export class SymmetricCryptoKey {
return { keyB64: this.keyB64 };
}
/**
* @returns The inner key instance that can be directly used for encryption primitives
*/
inner(): Aes256CbcHmacKey | Aes256CbcKey {
return this.innerKey;
}
/**
* @returns The serialized key in base64 format
*/
toBase64(): string {
return Utils.fromBufferToB64(this.toEncoded());
}
/**
* Serializes the key to a format that can be written to state or shared
* The currently permitted format is:
* - AesCbc256_B64: 32 bytes (the raw key)
* - AesCbc256_HmacSha256_B64: 64 bytes (32 bytes encryption key, 32 bytes authentication key, concatenated)
*
* @returns The serialized key that can be written to state or encrypted and then written to state / shared
*/
toEncoded(): Uint8Array {
if (this.innerKey.type === EncryptionType.AesCbc256_B64) {
return this.innerKey.encryptionKey;
} else if (this.innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encodedKey = new Uint8Array(64);
encodedKey.set(this.innerKey.encryptionKey, 0);
encodedKey.set(this.innerKey.authenticationKey, 32);
return encodedKey;
} else {
throw new Error("Unsupported encryption type.");
}
}
/**
* @param s The serialized key in base64 format
* @returns A SymmetricCryptoKey instance
*/
static fromString(s: string): SymmetricCryptoKey {
if (s == null) {
return null;