mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Add diffie-hellman and aes-gcm cryptography
We're planning on using x25519 encapsulating an aes-256-gcm key.
This commit is contained in:
@@ -52,15 +52,36 @@ export abstract class CryptoFunctionService {
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey,
|
||||
): DecryptParameters<Uint8Array | string>;
|
||||
/**
|
||||
* Decrypts AES encrypted data using Forge in the web. Available modes are CBC, ECB, and GCM.
|
||||
*
|
||||
* 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 iv the initialization vector to use for decryption
|
||||
* @param key the key to use for decryption
|
||||
* @param mode the mode to use for decryption
|
||||
*/
|
||||
|
||||
abstract aesDecryptFast(
|
||||
parameters: DecryptParameters<Uint8Array | string>,
|
||||
mode: "cbc" | "ecb",
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Decrypts AES encrypted data. Available modes are CBC, ECB, and GCM.
|
||||
*
|
||||
* 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 iv the initialization vector to use for decryption
|
||||
* @param key the key to use for decryption
|
||||
* @param mode the mode to use for decryption
|
||||
*/
|
||||
abstract aesDecrypt(
|
||||
data: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb",
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): Promise<Uint8Array>;
|
||||
abstract rsaEncrypt(
|
||||
data: Uint8Array,
|
||||
@@ -84,4 +105,33 @@ export abstract class CryptoFunctionService {
|
||||
* Do not use this for generating encryption keys. Use aesGenerateKey or rsaGenerateKeyPair instead.
|
||||
*/
|
||||
abstract randomBytes(length: number): Promise<CsprngArray>;
|
||||
/**
|
||||
* Generate a random asymmetric key pair using the given algorithm on the given curve
|
||||
*
|
||||
* x25519: Curve25519, does not use the curve parameter and will throw if passed in a non-nullish curve
|
||||
* ecdh: Elliptic Curve Diffie-Hellman, uses the curve parameter to specify the curve to use
|
||||
* P-256: NIST P-256
|
||||
* P-384: NIST P-384
|
||||
* P-521: NIST P-521
|
||||
* @param algorithm the algorithm to use
|
||||
* @param curve the curve to use for ecdh, or undefined for x25519
|
||||
* @returns a promise that resolves to an object containing the private and public keys
|
||||
*/
|
||||
abstract diffieHellmanGenerateKeyPair(
|
||||
algorithm: "x25519" | "ecdh",
|
||||
curve: undefined | "P-256" | "P-384" | "P-521",
|
||||
): Promise<{ keyPair: CryptoKeyPair; publicKey: Uint8Array }>;
|
||||
/**
|
||||
* Derive a shared key from a private key and an external public key
|
||||
*
|
||||
* @param privateKey the private key to use
|
||||
* @param publicKey the public key to use
|
||||
* @returns a promise that resolves to the shared key bits
|
||||
*/
|
||||
abstract deriveSharedKeyBits(
|
||||
privateKey: CryptoKey,
|
||||
publicKeyRaw: Uint8Array,
|
||||
algorithm: "x25519" | "ecdh",
|
||||
curve: undefined | "P-256" | "P-384" | "P-521",
|
||||
): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
@@ -295,6 +295,17 @@ 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();
|
||||
@@ -316,6 +327,17 @@ describe("WebCrypto Function Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecrypt 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("rsaEncrypt", () => {
|
||||
it("should successfully encrypt and then decrypt data", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
@@ -391,6 +413,292 @@ describe("WebCrypto Function Service", () => {
|
||||
expect(key.slice(0, 32)).not.toEqual(key.slice(32, 64));
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffieHellmanGenerateKeyPair", () => {
|
||||
describe("x25519", () => {
|
||||
it("generates a key pair", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
expect(keyPair.privateKey).toBeDefined();
|
||||
expect(keyPair.publicKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("pre-extracts the public key in raw format", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair, publicKey } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
const publicFromKeyPair = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
|
||||
expect(publicKey).toEqualBuffer(publicFromKeyPair);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ecdh", () => {
|
||||
describe.each(["P-256", "P-384", "P-521"] as const)("curve %s", (curve) => {
|
||||
it("generates a key pair", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
expect(keyPair.privateKey).toBeDefined();
|
||||
expect(keyPair.publicKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("pre-extracts the public key in raw format", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair, publicKey } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
const publicFromKeyPair = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
|
||||
expect(publicKey).toEqualBuffer(publicFromKeyPair);
|
||||
// Should be odd
|
||||
expect(publicKey.byteLength % 2).toBe(1);
|
||||
// First byte should be 0x04
|
||||
expect(publicKey[0]).toBe(0x04);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffieHellmanGenerateKeyPair", () => {
|
||||
describe("x25519", () => {
|
||||
it("generates a key pair", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
expect(keyPair.privateKey).toBeDefined();
|
||||
expect(keyPair.publicKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("pre-extracts the public key in raw format", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair, publicKey } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
const publicFromKeyPair = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
|
||||
expect(publicKey).toEqual(new Uint8Array(publicFromKeyPair));
|
||||
});
|
||||
});
|
||||
|
||||
describe("ecdh", () => {
|
||||
describe.each(["P-256", "P-384", "P-521"] as const)("curve %s", (curve) => {
|
||||
it("generates a key pair", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
expect(keyPair.privateKey).toBeDefined();
|
||||
expect(keyPair.publicKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("pre-extracts the public key in raw format", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const { keyPair, publicKey } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
const publicFromKeyPair = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
|
||||
expect(publicKey).toEqual(new Uint8Array(publicFromKeyPair));
|
||||
// Should be odd
|
||||
expect(publicKey.byteLength % 2).toBe(1);
|
||||
// First byte should be 0x04
|
||||
expect(publicKey[0]).toBe(0x04);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveSharedKeyBits", () => {
|
||||
let cryptoFunctionService: WebCryptoFunctionService;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = getWebCryptoFunctionService();
|
||||
});
|
||||
|
||||
describe("x25519", () => {
|
||||
const testVectors = Object.freeze({
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
crv: "X25519",
|
||||
d: "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
|
||||
x: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=",
|
||||
kty: "OKP",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
crv: "X25519",
|
||||
d: "XasIfmJKikt54X+Lg4AO5m87sSkmGLb9HC+LJ/+I4Os=",
|
||||
x: "3p7bfXt9wbTTW2HC7OQ1Nz+DQ8hbeGdNrfx+FG+IK08=",
|
||||
kty: "OKP",
|
||||
},
|
||||
expected: "Sl2dW6TOLeFyjjv0gDUPJeB+IclH0Z4zdvCbPB4WF0I", // hex
|
||||
});
|
||||
|
||||
it("computes the correct shared key for a test vector", async () => {
|
||||
function testVectorToUint8Array(hex: string): Uint8Array {
|
||||
return new Uint8Array(Buffer.from(hex, "hex"));
|
||||
}
|
||||
const aPrivateKey = await crypto.subtle.importKey("jwk", testVectors.a, "x25519", false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
const aPublicKey = new Uint8Array(Buffer.from(testVectors.a.x, "base64"));
|
||||
const bPrivateKey = await crypto.subtle.importKey("jwk", testVectors.b, "x25519", false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
const bPublicKey = new Uint8Array(Buffer.from(testVectors.b.x, "base64"));
|
||||
const expectedSharedKey = testVectorToUint8Array(
|
||||
"4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742",
|
||||
);
|
||||
|
||||
const actualAB = await cryptoFunctionService.deriveSharedKeyBits(
|
||||
aPrivateKey,
|
||||
bPublicKey,
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
const actualBA = await cryptoFunctionService.deriveSharedKeyBits(
|
||||
bPrivateKey,
|
||||
aPublicKey,
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actualAB).toEqual(expectedSharedKey);
|
||||
expect(actualBA).toEqual(expectedSharedKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ecdh", () => {
|
||||
// from rfc 7027 https://datatracker.ietf.org/doc/html/rfc7027.html#page-6
|
||||
const testVectors = Object.freeze({
|
||||
"P-256": {
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "slavLyCsXalaOmUgYA-cG4RFNxapMfWK5cnGFZXEsmE",
|
||||
y: "fR3rqDZX_m9ba5OPD4EM4AecqltCTNyXsGmV6e5UYkk",
|
||||
crv: "P-256",
|
||||
d: "HauUPXMuCzvArJpcNJisBzfHq05MVPmHv1Ixd_8Trxk",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "UDVHYHeNMuViKX2ixvJfUdRkP2vYaLH5LEtaZj0Sp5U",
|
||||
y: "ScRWbvlcz4IZc6h4SkUu6ejmho1hOrg3kUgEkpJf4OE",
|
||||
crv: "P-256",
|
||||
d: "xquL6bqtpdDaEoZ0h2HDwpXlPdX__w0xrr8VZLAS2wQ",
|
||||
},
|
||||
expectedSharedKey: {
|
||||
x: "89AFC39D41D3B327814B80940B042590F96556EC91E6AE7939BCE31F3A18BF2B",
|
||||
y: "49C27868F4ECA2179BFD7D59B1E3BF34C1DBDE61AE12931648F43E59632504DE",
|
||||
},
|
||||
},
|
||||
"P-384": {
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "0vsFRhdWTrrQyXF6MJcB3Lg-JjalCra973eA46nFNBwD4oLbTzpL514ZVNIb5nPG",
|
||||
y: "_KZHmMem5pmjhKjAmQ2twvwkssOh5ww28ZXBabgOsCF3MYi-ICkvkqdVvdPNCPaM",
|
||||
crv: "P-384",
|
||||
d: "V7yD-hx8gep_0pw8nFTYD0pRVkxGGN9Uz1zL1Ynq99oSJBG-mceLkwhXACvTKN2o",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "4pgonl58vdZpXrBurolmIw85fAwIY0gMfNRYt1TZWY-kXYz5vKxhi1ycjqotiprR",
|
||||
y: "CCFnRehKJtFX1-0zJWHcXaIpm09B88rq2nhmCRcc5E9NOdgh8dRyJw63o7c7F7XQ",
|
||||
crv: "P-384",
|
||||
d: "40D_ztyfZdBzV3CT8r5JC-SvZfGx4IfUnQNOzbgLcD98zvGcCH04Zs41JCEQ0t-y",
|
||||
},
|
||||
},
|
||||
"P-521": {
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "APD7DZbPsfQ-9tDlc1gFnZGQg5seDx59BmKeFDRERULUqr3EYoxpH-8eutJoXaKCTdzdiXWYpG7nEO99Q6CQuR6Q",
|
||||
y: "ACbcq7utMbQ5XRGwg-O23brBa29Gcaht6TRQvT9k3worJDQlKN-awn8b68HGMb8WEfJe2gFoPAB9D9Cr8sRT5W35",
|
||||
crv: "P-521",
|
||||
d: "AVeaaA3VUhbZfz_oJOCZjhg6gqxXjf96YDJ7rawtAnJDHxtLif7LHVQcOHRKiB6dN9al5KdCGhvo8eekMgRcDXPI",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "AD_GaHOGgjivKoC4C7L5P-fEUzvEpXKFSi32tmscW4NJqiCWQsekuR0EMNgXTw_7GTlrnsn5CcLHfianF3Way-bk",
|
||||
y: "AcJO4Pl7_qys5QS0-XP_Wluy5KgBXZIIG5vmmDLK7Vhsl_30IP6_WPA5JkqEOHZs25uTMdEERvcJlw7EAItGrM5t",
|
||||
crv: "P-521",
|
||||
d: "ADYRyQSF0aLOcYGukdjtFEe5d_pI8wLoU5kNHXfUPoVDHqK02zC-bDRIjJB8f0VWBZYVs88WXFVabUzYlbFwcSi1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe.each(["P-256", "P-384", "P-521"] as const)("curve %s", (curve) => {
|
||||
it("computes the correct shared key for a test vector", async () => {
|
||||
const aPrivateKey = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
testVectors[curve].a,
|
||||
{ name: "ecdh", namedCurve: curve },
|
||||
true,
|
||||
["deriveBits"],
|
||||
);
|
||||
const aPublicKey = Buffer.concat([
|
||||
new Uint8Array([0x04]),
|
||||
Buffer.from(testVectors[curve].a.x, "base64"),
|
||||
Buffer.from(testVectors[curve].a.y, "base64"),
|
||||
]);
|
||||
const bPrivateKey = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
testVectors[curve].b,
|
||||
{ name: "ecdh", namedCurve: curve },
|
||||
true,
|
||||
["deriveBits"],
|
||||
);
|
||||
const bPublicKey = Buffer.concat([
|
||||
new Uint8Array([0x04]),
|
||||
Buffer.from(testVectors[curve].b.x, "base64"),
|
||||
Buffer.from(testVectors[curve].b.y, "base64"),
|
||||
]);
|
||||
|
||||
const actualAB = cryptoFunctionService.deriveSharedKeyBits(
|
||||
aPrivateKey,
|
||||
bPublicKey,
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
|
||||
const actualBA = cryptoFunctionService.deriveSharedKeyBits(
|
||||
bPrivateKey,
|
||||
aPublicKey,
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
|
||||
expect(actualAB).toEqual(actualBA);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testPbkdf2(
|
||||
|
||||
@@ -273,13 +273,16 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return p;
|
||||
}
|
||||
|
||||
aesDecryptFast(parameters: DecryptParameters<string>, mode: "cbc" | "ecb"): Promise<string> {
|
||||
aesDecryptFast(
|
||||
parameters: DecryptParameters<string>,
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): Promise<string> {
|
||||
const decipher = (forge as any).cipher.createDecipher(
|
||||
this.toWebCryptoAesMode(mode),
|
||||
parameters.encKey,
|
||||
);
|
||||
const options = {} as any;
|
||||
if (mode === "cbc") {
|
||||
if (mode === "cbc" || mode === "gcm") {
|
||||
options.iv = parameters.iv;
|
||||
}
|
||||
const dataBuffer = (forge as any).util.createBuffer(parameters.data);
|
||||
@@ -294,21 +297,34 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
data: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb",
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): 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");
|
||||
return Utils.fromByteStringToArray(result);
|
||||
switch (mode) {
|
||||
case "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");
|
||||
return Utils.fromByteStringToArray(result);
|
||||
}
|
||||
case "cbc":
|
||||
case "gcm": {
|
||||
const impKey = await this.subtle.importKey(
|
||||
"raw",
|
||||
key,
|
||||
{ name: this.toWebCryptoAesMode(mode) } as any,
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
const buffer = await this.subtle.decrypt(
|
||||
{ name: this.toWebCryptoAesMode(mode), iv: iv },
|
||||
impKey,
|
||||
data,
|
||||
);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
}
|
||||
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
|
||||
"decrypt",
|
||||
]);
|
||||
const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
async rsaEncrypt(
|
||||
@@ -384,6 +400,61 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return new Uint8Array(rawKey) as CsprngArray;
|
||||
}
|
||||
|
||||
async diffieHellmanGenerateKeyPair(
|
||||
algorithm: "x25519" | "ecdh",
|
||||
curve: undefined | "P-256" | "P-384" | "P-521",
|
||||
): Promise<{
|
||||
keyPair: CryptoKeyPair;
|
||||
publicKey: Uint8Array;
|
||||
}> {
|
||||
if (algorithm === "x25519" && curve != null) {
|
||||
throw new Error("x25519 does not use the curve parameter.");
|
||||
}
|
||||
|
||||
const keys = await this.subtle.generateKey(
|
||||
{
|
||||
name: algorithm,
|
||||
namedCurve: curve,
|
||||
},
|
||||
true,
|
||||
["deriveKey", "deriveBits"],
|
||||
);
|
||||
return {
|
||||
keyPair: keys,
|
||||
publicKey: new Uint8Array(await this.subtle.exportKey("raw", keys.publicKey)),
|
||||
};
|
||||
}
|
||||
|
||||
async deriveSharedKeyBits(
|
||||
privateKey: CryptoKey,
|
||||
publicKeyRaw: Uint8Array,
|
||||
algorithm: "x25519" | "ecdh",
|
||||
curve: undefined | "P-256" | "P-384" | "P-521",
|
||||
): Promise<Uint8Array> {
|
||||
if (algorithm === "x25519" && curve != null) {
|
||||
throw new Error("x25519 does not use the curve parameter.");
|
||||
}
|
||||
|
||||
const publicKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
publicKeyRaw,
|
||||
{ name: algorithm, namedCurve: curve },
|
||||
true,
|
||||
[],
|
||||
);
|
||||
|
||||
const dhSecret = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: algorithm,
|
||||
public: publicKey,
|
||||
},
|
||||
privateKey,
|
||||
256,
|
||||
);
|
||||
|
||||
return new Uint8Array(dhSecret);
|
||||
}
|
||||
|
||||
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]> {
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
@@ -431,8 +502,13 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return algorithm === "sha1" ? "SHA-1" : algorithm === "sha256" ? "SHA-256" : "SHA-512";
|
||||
}
|
||||
|
||||
private toWebCryptoAesMode(mode: "cbc" | "ecb"): string {
|
||||
return mode === "cbc" ? "AES-CBC" : "AES-ECB";
|
||||
private readonly WEB_CRYPTO_AES_MODES = Object.freeze({
|
||||
cbc: "AES-CBC",
|
||||
ecb: "AES-ECB",
|
||||
gcm: "AES-GCM",
|
||||
} as const);
|
||||
private toWebCryptoAesMode(mode: "cbc" | "ecb" | "gcm"): string {
|
||||
return this.WEB_CRYPTO_AES_MODES[mode];
|
||||
}
|
||||
|
||||
// ref: https://stackoverflow.com/a/47880734/1090359
|
||||
|
||||
@@ -210,6 +210,18 @@ 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();
|
||||
@@ -231,6 +243,17 @@ describe("NodeCrypto Function Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecrypt GCM mode", () => {
|
||||
it("successfully decrypts data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(12);
|
||||
const key = makeStaticByteArray(32);
|
||||
const data = Utils.fromB64ToArray("Amy1abyVtlboYFBtLnDAzAwAgb3Qg2m4fMo=");
|
||||
const decValue = await nodeCryptoFunctionService.aesDecrypt(data, iv, key, "gcm");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsaEncrypt", () => {
|
||||
it("should successfully encrypt and then decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
@@ -302,6 +325,238 @@ describe("NodeCrypto Function Service", () => {
|
||||
expect(spy).toHaveBeenCalledWith(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffieHellmanGenerateKeyPair", () => {
|
||||
describe("x25519", () => {
|
||||
it("generates a key pair", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const { keyPair } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
expect(keyPair.privateKey).toBeDefined();
|
||||
expect(keyPair.publicKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("pre-extracts the public key in raw format", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const { keyPair, publicKey } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
const publicFromKeyPair = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
|
||||
expect(publicKey).toEqual(new Uint8Array(publicFromKeyPair));
|
||||
});
|
||||
});
|
||||
|
||||
describe("ecdh", () => {
|
||||
describe.each(["P-256", "P-384", "P-521"] as const)("curve %s", (curve) => {
|
||||
it("generates a key pair", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const { keyPair } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
expect(keyPair.privateKey).toBeDefined();
|
||||
expect(keyPair.publicKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("pre-extracts the public key in raw format", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const { keyPair, publicKey } = await cryptoFunctionService.diffieHellmanGenerateKeyPair(
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
const publicFromKeyPair = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
|
||||
expect(publicKey).toEqual(new Uint8Array(publicFromKeyPair));
|
||||
// Should be odd
|
||||
expect(publicKey.byteLength % 2).toBe(1);
|
||||
// First byte should be 0x04
|
||||
expect(publicKey[0]).toBe(0x04);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveSharedKeyBits", () => {
|
||||
let cryptoFunctionService: NodeCryptoFunctionService;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
});
|
||||
|
||||
describe("x25519", () => {
|
||||
const testVectors = Object.freeze({
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
crv: "X25519",
|
||||
d: "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
|
||||
x: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=",
|
||||
kty: "OKP",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
crv: "X25519",
|
||||
d: "XasIfmJKikt54X+Lg4AO5m87sSkmGLb9HC+LJ/+I4Os=",
|
||||
x: "3p7bfXt9wbTTW2HC7OQ1Nz+DQ8hbeGdNrfx+FG+IK08=",
|
||||
kty: "OKP",
|
||||
},
|
||||
expected: "Sl2dW6TOLeFyjjv0gDUPJeB+IclH0Z4zdvCbPB4WF0I", // hex
|
||||
});
|
||||
|
||||
it("computes the correct shared key for a test vector", async () => {
|
||||
function testVectorToUint8Array(hex: string): Uint8Array {
|
||||
return new Uint8Array(Buffer.from(hex, "hex"));
|
||||
}
|
||||
const aPrivateKey = await crypto.subtle.importKey("jwk", testVectors.a, "x25519", false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
const aPublicKey = new Uint8Array(Buffer.from(testVectors.a.x, "base64"));
|
||||
const bPrivateKey = await crypto.subtle.importKey("jwk", testVectors.b, "x25519", false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
const bPublicKey = new Uint8Array(Buffer.from(testVectors.b.x, "base64"));
|
||||
const expectedSharedKey = testVectorToUint8Array(
|
||||
"4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742",
|
||||
);
|
||||
|
||||
const actualAB = await cryptoFunctionService.deriveSharedKeyBits(
|
||||
aPrivateKey,
|
||||
bPublicKey,
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
const actualBA = await cryptoFunctionService.deriveSharedKeyBits(
|
||||
bPrivateKey,
|
||||
aPublicKey,
|
||||
"x25519",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actualAB).toEqual(expectedSharedKey);
|
||||
expect(actualBA).toEqual(expectedSharedKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ecdh", () => {
|
||||
// from rfc 7027 https://datatracker.ietf.org/doc/html/rfc7027.html#page-6
|
||||
const testVectors = Object.freeze({
|
||||
"P-256": {
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "slavLyCsXalaOmUgYA-cG4RFNxapMfWK5cnGFZXEsmE",
|
||||
y: "fR3rqDZX_m9ba5OPD4EM4AecqltCTNyXsGmV6e5UYkk",
|
||||
crv: "P-256",
|
||||
d: "HauUPXMuCzvArJpcNJisBzfHq05MVPmHv1Ixd_8Trxk",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "UDVHYHeNMuViKX2ixvJfUdRkP2vYaLH5LEtaZj0Sp5U",
|
||||
y: "ScRWbvlcz4IZc6h4SkUu6ejmho1hOrg3kUgEkpJf4OE",
|
||||
crv: "P-256",
|
||||
d: "xquL6bqtpdDaEoZ0h2HDwpXlPdX__w0xrr8VZLAS2wQ",
|
||||
},
|
||||
expectedSharedKey: {
|
||||
x: "89AFC39D41D3B327814B80940B042590F96556EC91E6AE7939BCE31F3A18BF2B",
|
||||
y: "49C27868F4ECA2179BFD7D59B1E3BF34C1DBDE61AE12931648F43E59632504DE",
|
||||
},
|
||||
},
|
||||
"P-384": {
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "0vsFRhdWTrrQyXF6MJcB3Lg-JjalCra973eA46nFNBwD4oLbTzpL514ZVNIb5nPG",
|
||||
y: "_KZHmMem5pmjhKjAmQ2twvwkssOh5ww28ZXBabgOsCF3MYi-ICkvkqdVvdPNCPaM",
|
||||
crv: "P-384",
|
||||
d: "V7yD-hx8gep_0pw8nFTYD0pRVkxGGN9Uz1zL1Ynq99oSJBG-mceLkwhXACvTKN2o",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "4pgonl58vdZpXrBurolmIw85fAwIY0gMfNRYt1TZWY-kXYz5vKxhi1ycjqotiprR",
|
||||
y: "CCFnRehKJtFX1-0zJWHcXaIpm09B88rq2nhmCRcc5E9NOdgh8dRyJw63o7c7F7XQ",
|
||||
crv: "P-384",
|
||||
d: "40D_ztyfZdBzV3CT8r5JC-SvZfGx4IfUnQNOzbgLcD98zvGcCH04Zs41JCEQ0t-y",
|
||||
},
|
||||
},
|
||||
"P-521": {
|
||||
a: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "APD7DZbPsfQ-9tDlc1gFnZGQg5seDx59BmKeFDRERULUqr3EYoxpH-8eutJoXaKCTdzdiXWYpG7nEO99Q6CQuR6Q",
|
||||
y: "ACbcq7utMbQ5XRGwg-O23brBa29Gcaht6TRQvT9k3worJDQlKN-awn8b68HGMb8WEfJe2gFoPAB9D9Cr8sRT5W35",
|
||||
crv: "P-521",
|
||||
d: "AVeaaA3VUhbZfz_oJOCZjhg6gqxXjf96YDJ7rawtAnJDHxtLif7LHVQcOHRKiB6dN9al5KdCGhvo8eekMgRcDXPI",
|
||||
},
|
||||
b: {
|
||||
key_ops: ["deriveKey", "deriveBits"],
|
||||
ext: true,
|
||||
kty: "EC",
|
||||
x: "AD_GaHOGgjivKoC4C7L5P-fEUzvEpXKFSi32tmscW4NJqiCWQsekuR0EMNgXTw_7GTlrnsn5CcLHfianF3Way-bk",
|
||||
y: "AcJO4Pl7_qys5QS0-XP_Wluy5KgBXZIIG5vmmDLK7Vhsl_30IP6_WPA5JkqEOHZs25uTMdEERvcJlw7EAItGrM5t",
|
||||
crv: "P-521",
|
||||
d: "ADYRyQSF0aLOcYGukdjtFEe5d_pI8wLoU5kNHXfUPoVDHqK02zC-bDRIjJB8f0VWBZYVs88WXFVabUzYlbFwcSi1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe.each(["P-256", "P-384", "P-521"] as const)("curve %s", (curve) => {
|
||||
it("computes the correct shared key for a test vector", async () => {
|
||||
const aPrivateKey = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
testVectors[curve].a,
|
||||
{ name: "ecdh", namedCurve: curve },
|
||||
true,
|
||||
["deriveBits"],
|
||||
);
|
||||
const aPublicKey = Buffer.concat([
|
||||
new Uint8Array([0x04]),
|
||||
Buffer.from(testVectors[curve].a.x, "base64"),
|
||||
Buffer.from(testVectors[curve].a.y, "base64"),
|
||||
]);
|
||||
const bPrivateKey = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
testVectors[curve].b,
|
||||
{ name: "ecdh", namedCurve: curve },
|
||||
true,
|
||||
["deriveBits"],
|
||||
);
|
||||
const bPublicKey = Buffer.concat([
|
||||
new Uint8Array([0x04]),
|
||||
Buffer.from(testVectors[curve].b.x, "base64"),
|
||||
Buffer.from(testVectors[curve].b.y, "base64"),
|
||||
]);
|
||||
|
||||
const actualAB = cryptoFunctionService.deriveSharedKeyBits(
|
||||
aPrivateKey,
|
||||
bPublicKey,
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
|
||||
const actualBA = cryptoFunctionService.deriveSharedKeyBits(
|
||||
bPrivateKey,
|
||||
aPublicKey,
|
||||
"ecdh",
|
||||
curve,
|
||||
);
|
||||
|
||||
expect(actualAB).toEqual(actualBA);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testPbkdf2(
|
||||
|
||||
@@ -191,7 +191,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
|
||||
async aesDecryptFast(
|
||||
parameters: DecryptParameters<Uint8Array>,
|
||||
mode: "cbc" | "ecb",
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): Promise<string> {
|
||||
const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey, mode);
|
||||
return Utils.fromBufferToUtf8(decBuf);
|
||||
@@ -201,12 +201,16 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
data: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb",
|
||||
mode: "cbc" | "ecb" | "gcm",
|
||||
): Promise<Uint8Array> {
|
||||
const nodeData = this.toNodeBuffer(data);
|
||||
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));
|
||||
}
|
||||
const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]);
|
||||
return Promise.resolve(this.toUint8Buffer(decBuf));
|
||||
}
|
||||
@@ -279,6 +283,61 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
});
|
||||
}
|
||||
|
||||
async diffieHellmanGenerateKeyPair(
|
||||
algorithm: "x25519" | "ecdh",
|
||||
curve: undefined | "P-256" | "P-384" | "P-521",
|
||||
): Promise<{
|
||||
keyPair: CryptoKeyPair;
|
||||
publicKey: Uint8Array;
|
||||
}> {
|
||||
if (algorithm === "x25519" && curve != null) {
|
||||
throw new Error("x25519 does not use the curve parameter.");
|
||||
}
|
||||
|
||||
const keys = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: algorithm,
|
||||
namedCurve: curve,
|
||||
},
|
||||
true,
|
||||
["deriveKey", "deriveBits"],
|
||||
);
|
||||
return {
|
||||
keyPair: keys,
|
||||
publicKey: new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey)),
|
||||
};
|
||||
}
|
||||
|
||||
async deriveSharedKeyBits(
|
||||
privateKey: CryptoKey,
|
||||
publicKeyRaw: Uint8Array,
|
||||
algorithm: "x25519" | "ecdh",
|
||||
curve: undefined | "P-256" | "P-384" | "P-521",
|
||||
): Promise<Uint8Array> {
|
||||
if (algorithm === "x25519" && curve != null) {
|
||||
throw new Error("x25519 does not use the curve parameter.");
|
||||
}
|
||||
|
||||
const publicKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
publicKeyRaw,
|
||||
{ name: algorithm, namedCurve: curve },
|
||||
true,
|
||||
[],
|
||||
);
|
||||
|
||||
const dhSecret = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: algorithm,
|
||||
public: publicKey,
|
||||
},
|
||||
privateKey,
|
||||
256,
|
||||
);
|
||||
|
||||
return new Uint8Array(dhSecret);
|
||||
}
|
||||
|
||||
aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise<CsprngArray> {
|
||||
return this.randomBytes(bitLength / 8);
|
||||
}
|
||||
@@ -335,7 +394,12 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
return forge.pki.publicKeyToPem(publicKey);
|
||||
}
|
||||
|
||||
private toNodeCryptoAesMode(mode: "cbc" | "ecb"): string {
|
||||
return mode === "cbc" ? "aes-256-cbc" : "aes-256-ecb";
|
||||
private readonly NODE_CRYPTO_AES_MODES = Object.freeze({
|
||||
cbc: "aes-256-cbc",
|
||||
ecb: "aes-256-ecb",
|
||||
gcm: "aes-256-gcm",
|
||||
} as const);
|
||||
private toNodeCryptoAesMode(mode: "cbc" | "ecb" | "gcm"): string {
|
||||
return this.NODE_CRYPTO_AES_MODES[mode];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user