diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index 18c14677dd0..9ba09d0879d 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -52,15 +52,36 @@ export abstract class CryptoFunctionService { mac: string, key: SymmetricCryptoKey, ): DecryptParameters; + /** + * 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, - mode: "cbc" | "ecb", + mode: "cbc" | "ecb" | "gcm", ): Promise; + /** + * 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; 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; + /** + * 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; } diff --git a/libs/common/src/platform/services/web-crypto-function.service.spec.ts b/libs/common/src/platform/services/web-crypto-function.service.spec.ts index 71f2828855f..2ed7e656e1e 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.spec.ts @@ -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( diff --git a/libs/common/src/platform/services/web-crypto-function.service.ts b/libs/common/src/platform/services/web-crypto-function.service.ts index fd0763714ad..3c115393e64 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.ts @@ -273,13 +273,16 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return p; } - aesDecryptFast(parameters: DecryptParameters, mode: "cbc" | "ecb"): Promise { + aesDecryptFast( + parameters: DecryptParameters, + mode: "cbc" | "ecb" | "gcm", + ): Promise { 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 { - if (mode === "ecb") { - // Web crypto does not support AES-ECB mode, so we need to do this in forge. - const params = new DecryptParameters(); - 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(); + 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 { + 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 diff --git a/libs/node/src/services/node-crypto-function.service.spec.ts b/libs/node/src/services/node-crypto-function.service.spec.ts index 61200b92855..201b3810b15 100644 --- a/libs/node/src/services/node-crypto-function.service.spec.ts +++ b/libs/node/src/services/node-crypto-function.service.spec.ts @@ -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( diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index c8d676eeaa5..64239da003f 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -191,7 +191,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { async aesDecryptFast( parameters: DecryptParameters, - mode: "cbc" | "ecb", + mode: "cbc" | "ecb" | "gcm", ): Promise { 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 { - 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 { + 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 { 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]; } }