mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Add optional aad for aes gcm mode decryption
This is used in key connector communication tunneling to prevent downgrade attacks in the future
This commit is contained in:
@@ -46,6 +46,20 @@ export abstract class CryptoFunctionService {
|
||||
): Promise<Uint8Array | string>;
|
||||
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
|
||||
abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array>;
|
||||
/**
|
||||
* encrypt data using AES-256-GCM.
|
||||
*
|
||||
* @param data data to encrypt
|
||||
* @param iv the iv to use for encryption
|
||||
* @param key the key to use for encryption
|
||||
* @returns the encrypted data in the form of data + tag + iv
|
||||
*/
|
||||
abstract aesGcmEncrypt(
|
||||
data: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array>;
|
||||
abstract aesDecryptFastParameters(
|
||||
data: string,
|
||||
iv: string,
|
||||
@@ -53,11 +67,10 @@ export abstract class CryptoFunctionService {
|
||||
key: SymmetricCryptoKey,
|
||||
): DecryptParameters<Uint8Array | string>;
|
||||
/**
|
||||
* Decrypts AES encrypted data using Forge in the web. Available modes are CBC, ECB, and GCM.
|
||||
* Decrypts AES encrypted data using Forge in the web. Available modes are CBC and ECB.
|
||||
*
|
||||
* GCM mode supports only GCM 256 with a 12 byte IV and a 16 byte tag.
|
||||
*
|
||||
* @param data the data to decrypt. For CBC and ECB mode, this should be the ciphertext. For GCM mode, this should be the ciphertext + tag.
|
||||
* @param data the data to decrypt.
|
||||
* @param iv the initialization vector to use for decryption
|
||||
* @param key the key to use for decryption
|
||||
* @param mode the mode to use for decryption
|
||||
@@ -65,7 +78,8 @@ export abstract class CryptoFunctionService {
|
||||
|
||||
abstract aesDecryptFast(
|
||||
parameters: DecryptParameters<Uint8Array | string>,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
mode: "cbc" | "ecb",
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Decrypts AES encrypted data. Available modes are CBC, ECB, and GCM.
|
||||
@@ -76,12 +90,14 @@ export abstract class CryptoFunctionService {
|
||||
* @param iv the initialization vector to use for decryption
|
||||
* @param key the key to use for decryption
|
||||
* @param mode the mode to use for decryption
|
||||
* @param additionalData additional data to use for decryption in GCM mode. Ignored for CBC and ECB mode.
|
||||
*/
|
||||
abstract aesDecrypt(
|
||||
data: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array>;
|
||||
abstract rsaEncrypt(
|
||||
data: Uint8Array,
|
||||
|
||||
@@ -23,8 +23,28 @@ export abstract class EncryptService {
|
||||
*
|
||||
* @param data The encrypted data + tag + iv
|
||||
* @param key The key to decrypt the data
|
||||
* @param additionalData Additional data to authenticate
|
||||
*/
|
||||
abstract aesGcmDecryptToBytes(data: Uint8Array, key: Uint8Array): Promise<Uint8Array>;
|
||||
abstract aesGcmDecryptToBytes(
|
||||
data: Uint8Array,
|
||||
key: Uint8Array,
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* Encrypts data using AES-256-GCM.
|
||||
*
|
||||
* @remarks this is currently used only for Key Connector communications. Do not use for general encryption.
|
||||
*
|
||||
* @param data data to encrypt
|
||||
* @param key key to encrypt with
|
||||
* @param additionalData additional data to authenticate
|
||||
* @returns the encrypted data in the form of data + tag + iv
|
||||
*/
|
||||
abstract aesGcmEncryptToBytes(
|
||||
data: Uint8Array,
|
||||
key: Uint8Array,
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array>;
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
|
||||
|
||||
@@ -126,7 +126,28 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc");
|
||||
}
|
||||
|
||||
async aesGcmDecryptToBytes(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||
async aesGcmEncryptToBytes(
|
||||
data: Uint8Array,
|
||||
key: Uint8Array,
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
throw new Error("Nothing provided for encryption.");
|
||||
}
|
||||
|
||||
const iv = await this.cryptoFunctionService.randomBytes(12);
|
||||
return await this.cryptoFunctionService.aesGcmEncrypt(data, iv, key, additionalData);
|
||||
}
|
||||
|
||||
async aesGcmDecryptToBytes(
|
||||
data: Uint8Array,
|
||||
key: Uint8Array,
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
@@ -140,7 +161,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const iv = data.slice(-12);
|
||||
|
||||
// aesDecrypt expects cipher + tag, but iv split
|
||||
return await this.cryptoFunctionService.aesDecrypt(dataAndTag, iv, key, "gcm");
|
||||
return await this.cryptoFunctionService.aesDecrypt(dataAndTag, iv, key, "gcm", additionalData);
|
||||
}
|
||||
|
||||
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
|
||||
@@ -82,38 +82,96 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesGcmDecryptToBytes", () => {
|
||||
describe("aesGcm", () => {
|
||||
const data = makeStaticByteArray(10, 100);
|
||||
const key = makeStaticByteArray(32, 200);
|
||||
const iv = makeStaticByteArray(12, 300);
|
||||
const encryptedData = new Uint8Array(1);
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService.randomBytes.calledWith(12).mockResolvedValueOnce(iv as CsprngArray);
|
||||
cryptoFunctionService.aesGcmEncrypt.mockResolvedValue(encryptedData);
|
||||
cryptoFunctionService.aesDecrypt.mockResolvedValue(data);
|
||||
});
|
||||
|
||||
it("throws if no data is provided", () => {
|
||||
return expect(encryptService.aesGcmDecryptToBytes(null, key)).rejects.toThrow(
|
||||
"Nothing provided for decryption",
|
||||
);
|
||||
describe("aesGcmEncryptToBytes", () => {
|
||||
it("throws if no data is provided", () => {
|
||||
return expect(encryptService.aesGcmEncryptToBytes(null, key)).rejects.toThrow(
|
||||
"Nothing provided for encryption",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.aesGcmEncryptToBytes(data, null)).rejects.toThrow(
|
||||
"No encryption key",
|
||||
);
|
||||
});
|
||||
|
||||
it("encrypts data with provided key", async () => {
|
||||
const actual = await encryptService.aesGcmEncryptToBytes(data, key);
|
||||
|
||||
expect(cryptoFunctionService.aesGcmEncrypt).toHaveBeenCalledWith(
|
||||
expect.toEqualBuffer(data),
|
||||
expect.toEqualBuffer(iv),
|
||||
expect.toEqualBuffer(key),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actual).toEqualBuffer(encryptedData);
|
||||
});
|
||||
|
||||
it("encrypts data with the provided additional data", async () => {
|
||||
const additionalData = makeStaticByteArray(10, 400);
|
||||
await encryptService.aesGcmEncryptToBytes(data, key, additionalData);
|
||||
|
||||
expect(cryptoFunctionService.aesGcmEncrypt).toHaveBeenCalledWith(
|
||||
expect.toEqualBuffer(data),
|
||||
expect.toEqualBuffer(iv),
|
||||
expect.toEqualBuffer(key),
|
||||
expect.toEqualBuffer(additionalData),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.aesGcmDecryptToBytes(encryptedData, null)).rejects.toThrow(
|
||||
"No encryption key",
|
||||
);
|
||||
});
|
||||
describe("aesGcmDecryptToBytes", () => {
|
||||
it("throws if no data is provided", () => {
|
||||
return expect(encryptService.aesGcmDecryptToBytes(null, key)).rejects.toThrow(
|
||||
"Nothing provided for decryption",
|
||||
);
|
||||
});
|
||||
|
||||
it("strips off the tag and decrypts data with provided key and iv", async () => {
|
||||
const actual = await encryptService.aesGcmDecryptToBytes(encryptedData, key);
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.aesGcmDecryptToBytes(encryptedData, null)).rejects.toThrow(
|
||||
"No encryption key",
|
||||
);
|
||||
});
|
||||
|
||||
expect(cryptoFunctionService.aesDecrypt).toHaveBeenCalledWith(
|
||||
expect.toEqualBuffer(encryptedData.slice(0, -12)),
|
||||
expect.toEqualBuffer(encryptedData.slice(-12)),
|
||||
expect.toEqualBuffer(key),
|
||||
"gcm",
|
||||
);
|
||||
it("strips off the tag and decrypts data with provided key and iv", async () => {
|
||||
const actual = await encryptService.aesGcmDecryptToBytes(encryptedData, key);
|
||||
|
||||
expect(actual).toEqualBuffer(data);
|
||||
expect(cryptoFunctionService.aesDecrypt).toHaveBeenCalledWith(
|
||||
expect.toEqualBuffer(encryptedData.slice(0, -12)),
|
||||
expect.toEqualBuffer(encryptedData.slice(-12)),
|
||||
expect.toEqualBuffer(key),
|
||||
"gcm",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actual).toEqualBuffer(data);
|
||||
});
|
||||
|
||||
it("decrypts data with the provided additional data", async () => {
|
||||
const additionalData = makeStaticByteArray(10, 400);
|
||||
await encryptService.aesGcmDecryptToBytes(encryptedData, key, additionalData);
|
||||
|
||||
expect(cryptoFunctionService.aesDecrypt).toHaveBeenCalledWith(
|
||||
expect.toEqualBuffer(encryptedData.slice(0, -12)),
|
||||
expect.toEqualBuffer(encryptedData.slice(-12)),
|
||||
expect.toEqualBuffer(key),
|
||||
"gcm",
|
||||
expect.toEqualBuffer(additionalData),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -309,6 +309,21 @@ describe("WebCrypto Function Service", () => {
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
|
||||
it("should successfully encrypt and then decrypt data with aad", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(12);
|
||||
const key = makeStaticByteArray(32);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const aad = Utils.fromUtf8ToArray("aad");
|
||||
const encAndIv = new Uint8Array(
|
||||
await cryptoFunctionService.aesGcmEncrypt(data, iv, key, aad),
|
||||
);
|
||||
const envValue = encAndIv.slice(0, encAndIv.length - 12);
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm", aad);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecryptFast CBC mode", () => {
|
||||
@@ -336,17 +351,6 @@ describe("WebCrypto Function Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecryptFast GCM mode", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(12);
|
||||
const key = makeStaticByteArray(32);
|
||||
const data = Utils.fromB64ToArray("Amy1abyVtlboYFBtLnDAzAwAgb3Qg2m4fMo=");
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "gcm");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecrypt CBC mode", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
|
||||
@@ -293,16 +293,13 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return p;
|
||||
}
|
||||
|
||||
aesDecryptFast(
|
||||
parameters: DecryptParameters<string>,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): Promise<string> {
|
||||
aesDecryptFast(parameters: DecryptParameters<string>, mode: "cbc" | "ecb"): Promise<string> {
|
||||
const decipher = (forge as any).cipher.createDecipher(
|
||||
this.toWebCryptoAesMode(mode),
|
||||
parameters.encKey,
|
||||
);
|
||||
const options = {} as any;
|
||||
if (mode === "cbc" || mode === "gcm") {
|
||||
if (mode === "cbc") {
|
||||
options.iv = parameters.iv;
|
||||
}
|
||||
const dataBuffer = (forge as any).util.createBuffer(parameters.data);
|
||||
@@ -318,6 +315,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
switch (mode) {
|
||||
case "ecb": {
|
||||
@@ -337,11 +335,15 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
const buffer = await this.subtle.decrypt(
|
||||
{ name: this.toWebCryptoAesMode(mode), iv: iv },
|
||||
impKey,
|
||||
data,
|
||||
);
|
||||
const parameters: AesGcmParams | AesCbcParams = {
|
||||
name: this.toWebCryptoAesMode(mode),
|
||||
iv: iv,
|
||||
};
|
||||
if (mode === "gcm") {
|
||||
(parameters as AesGcmParams).tagLength = 128;
|
||||
(parameters as AesGcmParams).additionalData = additionalData;
|
||||
}
|
||||
const buffer = await this.subtle.decrypt(parameters, impKey, data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,21 @@ describe("NodeCrypto Function Service", () => {
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
|
||||
it("should successfully encrypt and then decrypt data with aad", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(12);
|
||||
const key = makeStaticByteArray(32);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const aad = Utils.fromUtf8ToArray("aad");
|
||||
const encAndIv = new Uint8Array(
|
||||
await cryptoFunctionService.aesGcmEncrypt(data, iv, key, aad),
|
||||
);
|
||||
const envValue = encAndIv.slice(0, encAndIv.length - 12);
|
||||
const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm", aad);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecryptFast CBC mode", () => {
|
||||
@@ -251,18 +266,6 @@ describe("NodeCrypto Function Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecryptFast GCM mode", () => {
|
||||
it("successfully decrypts data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = Utils.fromBufferToB64(makeStaticByteArray(12));
|
||||
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32));
|
||||
const data = "Amy1abyVtlboYFBtLnDAzAwAgb3Qg2m4fMo=";
|
||||
const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
|
||||
const decValue = await nodeCryptoFunctionService.aesDecryptFast(params, "gcm");
|
||||
expect(decValue).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecrypt CBC mode", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
|
||||
@@ -210,7 +210,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
|
||||
async aesDecryptFast(
|
||||
parameters: DecryptParameters<Uint8Array>,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
mode: "cbc" | "ecb",
|
||||
): Promise<string> {
|
||||
const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey, mode);
|
||||
return Utils.fromBufferToUtf8(decBuf);
|
||||
@@ -221,15 +221,22 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
additionalData?: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
const nodeData =
|
||||
mode !== "gcm" ? this.toNodeBuffer(data) : this.toNodeBuffer(data.slice(0, -16)); // remove gcm tag
|
||||
const nodeIv = mode === "ecb" ? null : this.toNodeBuffer(iv);
|
||||
const nodeKey = this.toNodeBuffer(key);
|
||||
const decipher = crypto.createDecipheriv(this.toNodeCryptoAesMode(mode), nodeKey, nodeIv);
|
||||
|
||||
if (mode === "gcm") {
|
||||
(decipher as crypto.DecipherGCM).setAuthTag(data.slice(-16));
|
||||
}
|
||||
if (additionalData != null && mode === "gcm") {
|
||||
const nodeAdditionalData = this.toNodeBuffer(additionalData);
|
||||
(decipher as crypto.DecipherGCM).setAAD(nodeAdditionalData);
|
||||
}
|
||||
|
||||
const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]);
|
||||
return Promise.resolve(this.toUint8Buffer(decBuf));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user