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

Allow for easy creation of EncArrayBuffer

These are to support SDK processing of `Encrypted` interface items.
This commit is contained in:
Matt Gibson
2025-03-05 16:39:51 -08:00
parent ebb5584d43
commit 394ed91993
4 changed files with 176 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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