Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16831] TS Strict crypto function service #12737

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CsprngArray } from "../../types/csprng";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";

export abstract class CryptoFunctionService {
Expand Down Expand Up @@ -51,11 +51,13 @@ export abstract class CryptoFunctionService {
iv: string,
mac: string,
key: SymmetricCryptoKey,
): DecryptParameters<Uint8Array | string>;
abstract aesDecryptFast(
parameters: DecryptParameters<Uint8Array | string>,
mode: "cbc" | "ecb",
): Promise<string>;
): CbcDecryptParameters<Uint8Array | string>;
abstract aesDecryptFast({
mode,
parameters,
}:
| { mode: "cbc"; parameters: CbcDecryptParameters<Uint8Array | string> }
| { mode: "ecb"; parameters: EcbDecryptParameters<Uint8Array | string> }): Promise<string>;
abstract aesDecrypt(
data: Uint8Array,
iv: Uint8Array,
Expand Down
15 changes: 9 additions & 6 deletions libs/common/src/platform/models/domain/decrypt-parameters.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export class DecryptParameters<T> {
export type CbcDecryptParameters<T> = {
encKey: T;
data: T;
iv: T;
macKey: T;
mac: T;
macKey?: T;
mac?: T;
macData: T;
}
};

export type EcbDecryptParameters<T> = {
encKey: T;
data: T;
};
10 changes: 3 additions & 7 deletions libs/common/src/platform/models/domain/symmetric-crypto-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EncryptionType } from "../../enums";

export class SymmetricCryptoKey {
key: Uint8Array;
encKey?: Uint8Array;
encKey: Uint8Array;
macKey?: Uint8Array;
encType: EncryptionType;

Expand Down Expand Up @@ -48,12 +48,8 @@ export class SymmetricCryptoKey {
throw new Error("Unsupported encType/key length.");
}

if (this.key != null) {
this.keyB64 = Utils.fromBufferToB64(this.key);
}
if (this.encKey != null) {
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
}
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
if (this.macKey != null) {
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
}
}

return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc");
return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams });

Check warning on line 128 in libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts#L128

Added line #L128 was not covered by tests
}

async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";

import { Utils } from "../../platform/misc/utils";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { EcbDecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";

import { WebCryptoFunctionService } from "./web-crypto-function.service";
Expand Down Expand Up @@ -253,8 +253,13 @@ describe("WebCrypto Function Service", () => {
const encData = Utils.fromBufferToB64(encValue);
const b64Iv = Utils.fromBufferToB64(iv);
const symKey = new SymmetricCryptoKey(key);
const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc");
const parameters = cryptoFunctionService.aesDecryptFastParameters(
encData,
b64Iv,
null,
symKey,
);
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
expect(decValue).toBe(value);
});

Expand All @@ -276,8 +281,8 @@ describe("WebCrypto Function Service", () => {
const iv = Utils.fromBufferToB64(makeStaticByteArray(16));
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32));
const data = "ByUF8vhyX4ddU9gcooznwA==";
const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc");
const parameters = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
expect(decValue).toBe("EncryptMe!");
});
});
Expand All @@ -287,10 +292,11 @@ describe("WebCrypto Function Service", () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw==");
const params = new DecryptParameters<string>();
params.encKey = Utils.fromBufferToByteString(key);
params.data = Utils.fromBufferToByteString(data);
const decValue = await cryptoFunctionService.aesDecryptFast(params, "ecb");
const parameters: EcbDecryptParameters<string> = {
encKey: Utils.fromBufferToByteString(key),
data: Utils.fromBufferToByteString(data),
};
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "ecb", parameters });
expect(decValue).toBe("EncryptMe!");
});
});
Expand All @@ -304,6 +310,15 @@ describe("WebCrypto Function Service", () => {
const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "cbc");
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
});

it("throws if iv is not provided", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
await expect(() => cryptoFunctionService.aesDecrypt(data, null, key, "cbc")).rejects.toThrow(
"IV is required for CBC mode",
);
});
});

