diff --git a/package-lock.json b/package-lock.json index 7a5b906..e6d8f7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "6.2.0", "license": "CC0-1.0", "dependencies": { + "@noble/ciphers": "^1.3.0", + "@noble/hashes": "^1.8.0", + "@noble/secp256k1": "^2.2.3", "elliptic": "^6.6.1" }, "devDependencies": { @@ -2888,6 +2891,39 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz", + "integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -14542,6 +14578,21 @@ "@tybys/wasm-util": "^0.10.0" } }, + "@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==" + }, + "@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + }, + "@noble/secp256k1": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz", + "integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 4301626..cac4b50 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,9 @@ "npm": ">=10.x" }, "dependencies": { + "@noble/secp256k1": "^2.2.3", + "@noble/ciphers": "^1.3.0", + "@noble/hashes": "^1.8.0", "elliptic": "^6.6.1" } } diff --git a/src/nobleEncryption.ts b/src/nobleEncryption.ts new file mode 100644 index 0000000..d672bc2 --- /dev/null +++ b/src/nobleEncryption.ts @@ -0,0 +1,139 @@ +import { cbc } from "@noble/ciphers/aes"; +import { hmac } from "@noble/hashes/hmac"; +import { sha256, sha512 } from "@noble/hashes/sha2"; +import { getPublicKey, getSharedSecret, utils } from "@noble/secp256k1"; + +import { Ecies } from "."; + +export interface NobleEcies { + iv: Uint8Array; + ephemPublicKey: Uint8Array; + ciphertext: Uint8Array; + mac: Uint8Array; +} +// Compare two buffers in constant time to prevent timing attacks. +function equalConstTime(b1: Uint8Array, b2: Uint8Array): boolean { + if (b1.length !== b2.length) { + return false; + } + let res = 0; + for (let i = 0; i < b1.length; i++) { + res |= b1[i] ^ b2[i]; // jshint ignore:line + } + + return res === 0; +} + +export const NobleEciesToEcies = (nobleEcies: NobleEcies): Ecies => { + return { + iv: Buffer.from(nobleEcies.iv), + ciphertext: Buffer.from(nobleEcies.ciphertext), + mac: Buffer.from(nobleEcies.mac), + ephemPublicKey: Buffer.from(nobleEcies.ephemPublicKey), + }; +}; + +export const EciesToNobleEcies = (ecies: Ecies): NobleEcies => { + return { + iv: new Uint8Array(ecies.iv), + ephemPublicKey: new Uint8Array(ecies.ephemPublicKey), + ciphertext: new Uint8Array(ecies.ciphertext), + mac: new Uint8Array(ecies.mac), + }; +}; + +export const hmacSha256Sign = (key: Uint8Array, msg: Uint8Array) => { + const mac = hmac(sha256, key, msg); + return mac; +}; + +export function hmacSha256Verify(key: Uint8Array, msg: Uint8Array, sig: Uint8Array): boolean { + const expectedSig = hmacSha256Sign(key, msg); + return equalConstTime(expectedSig, sig); +} + +export const nobleEncrypt = async function ( + publicKeyTo: Uint8Array, + msg: Uint8Array, + opts?: { iv?: Uint8Array; ephemPrivateKey?: Uint8Array } +): Promise { + const ephemPrivateKey = opts?.ephemPrivateKey || utils.randomPrivateKey(); + const ephemPublicKey = getPublicKey(ephemPrivateKey, false); + + const sharedSecret = getSharedSecret(ephemPrivateKey, publicKeyTo); + + // need to remove first byte + const sharedSecretSliced = sharedSecret.slice(1); + + const hash = sha512(sharedSecretSliced); + const key = hash.slice(0, 32); + const macKey = hash.slice(32); + + const iv = opts?.iv || utils.randomPrivateKey().slice(0, 16); + const cipher = cbc(key, iv); + + const cipherText = cipher.encrypt(msg); + + const dataToMac = new Uint8Array(iv.length + ephemPublicKey.length + cipherText.length); + dataToMac.set(iv, 0); + dataToMac.set(ephemPublicKey, iv.length); + dataToMac.set(cipherText, iv.length + ephemPublicKey.length); + const mac = hmacSha256Sign(Buffer.from(macKey), dataToMac); + + return { + iv, + ephemPublicKey, + ciphertext: cipherText, + mac, + }; +}; + +export const nobleDecrypt = async function (privateKey: Uint8Array, opts: NobleEcies, padding?: boolean): Promise { + const { iv, ephemPublicKey, ciphertext, mac } = opts; + const sharedSecret = getSharedSecret(privateKey, ephemPublicKey); + // need to remove first byte + let sharedSecretSliced = sharedSecret.slice(1); + + if (!padding) { + while (sharedSecretSliced.at(0) === 0) { + sharedSecretSliced = sharedSecretSliced.slice(1); + } + } + + const hash = sha512(sharedSecretSliced); + const key = hash.slice(0, 32); + const macKey = hash.slice(32); + + const dataToMac = new Uint8Array(iv.length + ephemPublicKey.length + ciphertext.length); + dataToMac.set(iv, 0); + dataToMac.set(ephemPublicKey, iv.length); + dataToMac.set(ciphertext, iv.length + ephemPublicKey.length); + const macGood = hmacSha256Verify(macKey, dataToMac, mac); + + if (!macGood && !padding) { + return nobleDecrypt(privateKey, opts, true); + } else if (!macGood && padding === true) { + throw new Error("bad MAC after trying padded"); + } + + const cipher = cbc(key, iv); + const decrypted = cipher.decrypt(ciphertext); + + return decrypted; +}; + +export const encrypt = async function ( + publicKeyTo: Buffer, + msg: Buffer, + opts?: { iv?: Buffer; ephemPrivateKey?: Buffer; padding?: boolean } +): Promise { + if (opts?.padding !== undefined) throw new Error("padding opts is not supported"); + const nobleEcies = await nobleEncrypt(publicKeyTo, msg, opts); + return NobleEciesToEcies(nobleEcies); +}; + +export const decrypt = async function (privateKey: Buffer, opts: Ecies, padding?: boolean): Promise { + const nobleEcies = EciesToNobleEcies(opts); + const decrypted = await nobleDecrypt(privateKey, nobleEcies, padding); + return Buffer.from(decrypted); +}; diff --git a/test/encrypt_decrypt.spec.ts b/test/encrypt_decrypt.spec.ts index ce387a2..9e25fc5 100644 --- a/test/encrypt_decrypt.spec.ts +++ b/test/encrypt_decrypt.spec.ts @@ -1,7 +1,10 @@ /* eslint-disable import/no-extraneous-dependencies */ +import { bytesToUtf8 } from "@noble/ciphers/utils"; +import { getPublicKey } from "@noble/secp256k1"; import { beforeEach, describe, expect, it } from "vitest"; import * as eccrypto from "../src/index"; +import { decrypt, encrypt, nobleDecrypt, NobleEciesToEcies, nobleEncrypt } from "../src/nobleEncryption"; describe("Functions: encrypt & decrypt", () => { let ephemPublicKey: Buffer; @@ -14,8 +17,9 @@ describe("Functions: encrypt & decrypt", () => { let publicKey: Buffer; let publicKeyCompressed: Buffer; + let ephemPrivateKey: Buffer; beforeEach(() => { - const ephemPrivateKey = Buffer.alloc(32).fill(4); + ephemPrivateKey = Buffer.alloc(32).fill(4); ephemPublicKey = eccrypto.getPublic(ephemPrivateKey); iv = Buffer.alloc(16).fill(5); ciphertext = Buffer.from("bbf3f0e7486b552b0e2ba9c4ca8c4579", "hex"); @@ -181,4 +185,39 @@ describe("Functions: encrypt & decrypt", () => { expect(message.toString()).toBe("generated private key"); }); }); + + describe("ECIES:encrypt -> decrypt withnoble/ciphers", () => { + it("should encrypt and decrypt", async () => { + const message = "random message which is very very long and should be encrypted"; + const nobleEcies = await nobleEncrypt(getPublicKey(privateKey), Buffer.from(message), { + iv: iv, + ephemPrivateKey: ephemPrivateKey, + }); + + const convertedEcies = NobleEciesToEcies(nobleEcies); + + const encrypted = await eccrypto.encrypt(publicKey, Buffer.from(message), { + iv: iv, + ephemPrivateKey: ephemPrivateKey, + }); + + const wrappedEncrypted = await encrypt(publicKey, Buffer.from(message), { + iv: iv, + ephemPrivateKey: ephemPrivateKey, + }); + + expect(convertedEcies).toEqual(encrypted); + + expect(wrappedEncrypted).toEqual(encrypted); + + const decrypted = await nobleDecrypt(privateKey, nobleEcies); + expect(bytesToUtf8(decrypted)).toBe(message); + + const decrypted1 = await eccrypto.decrypt(privateKey, convertedEcies); + expect(bytesToUtf8(decrypted1)).toBe(message); + + const wrappedDecrypted = await decrypt(privateKey, encrypted); + expect(bytesToUtf8(wrappedDecrypted)).toBe(message); + }); + }); });