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:
15
libs/common/src/platform/enums/encryption-type.enum.spec.ts
Normal file
15
libs/common/src/platform/enums/encryption-type.enum.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user