describe("aesDecrypt ECB mode", () => {
Expand Down
47 changes: 30 additions & 17 deletions libs/common/src/platform/services/web-crypto-function.service.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as argon2 from "argon2-browser";
import * as forge from "node-forge";

import { Utils } from "../../platform/misc/utils";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";

export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto;
private subtle: SubtleCrypto;
private wasmSupported: boolean;

constructor(globalContext: Window | typeof global) {
this.crypto = typeof globalContext.crypto !== "undefined" ? globalContext.crypto : null;
this.subtle =
!!this.crypto && typeof this.crypto.subtle !== "undefined" ? this.crypto.subtle : null;
constructor(globalContext: { crypto: Crypto }) {
if (globalContext?.crypto?.subtle == null) {
throw new Error(

Check warning on line 17 in libs/common/src/platform/services/web-crypto-function.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/web-crypto-function.service.ts#L17

Added line #L17 was not covered by tests
"Could not instantiate WebCryptoFunctionService. Could not locate Subtle crypto.",
);
}
this.crypto = globalContext.crypto;
this.subtle = this.crypto.subtle;
this.wasmSupported = this.checkIfWasmSupported();
}

Expand Down Expand Up @@ -220,7 +222,7 @@
hmac.update(a);
const mac1 = hmac.digest().getBytes();

hmac.start(null, null);
hmac.start("sha256", null);
hmac.update(b);
const mac2 = hmac.digest().getBytes();

Expand All @@ -239,10 +241,10 @@
aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
mac: string | null,
key: SymmetricCryptoKey,
): DecryptParameters<string> {
const p = new DecryptParameters<string>();
): CbcDecryptParameters<string> {
const p = {} as CbcDecryptParameters<string>;
if (key.meta != null) {
p.encKey = key.meta.encKeyByteString;
p.macKey = key.meta.macKeyByteString;
Expand Down Expand Up @@ -275,7 +277,12 @@
return p;
}

aesDecryptFast(parameters: DecryptParameters<string>, mode: "cbc" | "ecb"): Promise<string> {
aesDecryptFast({
mode,
parameters,
}:
| { mode: "cbc"; parameters: CbcDecryptParameters<string> }
| { mode: "ecb"; parameters: EcbDecryptParameters<string> }): Promise<string> {
const decipher = (forge as any).cipher.createDecipher(
this.toWebCryptoAesMode(mode),
parameters.encKey,
Expand All @@ -294,21 +301,27 @@

async aesDecrypt(
data: Uint8Array,
iv: Uint8Array,
iv: Uint8Array | null,
key: Uint8Array,
mode: "cbc" | "ecb",
): Promise<Uint8Array> {
if (mode === "ecb") {
// Web crypto does not support AES-ECB mode, so we need to do this in forge.
const params = new DecryptParameters<string>();
params.data = this.toByteString(data);
params.encKey = this.toByteString(key);
const result = await this.aesDecryptFast(params, "ecb");
const parameters: EcbDecryptParameters<string> = {
data: this.toByteString(data),
encKey: this.toByteString(key),
};
const result = await this.aesDecryptFast({ mode: "ecb", parameters });
return Utils.fromByteStringToArray(result);
}
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"decrypt",
]);

// CBC
if (iv == null) {
throw new Error("IV is required for CBC mode.");
}
const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
return new Uint8Array(buffer);
}
Expand Down
26 changes: 18 additions & 8 deletions libs/node/src/services/node-crypto-function.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters";
import { EcbDecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";

import { NodeCryptoFunctionService } from "./node-crypto-function.service";
Expand Down Expand Up @@ -193,19 +193,20 @@ describe("NodeCrypto Function Service", () => {
const iv = Utils.fromBufferToB64(makeStaticByteArray(16));
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32));
const data = "ByUF8vhyX4ddU9gcooznwA==";
const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await nodeCryptoFunctionService.aesDecryptFast(params, "cbc");
const parameters = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await nodeCryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
expect(decValue).toBe("EncryptMe!");
});
});

describe("aesDecryptFast ECB mode", () => {
it("should successfully decrypt data", async () => {
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
const params = new DecryptParameters<Uint8Array>();
params.encKey = makeStaticByteArray(32);
params.data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw==");
const decValue = await nodeCryptoFunctionService.aesDecryptFast(params, "ecb");
const parameters: EcbDecryptParameters<Uint8Array> = {
encKey: makeStaticByteArray(32),
data: Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="),
};
const decValue = await nodeCryptoFunctionService.aesDecryptFast({ mode: "ecb", parameters });
expect(decValue).toBe("EncryptMe!");
});
});
Expand All @@ -219,6 +220,15 @@ describe("NodeCrypto Function Service", () => {
const decValue = await nodeCryptoFunctionService.aesDecrypt(data, iv, key, "cbc");
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
});

it("throws if IV is not provided", async () => {
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
await expect(
async () => await nodeCryptoFunctionService.aesDecrypt(data, null, key, "cbc"),
).rejects.toThrow("Invalid initialization vector");
});
});

