From 394ed9199320e71772eb4c33145d20dc47298347 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 5 Mar 2025 16:39:51 -0800 Subject: [PATCH] Allow for easy creation of `EncArrayBuffer` These are to support SDK processing of `Encrypted` interface items. --- .../enums/encryption-type.enum.spec.ts | 15 +++ .../platform/enums/encryption-type.enum.ts | 13 +++ .../models/domain/enc-array-buffer.spec.ts | 69 +++++++++++- .../models/domain/enc-array-buffer.ts | 100 ++++++++++++++---- 4 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 libs/common/src/platform/enums/encryption-type.enum.spec.ts diff --git a/libs/common/src/platform/enums/encryption-type.enum.spec.ts b/libs/common/src/platform/enums/encryption-type.enum.spec.ts new file mode 100644 index 00000000000..54fcd2e1227 --- /dev/null +++ b/libs/common/src/platform/enums/encryption-type.enum.spec.ts @@ -0,0 +1,15 @@ +import { + AsymmetricEncryptionTypes, + EncryptionType, + SymmetricEncryptionTypes, +} from "./encryption-type.enum"; + +describe("EncryptionType", () => { + it("classifies all types as symmetric or asymmetric", () => { + const nSymmetric = SymmetricEncryptionTypes.length; + const nAsymmetric = AsymmetricEncryptionTypes.length; + const nTotal = nSymmetric + nAsymmetric; + // enums are indexable by string and number + expect(Object.keys(EncryptionType).length).toEqual(nTotal * 2); + }); +}); diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts index a0ffe679279..bc820fe4889 100644 --- a/libs/common/src/platform/enums/encryption-type.enum.ts +++ b/libs/common/src/platform/enums/encryption-type.enum.ts @@ -8,6 +8,19 @@ export enum EncryptionType { Rsa2048_OaepSha1_HmacSha256_B64 = 6, } +export const SymmetricEncryptionTypes = [ + EncryptionType.AesCbc256_B64, + EncryptionType.AesCbc128_HmacSha256_B64, + EncryptionType.AesCbc256_HmacSha256_B64, +] as const; + +export const AsymmetricEncryptionTypes = [ + EncryptionType.Rsa2048_OaepSha256_B64, + EncryptionType.Rsa2048_OaepSha1_B64, + EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64, + EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64, +] as const; + export function encryptionTypeToString(encryptionType: EncryptionType): string { if (encryptionType in EncryptionType) { return EncryptionType[encryptionType]; diff --git a/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts b/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts index 45a45ffe087..1259fc6326e 100644 --- a/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts +++ b/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts @@ -1,5 +1,10 @@ import { makeStaticByteArray } from "../../../../spec"; -import { EncryptionType } from "../../enums"; +import { + EncryptionType, + SymmetricEncryptionTypes, + AsymmetricEncryptionTypes, + encryptionTypeToString, +} from "../../enums"; import { EncArrayBuffer } from "./enc-array-buffer"; @@ -71,4 +76,66 @@ describe("encArrayBuffer", () => { const bytes = makeStaticByteArray(50, 9); expect(() => new EncArrayBuffer(bytes)).toThrow("Error parsing encrypted ArrayBuffer"); }); + + describe("fromParts factory", () => { + const plainValue = makeStaticByteArray(16, 1); + + it("throws if required data is null", () => { + expect(() => + EncArrayBuffer.fromParts(EncryptionType.AesCbc128_HmacSha256_B64, plainValue, null!, null), + ).toThrow("encryptionType, iv, and data must be provided"); + expect(() => + EncArrayBuffer.fromParts(EncryptionType.AesCbc128_HmacSha256_B64, null!, plainValue, null), + ).toThrow("encryptionType, iv, and data must be provided"); + expect(() => EncArrayBuffer.fromParts(null!, plainValue, plainValue, null)).toThrow( + "encryptionType, iv, and data must be provided", + ); + }); + + it.each(SymmetricEncryptionTypes.map((t) => encryptionTypeToString(t)))( + "works for %s", + async (typeName) => { + const type = EncryptionType[typeName as keyof typeof EncryptionType]; + const iv = plainValue; + const mac = type === EncryptionType.AesCbc256_B64 ? null : makeStaticByteArray(32, 20); + const data = plainValue; + + const actual = EncArrayBuffer.fromParts(type, iv, data, mac); + + expect(actual.encryptionType).toEqual(type); + expect(actual.ivBytes).toEqual(iv); + expect(actual.macBytes).toEqual(mac); + expect(actual.dataBytes).toEqual(data); + }, + ); + + it.each(SymmetricEncryptionTypes.filter((t) => t !== EncryptionType.AesCbc256_B64))( + "validates mac length for %s", + (type) => { + const iv = plainValue; + const mac = makeStaticByteArray(1, 20); + const data = plainValue; + + expect(() => EncArrayBuffer.fromParts(type, iv, data, mac)).toThrow("Invalid MAC length"); + }, + ); + + it.each(SymmetricEncryptionTypes.map((t) => encryptionTypeToString(t)))( + "requires or forbids mac for %s", + async (typeName) => { + const type = EncryptionType[typeName as keyof typeof EncryptionType]; + const iv = makeStaticByteArray(16, 10); + const mac = type === EncryptionType.AesCbc256_B64 ? makeStaticByteArray(32, 20) : null; + const data = plainValue; + + expect(() => EncArrayBuffer.fromParts(type, iv, data, mac)).toThrow(); + }, + ); + + it.each(AsymmetricEncryptionTypes)("throws for async type %s", (type) => { + expect(() => EncArrayBuffer.fromParts(type, plainValue, plainValue, null)).toThrow( + `Unknown EncryptionType ${type} for EncArrayBuffer.fromParts`, + ); + }); + }); }); diff --git a/libs/common/src/platform/models/domain/enc-array-buffer.ts b/libs/common/src/platform/models/domain/enc-array-buffer.ts index ee3cf30fe1f..838a6284a94 100644 --- a/libs/common/src/platform/models/domain/enc-array-buffer.ts +++ b/libs/common/src/platform/models/domain/enc-array-buffer.ts @@ -8,50 +8,86 @@ const MAC_LENGTH = 32; const MIN_DATA_LENGTH = 1; export class EncArrayBuffer implements Encrypted { - readonly encryptionType?: EncryptionType; - readonly dataBytes: Uint8Array | null = null; - readonly ivBytes: Uint8Array | null = null; - readonly macBytes: Uint8Array | undefined | null = null; + readonly encryptionType: EncryptionType; + readonly dataBytes: Uint8Array; + readonly ivBytes: Uint8Array; + readonly macBytes: Uint8Array | null = null; + private static readonly DecryptionError = new Error( + "Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format.", + ); constructor(readonly buffer: Uint8Array) { - const encBytes = buffer; - this.encryptionType = encBytes[0]; + if (buffer == null) { + throw new Error("EncArrayBuffer initialized with null buffer."); + } + + this.encryptionType = this.buffer[0]; switch (this.encryptionType) { 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(); + if (this.buffer.length < minimumLength) { + throw EncArrayBuffer.DecryptionError; } - this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH); - this.macBytes = encBytes.slice( + this.ivBytes = this.buffer.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH); + this.macBytes = this.buffer.slice( ENC_TYPE_LENGTH + IV_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH, ); - this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH); + this.dataBytes = this.buffer.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH); break; } case EncryptionType.AesCbc256_B64: { const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MIN_DATA_LENGTH; - if (encBytes.length < minimumLength) { - this.throwDecryptionError(); + if (this.buffer.length < minimumLength) { + throw EncArrayBuffer.DecryptionError; } - this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH); - this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH); + this.ivBytes = this.buffer.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH); + this.dataBytes = this.buffer.slice(ENC_TYPE_LENGTH + IV_LENGTH); break; } default: - this.throwDecryptionError(); + throw EncArrayBuffer.DecryptionError; } } - private throwDecryptionError() { - throw new Error( - "Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format.", - ); + static fromParts( + encryptionType: EncryptionType, + iv: Uint8Array, + data: Uint8Array, + mac: Uint8Array | undefined | null, + ) { + if (encryptionType == null || iv == null || data == null) { + throw new Error("encryptionType, iv, and data must be provided"); + } + + switch (encryptionType) { + case EncryptionType.AesCbc256_B64: + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + EncArrayBuffer.validateIvLength(iv); + EncArrayBuffer.validateMacLength(encryptionType, mac); + break; + default: + throw new Error(`Unknown EncryptionType ${encryptionType} for EncArrayBuffer.fromParts`); + } + + let macLen = 0; + if (mac != null) { + macLen = mac.length; + } + + const bytes = new Uint8Array(1 + iv.byteLength + macLen + data.byteLength); + bytes.set([encryptionType], 0); + bytes.set(iv, 1); + if (mac != null) { + bytes.set(mac, 1 + iv.byteLength); + } + bytes.set(data, 1 + iv.byteLength + macLen); + return new EncArrayBuffer(bytes); } static async fromResponse(response: { @@ -68,4 +104,28 @@ export class EncArrayBuffer implements Encrypted { const buffer = Utils.fromB64ToArray(b64); return new EncArrayBuffer(buffer); } + + static validateIvLength(iv: Uint8Array) { + if (iv == null || iv.length !== IV_LENGTH) { + throw new Error("Invalid IV length"); + } + } + + static validateMacLength(encType: EncryptionType, mac: Uint8Array | null | undefined) { + switch (encType) { + case EncryptionType.AesCbc256_B64: + if (mac != null) { + throw new Error("mac must not be provided for AesCbc256_B64"); + } + break; + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (mac == null || mac.length !== MAC_LENGTH) { + throw new Error("Invalid MAC length"); + } + break; + default: + throw new Error("Invalid encryption type and mac combination"); + } + } }