describe("aesDecrypt ECB mode", () => {
Expand Down Expand Up @@ -454,7 +464,7 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string, fast = f
const cryptoFunctionService = new NodeCryptoFunctionService();
const value = Utils.fromUtf8ToArray("SignMe!!");
const key = Utils.fromUtf8ToArray("secretkey");
let computedMac: ArrayBuffer = null;
let computedMac: ArrayBuffer;
if (fast) {
computedMac = await cryptoFunctionService.hmacFast(value, key, algorithm);
} else {
Expand Down
37 changes: 24 additions & 13 deletions libs/node/src/services/node-crypto-function.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as crypto from "crypto";

import * as forge from "node-forge";

import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters";
import {
CbcDecryptParameters,
EcbDecryptParameters,
} from "@bitwarden/common/platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";

Expand Down Expand Up @@ -168,10 +169,10 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
mac: string | null,
key: SymmetricCryptoKey,
): DecryptParameters<Uint8Array> {
const p = new DecryptParameters<Uint8Array>();
): CbcDecryptParameters<Uint8Array> {
const p = {} as CbcDecryptParameters<Uint8Array>;
p.encKey = key.encKey;
p.data = Utils.fromB64ToArray(data);
p.iv = Utils.fromB64ToArray(iv);
Expand All @@ -191,22 +192,25 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
return p;
}

async aesDecryptFast(
parameters: DecryptParameters<Uint8Array>,
mode: "cbc" | "ecb",
): Promise<string> {
const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey, mode);
async aesDecryptFast({
mode,
parameters,
}:
| { mode: "cbc"; parameters: CbcDecryptParameters<Uint8Array> }
| { mode: "ecb"; parameters: EcbDecryptParameters<Uint8Array> }): Promise<string> {
const iv = mode === "cbc" ? parameters.iv : null;
const decBuf = await this.aesDecrypt(parameters.data, iv, parameters.encKey, mode);
return Utils.fromBufferToUtf8(decBuf);
}

aesDecrypt(
data: Uint8Array,
iv: Uint8Array,
iv: Uint8Array | null,
key: Uint8Array,
mode: "cbc" | "ecb",
): Promise<Uint8Array> {
const nodeData = this.toNodeBuffer(data);
const nodeIv = mode === "ecb" ? null : this.toNodeBuffer(iv);
const nodeIv = this.toNodeBufferOrNull(iv);
const nodeKey = this.toNodeBuffer(key);
const decipher = crypto.createDecipheriv(this.toNodeCryptoAesMode(mode), nodeKey, nodeIv);
const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]);
Expand Down Expand Up @@ -311,6 +315,13 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
return Buffer.from(value);
}

private toNodeBufferOrNull(value: Uint8Array | null): Buffer | null {
if (value == null) {
return null;
}
return this.toNodeBuffer(value);
}

private toUint8Buffer(value: Buffer | string | Uint8Array): Uint8Array {
let buf: Uint8Array;
if (typeof value === "string") {
Expand Down
Loading