@toruslabs/eccrypto
Advanced tools
+76
-97
| 'use strict'; | ||
| var elliptic = require('elliptic'); | ||
| var secp256k1_js = require('@noble/curves/secp256k1.js'); | ||
| var utils_js = require('@noble/curves/utils.js'); | ||
| const ec = new elliptic.ec("secp256k1"); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, n/no-unsupported-features/node-builtins | ||
| const browserCrypto = globalThis.crypto || globalThis.msCrypto || {}; | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, n/no-unsupported-features/node-builtins | ||
| const subtle = browserCrypto.subtle || browserCrypto.webkitSubtle; | ||
| const EC_GROUP_ORDER = Buffer.from("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", "hex"); | ||
| const ZERO32 = Buffer.alloc(32, 0); | ||
| const SECP256K1_GROUP_ORDER = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); | ||
| function assert(condition, message) { | ||
@@ -17,24 +16,11 @@ if (!condition) { | ||
| } | ||
| function isScalar(x) { | ||
| return Buffer.isBuffer(x) && x.length === 32; | ||
| } | ||
| function isValidPrivateKey(privateKey) { | ||
| if (!isScalar(privateKey)) { | ||
| if (privateKey.length !== 32) { | ||
| return false; | ||
| } | ||
| return privateKey.compare(ZERO32) > 0 && | ||
| const privateKeyBigInt = utils_js.bytesToNumberBE(privateKey); | ||
| return privateKeyBigInt > 0n && | ||
| // > 0 | ||
| privateKey.compare(EC_GROUP_ORDER) < 0; // < G | ||
| privateKeyBigInt < SECP256K1_GROUP_ORDER; // < G | ||
| } | ||
| // Compare two buffers in constant time to prevent timing attacks. | ||
| function equalConstTime(b1, b2) { | ||
| 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; | ||
| } | ||
| /* This must check if we're in the browser or | ||
@@ -45,7 +31,7 @@ not, since the functions are different and does | ||
| if (typeof browserCrypto.getRandomValues === "undefined") { | ||
| return Buffer.from(browserCrypto.randomBytes(size)); | ||
| return browserCrypto.randomBytes(size); | ||
| } | ||
| const arr = new Uint8Array(size); | ||
| browserCrypto.getRandomValues(arr); | ||
| return Buffer.from(arr); | ||
| return arr; | ||
| } | ||
@@ -75,3 +61,3 @@ async function sha512(msg) { | ||
| const result = await subtle[op](encAlgorithm, cryptoKey, data); | ||
| return Buffer.from(new Uint8Array(result)); | ||
| return new Uint8Array(result); | ||
| } else if (op === "encrypt" && browserCrypto.createCipheriv) { | ||
@@ -82,3 +68,3 @@ // This is available if crypto is polyfilled in react native environment | ||
| const secondChunk = cipher.final(); | ||
| return Buffer.concat([firstChunk, secondChunk]); | ||
| return utils_js.concatBytes(firstChunk, secondChunk); | ||
| } else if (op === "decrypt" && browserCrypto.createDecipheriv) { | ||
@@ -88,3 +74,3 @@ const decipher = browserCrypto.createDecipheriv("aes-256-cbc", key, iv); | ||
| const secondChunk = decipher.final(); | ||
| return Buffer.concat([firstChunk, secondChunk]); | ||
| return utils_js.concatBytes(firstChunk, secondChunk); | ||
| } | ||
@@ -104,8 +90,8 @@ throw new Error(`Unsupported operation: ${op}`); | ||
| }; | ||
| const cryptoKey = await subtle.importKey("raw", new Uint8Array(key), importAlgorithm, false, ["sign", "verify"]); | ||
| const cryptoKey = await subtle.importKey("raw", key, importAlgorithm, false, ["sign", "verify"]); | ||
| const sig = await subtle.sign("HMAC", cryptoKey, msg); | ||
| const result = Buffer.from(new Uint8Array(sig)); | ||
| const result = new Uint8Array(sig); | ||
| return result; | ||
| } | ||
| const hmac = browserCrypto.createHmac("sha256", Buffer.from(key)); | ||
| const hmac = browserCrypto.createHmac("sha256", key); | ||
| hmac.update(msg); | ||
@@ -117,4 +103,15 @@ const result = hmac.digest(); | ||
| const expectedSig = await hmacSha256Sign(key, msg); | ||
| return equalConstTime(expectedSig, sig); | ||
| return utils_js.equalBytes(expectedSig, sig); | ||
| } | ||
| function assertValidPrivateKey(privateKey) { | ||
| assert(isValidPrivateKey(privateKey), "Bad private key"); | ||
| } | ||
| function assertValidPublicKey(publicKey) { | ||
| const isValid = secp256k1_js.secp256k1.utils.isValidPublicKey(publicKey, true) || secp256k1_js.secp256k1.utils.isValidPublicKey(publicKey, false); | ||
| assert(isValid, "Bad public key"); | ||
| } | ||
| function assertValidMessage(msg) { | ||
| assert(msg.length > 0, "Message should not be empty"); | ||
| assert(msg.length <= 32, "Message is too long"); | ||
| } | ||
| /** | ||
@@ -132,7 +129,4 @@ * Generate a new valid private key. Will use the window.crypto or window.msCrypto as source | ||
| const getPublic = function (privateKey) { | ||
| // This function has sync API so we throw an error immediately. | ||
| assertPrivateKey(privateKey); | ||
| // XXX(Kagami): `elliptic.utils.encode` returns array for every | ||
| // encoding except `hex`. | ||
| return Buffer.from(ec.keyFromPrivate(privateKey).getPublic("array")); | ||
| assertValidPrivateKey(privateKey); | ||
| return secp256k1_js.secp256k1.getPublicKey(privateKey, false); | ||
| }; | ||
@@ -143,62 +137,45 @@ /** | ||
| const getPublicCompressed = function (privateKey) { | ||
| assertPrivateKey(privateKey); | ||
| // See https://github.com/wanderer/secp256k1-node/issues/46 | ||
| const compressed = true; | ||
| return Buffer.from(ec.keyFromPrivate(privateKey).getPublic(compressed, "array")); | ||
| assertValidPrivateKey(privateKey); | ||
| return secp256k1_js.secp256k1.getPublicKey(privateKey); | ||
| }; | ||
| // NOTE(Kagami): We don't use promise shim in Browser implementation | ||
| // because it's supported natively in new browsers (see | ||
| // <http://caniuse.com/#feat=promises>) and we can use only new browsers | ||
| // because of the WebCryptoAPI (see | ||
| // <http://caniuse.com/#feat=cryptography>). | ||
| const sign = async function (privateKey, msg) { | ||
| assertPrivateKey(privateKey); | ||
| assert(msg.length > 0, "Message should not be empty"); | ||
| assert(msg.length <= 32, "Message is too long"); | ||
| return Buffer.from(ec.sign(msg, privateKey, { | ||
| canonical: true | ||
| }).toDER()); | ||
| assertValidPrivateKey(privateKey); | ||
| assertValidMessage(msg); | ||
| const sig = secp256k1_js.secp256k1.sign(msg, privateKey, { | ||
| prehash: false, | ||
| format: "der" | ||
| }); | ||
| return sig; | ||
| }; | ||
| const assertPublicKey = function (publicKey) { | ||
| assert(publicKey.length === 65 || publicKey.length === 33, "Bad public key: expected 65 or 33 bytes, got " + publicKey.length); | ||
| if (publicKey.length === 65) { | ||
| assert(publicKey[0] === 4, "Bad public key: expected first byte 4, got " + publicKey[0]); | ||
| } | ||
| if (publicKey.length === 33) { | ||
| assert(publicKey[0] === 2 || publicKey[0] === 3, "Bad public key: expected first byte 2 or 3, got " + publicKey[0]); | ||
| } | ||
| }; | ||
| const assertPrivateKey = function (privateKey) { | ||
| assert(Buffer.isBuffer(privateKey), "Bad private key: expected Buffer"); | ||
| assert(privateKey.length === 32, "Bad private key: expected 32 bytes, got " + privateKey.length); | ||
| assert(isValidPrivateKey(privateKey), "Bad private key: out of range"); | ||
| }; | ||
| const verify = async function (publicKey, msg, sig) { | ||
| assertPublicKey(publicKey); | ||
| assert(msg.length > 0, "Message should not be empty"); | ||
| assert(msg.length <= 32, "Message is too long"); | ||
| if (ec.verify(msg, sig, publicKey)) { | ||
| return null; | ||
| } | ||
| assertValidPublicKey(publicKey); | ||
| assertValidMessage(msg); | ||
| if (secp256k1_js.secp256k1.verify(sig, msg, publicKey, { | ||
| prehash: false, | ||
| format: "der" | ||
| })) return null; | ||
| throw new Error("Bad signature"); | ||
| }; | ||
| const derive = async function (privateKeyA, publicKeyB, padding) { | ||
| assertPrivateKey(privateKeyA); | ||
| assertPublicKey(publicKeyB); | ||
| const keyA = ec.keyFromPrivate(privateKeyA); | ||
| const keyB = ec.keyFromPublic(publicKeyB); | ||
| const Px = keyA.derive(keyB.getPublic()); // BN instance | ||
| if (padding) { | ||
| return Buffer.from(Px.toString(16, 64), "hex"); | ||
| } | ||
| return Buffer.from(Px.toArray()); | ||
| const derive = async function (privateKeyA, publicKeyB) { | ||
| assertValidPrivateKey(privateKeyA); | ||
| assertValidPublicKey(publicKeyB); | ||
| // Strip leading zeros for backwards compatibility with older versions | ||
| // that used elliptic's BN.toArray() which didn't include leading zeros. | ||
| // Use derivePadded() if you need a fixed 32-byte output. | ||
| const sharedSecret = secp256k1_js.secp256k1.getSharedSecret(privateKeyA, publicKeyB); | ||
| const Px = sharedSecret.subarray(1); | ||
| const i = Px.findIndex(byte => byte !== 0); | ||
| return Px.subarray(i); | ||
| }; | ||
| const deriveUnpadded = derive; | ||
| const derivePadded = async function (privateKeyA, publicKeyB) { | ||
| return derive(privateKeyA, publicKeyB, true); | ||
| assertValidPrivateKey(privateKeyA); | ||
| assertValidPublicKey(publicKeyB); | ||
| const sharedSecret = secp256k1_js.secp256k1.getSharedSecret(privateKeyA, publicKeyB); | ||
| return sharedSecret.subarray(1); | ||
| }; | ||
| const deriveUnpadded = async function (privateKeyA, publicKeyB) { | ||
| return derive(privateKeyA, publicKeyB, false); | ||
| }; | ||
| const encrypt = async function (publicKeyTo, msg, opts) { | ||
| var _opts$padding; | ||
| opts = opts || {}; | ||
| const padding = (_opts$padding = opts.padding) !== null && _opts$padding !== void 0 ? _opts$padding : true; | ||
| let ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32); | ||
@@ -210,3 +187,4 @@ // There is a very unlikely possibility that it is not a valid key | ||
| const ephemPublicKey = getPublic(ephemPrivateKey); | ||
| const Px = await derive(ephemPrivateKey, publicKeyTo, opts.padding); | ||
| const deriveLocal = padding ? derivePadded : deriveUnpadded; | ||
| const Px = await deriveLocal(ephemPrivateKey, publicKeyTo); | ||
| const hash = await sha512(Px); | ||
@@ -216,6 +194,5 @@ const iv = opts.iv || randomBytes(16); | ||
| const macKey = hash.slice(32); | ||
| const data = await aesCbcEncrypt(iv, Buffer.from(encryptionKey), msg); | ||
| const ciphertext = data; | ||
| const dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]); | ||
| const mac = await hmacSha256Sign(Buffer.from(macKey), dataToMac); | ||
| const ciphertext = await aesCbcEncrypt(iv, encryptionKey, msg); | ||
| const dataToMac = utils_js.concatBytes(iv, ephemPublicKey, ciphertext); | ||
| const mac = await hmacSha256Sign(macKey, dataToMac); | ||
| return { | ||
@@ -228,10 +205,12 @@ iv, | ||
| }; | ||
| const decrypt = async function (privateKey, opts, padding) { | ||
| const Px = await derive(privateKey, opts.ephemPublicKey, padding); | ||
| const decrypt = async function (privateKey, opts, _padding) { | ||
| const padding = _padding !== null && _padding !== void 0 ? _padding : false; | ||
| const deriveLocal = padding ? derivePadded : deriveUnpadded; | ||
| const Px = await deriveLocal(privateKey, opts.ephemPublicKey); | ||
| const hash = await sha512(Px); | ||
| const encryptionKey = hash.slice(0, 32); | ||
| const macKey = hash.slice(32); | ||
| const dataToMac = Buffer.concat([opts.iv, opts.ephemPublicKey, opts.ciphertext]); | ||
| const macGood = await hmacSha256Verify(Buffer.from(macKey), dataToMac, opts.mac); | ||
| if (!macGood && !padding) { | ||
| const dataToMac = utils_js.concatBytes(opts.iv, opts.ephemPublicKey, opts.ciphertext); | ||
| const macGood = await hmacSha256Verify(macKey, dataToMac, opts.mac); | ||
| if (!macGood && padding === false) { | ||
| return decrypt(privateKey, opts, true); | ||
@@ -241,4 +220,4 @@ } else if (!macGood && padding === true) { | ||
| } | ||
| const msg = await aesCbcDecrypt(opts.iv, Buffer.from(encryptionKey), opts.ciphertext); | ||
| return Buffer.from(new Uint8Array(msg)); | ||
| const msg = await aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext); | ||
| return msg; | ||
| }; | ||
@@ -245,0 +224,0 @@ |
| export interface Ecies { | ||
| iv: Buffer; | ||
| ephemPublicKey: Buffer; | ||
| ciphertext: Buffer; | ||
| mac: Buffer; | ||
| iv: Uint8Array; | ||
| ephemPublicKey: Uint8Array; | ||
| ciphertext: Uint8Array; | ||
| mac: Uint8Array; | ||
| } | ||
@@ -11,18 +11,18 @@ /** | ||
| */ | ||
| export declare const generatePrivate: () => Buffer; | ||
| export declare const getPublic: (privateKey: Buffer) => Buffer; | ||
| export declare const generatePrivate: () => Uint8Array; | ||
| export declare const getPublic: (privateKey: Uint8Array) => Uint8Array; | ||
| /** | ||
| * Get compressed version of public key. | ||
| */ | ||
| export declare const getPublicCompressed: (privateKey: Buffer) => Buffer; | ||
| export declare const sign: (privateKey: Buffer, msg: Buffer) => Promise<Buffer>; | ||
| export declare const verify: (publicKey: Buffer, msg: Buffer, sig: Buffer) => Promise<null>; | ||
| export declare const derive: (privateKeyA: Buffer, publicKeyB: Buffer, padding?: boolean) => Promise<Buffer>; | ||
| export declare const derivePadded: (privateKeyA: Buffer, publicKeyB: Buffer) => Promise<Buffer>; | ||
| export declare const deriveUnpadded: (privateKeyA: Buffer, publicKeyB: Buffer) => Promise<Buffer>; | ||
| export declare const encrypt: (publicKeyTo: Buffer, msg: Buffer, opts?: { | ||
| iv?: Buffer; | ||
| ephemPrivateKey?: Buffer; | ||
| export declare const getPublicCompressed: (privateKey: Uint8Array) => Uint8Array; | ||
| export declare const sign: (privateKey: Uint8Array, msg: Uint8Array) => Promise<Uint8Array>; | ||
| export declare const verify: (publicKey: Uint8Array, msg: Uint8Array, sig: Uint8Array) => Promise<null>; | ||
| export declare const derive: (privateKeyA: Uint8Array, publicKeyB: Uint8Array) => Promise<Uint8Array>; | ||
| export declare const deriveUnpadded: (privateKeyA: Uint8Array, publicKeyB: Uint8Array) => Promise<Uint8Array>; | ||
| export declare const derivePadded: (privateKeyA: Uint8Array, publicKeyB: Uint8Array) => Promise<Uint8Array>; | ||
| export declare const encrypt: (publicKeyTo: Uint8Array, msg: Uint8Array, opts?: { | ||
| iv?: Uint8Array; | ||
| ephemPrivateKey?: Uint8Array; | ||
| padding?: boolean; | ||
| }) => Promise<Ecies>; | ||
| export declare const decrypt: (privateKey: Buffer, opts: Ecies, padding?: boolean) => Promise<Buffer>; | ||
| export declare const decrypt: (privateKey: Uint8Array, opts: Ecies, _padding?: boolean) => Promise<Uint8Array>; |
+77
-99
@@ -1,10 +0,9 @@ | ||
| import { ec as ec$1 } from 'elliptic'; | ||
| import { secp256k1 } from '@noble/curves/secp256k1.js'; | ||
| import { concatBytes, bytesToNumberBE, equalBytes } from '@noble/curves/utils.js'; | ||
| const ec = new ec$1("secp256k1"); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, n/no-unsupported-features/node-builtins | ||
| const browserCrypto = globalThis.crypto || globalThis.msCrypto || {}; | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, n/no-unsupported-features/node-builtins | ||
| const subtle = browserCrypto.subtle || browserCrypto.webkitSubtle; | ||
| const EC_GROUP_ORDER = Buffer.from("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", "hex"); | ||
| const ZERO32 = Buffer.alloc(32, 0); | ||
| const SECP256K1_GROUP_ORDER = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); | ||
| function assert(condition, message) { | ||
@@ -15,26 +14,12 @@ if (!condition) { | ||
| } | ||
| function isScalar(x) { | ||
| return Buffer.isBuffer(x) && x.length === 32; | ||
| } | ||
| function isValidPrivateKey(privateKey) { | ||
| if (!isScalar(privateKey)) { | ||
| if (privateKey.length !== 32) { | ||
| return false; | ||
| } | ||
| return privateKey.compare(ZERO32) > 0 && | ||
| const privateKeyBigInt = bytesToNumberBE(privateKey); | ||
| return privateKeyBigInt > 0n && | ||
| // > 0 | ||
| privateKey.compare(EC_GROUP_ORDER) < 0; // < G | ||
| privateKeyBigInt < SECP256K1_GROUP_ORDER; // < G | ||
| } | ||
| // Compare two buffers in constant time to prevent timing attacks. | ||
| function equalConstTime(b1, b2) { | ||
| 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; | ||
| } | ||
| /* This must check if we're in the browser or | ||
@@ -45,7 +30,7 @@ not, since the functions are different and does | ||
| if (typeof browserCrypto.getRandomValues === "undefined") { | ||
| return Buffer.from(browserCrypto.randomBytes(size)); | ||
| return browserCrypto.randomBytes(size); | ||
| } | ||
| const arr = new Uint8Array(size); | ||
| browserCrypto.getRandomValues(arr); | ||
| return Buffer.from(arr); | ||
| return arr; | ||
| } | ||
@@ -75,3 +60,3 @@ async function sha512(msg) { | ||
| const result = await subtle[op](encAlgorithm, cryptoKey, data); | ||
| return Buffer.from(new Uint8Array(result)); | ||
| return new Uint8Array(result); | ||
| } else if (op === "encrypt" && browserCrypto.createCipheriv) { | ||
@@ -82,3 +67,3 @@ // This is available if crypto is polyfilled in react native environment | ||
| const secondChunk = cipher.final(); | ||
| return Buffer.concat([firstChunk, secondChunk]); | ||
| return concatBytes(firstChunk, secondChunk); | ||
| } else if (op === "decrypt" && browserCrypto.createDecipheriv) { | ||
@@ -88,3 +73,3 @@ const decipher = browserCrypto.createDecipheriv("aes-256-cbc", key, iv); | ||
| const secondChunk = decipher.final(); | ||
| return Buffer.concat([firstChunk, secondChunk]); | ||
| return concatBytes(firstChunk, secondChunk); | ||
| } | ||
@@ -104,8 +89,8 @@ throw new Error(`Unsupported operation: ${op}`); | ||
| }; | ||
| const cryptoKey = await subtle.importKey("raw", new Uint8Array(key), importAlgorithm, false, ["sign", "verify"]); | ||
| const cryptoKey = await subtle.importKey("raw", key, importAlgorithm, false, ["sign", "verify"]); | ||
| const sig = await subtle.sign("HMAC", cryptoKey, msg); | ||
| const result = Buffer.from(new Uint8Array(sig)); | ||
| const result = new Uint8Array(sig); | ||
| return result; | ||
| } | ||
| const hmac = browserCrypto.createHmac("sha256", Buffer.from(key)); | ||
| const hmac = browserCrypto.createHmac("sha256", key); | ||
| hmac.update(msg); | ||
@@ -117,4 +102,15 @@ const result = hmac.digest(); | ||
| const expectedSig = await hmacSha256Sign(key, msg); | ||
| return equalConstTime(expectedSig, sig); | ||
| return equalBytes(expectedSig, sig); | ||
| } | ||
| function assertValidPrivateKey(privateKey) { | ||
| assert(isValidPrivateKey(privateKey), "Bad private key"); | ||
| } | ||
| function assertValidPublicKey(publicKey) { | ||
| const isValid = secp256k1.utils.isValidPublicKey(publicKey, true) || secp256k1.utils.isValidPublicKey(publicKey, false); | ||
| assert(isValid, "Bad public key"); | ||
| } | ||
| function assertValidMessage(msg) { | ||
| assert(msg.length > 0, "Message should not be empty"); | ||
| assert(msg.length <= 32, "Message is too long"); | ||
| } | ||
@@ -133,7 +129,4 @@ /** | ||
| const getPublic = function (privateKey) { | ||
| // This function has sync API so we throw an error immediately. | ||
| assertPrivateKey(privateKey); | ||
| // XXX(Kagami): `elliptic.utils.encode` returns array for every | ||
| // encoding except `hex`. | ||
| return Buffer.from(ec.keyFromPrivate(privateKey).getPublic("array")); | ||
| assertValidPrivateKey(privateKey); | ||
| return secp256k1.getPublicKey(privateKey, false); | ||
| }; | ||
@@ -145,63 +138,46 @@ | ||
| const getPublicCompressed = function (privateKey) { | ||
| assertPrivateKey(privateKey); | ||
| // See https://github.com/wanderer/secp256k1-node/issues/46 | ||
| const compressed = true; | ||
| return Buffer.from(ec.keyFromPrivate(privateKey).getPublic(compressed, "array")); | ||
| assertValidPrivateKey(privateKey); | ||
| return secp256k1.getPublicKey(privateKey); | ||
| }; | ||
| // NOTE(Kagami): We don't use promise shim in Browser implementation | ||
| // because it's supported natively in new browsers (see | ||
| // <http://caniuse.com/#feat=promises>) and we can use only new browsers | ||
| // because of the WebCryptoAPI (see | ||
| // <http://caniuse.com/#feat=cryptography>). | ||
| const sign = async function (privateKey, msg) { | ||
| assertPrivateKey(privateKey); | ||
| assert(msg.length > 0, "Message should not be empty"); | ||
| assert(msg.length <= 32, "Message is too long"); | ||
| return Buffer.from(ec.sign(msg, privateKey, { | ||
| canonical: true | ||
| }).toDER()); | ||
| assertValidPrivateKey(privateKey); | ||
| assertValidMessage(msg); | ||
| const sig = secp256k1.sign(msg, privateKey, { | ||
| prehash: false, | ||
| format: "der" | ||
| }); | ||
| return sig; | ||
| }; | ||
| const assertPublicKey = function (publicKey) { | ||
| assert(publicKey.length === 65 || publicKey.length === 33, "Bad public key: expected 65 or 33 bytes, got " + publicKey.length); | ||
| if (publicKey.length === 65) { | ||
| assert(publicKey[0] === 4, "Bad public key: expected first byte 4, got " + publicKey[0]); | ||
| } | ||
| if (publicKey.length === 33) { | ||
| assert(publicKey[0] === 2 || publicKey[0] === 3, "Bad public key: expected first byte 2 or 3, got " + publicKey[0]); | ||
| } | ||
| }; | ||
| const assertPrivateKey = function (privateKey) { | ||
| assert(Buffer.isBuffer(privateKey), "Bad private key: expected Buffer"); | ||
| assert(privateKey.length === 32, "Bad private key: expected 32 bytes, got " + privateKey.length); | ||
| assert(isValidPrivateKey(privateKey), "Bad private key: out of range"); | ||
| }; | ||
| const verify = async function (publicKey, msg, sig) { | ||
| assertPublicKey(publicKey); | ||
| assert(msg.length > 0, "Message should not be empty"); | ||
| assert(msg.length <= 32, "Message is too long"); | ||
| if (ec.verify(msg, sig, publicKey)) { | ||
| return null; | ||
| } | ||
| assertValidPublicKey(publicKey); | ||
| assertValidMessage(msg); | ||
| if (secp256k1.verify(sig, msg, publicKey, { | ||
| prehash: false, | ||
| format: "der" | ||
| })) return null; | ||
| throw new Error("Bad signature"); | ||
| }; | ||
| const derive = async function (privateKeyA, publicKeyB, padding) { | ||
| assertPrivateKey(privateKeyA); | ||
| assertPublicKey(publicKeyB); | ||
| const keyA = ec.keyFromPrivate(privateKeyA); | ||
| const keyB = ec.keyFromPublic(publicKeyB); | ||
| const Px = keyA.derive(keyB.getPublic()); // BN instance | ||
| if (padding) { | ||
| return Buffer.from(Px.toString(16, 64), "hex"); | ||
| } | ||
| return Buffer.from(Px.toArray()); | ||
| const derive = async function (privateKeyA, publicKeyB) { | ||
| assertValidPrivateKey(privateKeyA); | ||
| assertValidPublicKey(publicKeyB); | ||
| // Strip leading zeros for backwards compatibility with older versions | ||
| // that used elliptic's BN.toArray() which didn't include leading zeros. | ||
| // Use derivePadded() if you need a fixed 32-byte output. | ||
| const sharedSecret = secp256k1.getSharedSecret(privateKeyA, publicKeyB); | ||
| const Px = sharedSecret.subarray(1); | ||
| const i = Px.findIndex(byte => byte !== 0); | ||
| return Px.subarray(i); | ||
| }; | ||
| const deriveUnpadded = derive; | ||
| const derivePadded = async function (privateKeyA, publicKeyB) { | ||
| return derive(privateKeyA, publicKeyB, true); | ||
| assertValidPrivateKey(privateKeyA); | ||
| assertValidPublicKey(publicKeyB); | ||
| const sharedSecret = secp256k1.getSharedSecret(privateKeyA, publicKeyB); | ||
| return sharedSecret.subarray(1); | ||
| }; | ||
| const deriveUnpadded = async function (privateKeyA, publicKeyB) { | ||
| return derive(privateKeyA, publicKeyB, false); | ||
| }; | ||
| const encrypt = async function (publicKeyTo, msg, opts) { | ||
| var _opts$padding; | ||
| opts = opts || {}; | ||
| const padding = (_opts$padding = opts.padding) !== null && _opts$padding !== void 0 ? _opts$padding : true; | ||
| let ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32); | ||
@@ -213,3 +189,4 @@ // There is a very unlikely possibility that it is not a valid key | ||
| const ephemPublicKey = getPublic(ephemPrivateKey); | ||
| const Px = await derive(ephemPrivateKey, publicKeyTo, opts.padding); | ||
| const deriveLocal = padding ? derivePadded : deriveUnpadded; | ||
| const Px = await deriveLocal(ephemPrivateKey, publicKeyTo); | ||
| const hash = await sha512(Px); | ||
@@ -219,6 +196,5 @@ const iv = opts.iv || randomBytes(16); | ||
| const macKey = hash.slice(32); | ||
| const data = await aesCbcEncrypt(iv, Buffer.from(encryptionKey), msg); | ||
| const ciphertext = data; | ||
| const dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]); | ||
| const mac = await hmacSha256Sign(Buffer.from(macKey), dataToMac); | ||
| const ciphertext = await aesCbcEncrypt(iv, encryptionKey, msg); | ||
| const dataToMac = concatBytes(iv, ephemPublicKey, ciphertext); | ||
| const mac = await hmacSha256Sign(macKey, dataToMac); | ||
| return { | ||
@@ -231,10 +207,12 @@ iv, | ||
| }; | ||
| const decrypt = async function (privateKey, opts, padding) { | ||
| const Px = await derive(privateKey, opts.ephemPublicKey, padding); | ||
| const decrypt = async function (privateKey, opts, _padding) { | ||
| const padding = _padding !== null && _padding !== void 0 ? _padding : false; | ||
| const deriveLocal = padding ? derivePadded : deriveUnpadded; | ||
| const Px = await deriveLocal(privateKey, opts.ephemPublicKey); | ||
| const hash = await sha512(Px); | ||
| const encryptionKey = hash.slice(0, 32); | ||
| const macKey = hash.slice(32); | ||
| const dataToMac = Buffer.concat([opts.iv, opts.ephemPublicKey, opts.ciphertext]); | ||
| const macGood = await hmacSha256Verify(Buffer.from(macKey), dataToMac, opts.mac); | ||
| if (!macGood && !padding) { | ||
| const dataToMac = concatBytes(opts.iv, opts.ephemPublicKey, opts.ciphertext); | ||
| const macGood = await hmacSha256Verify(macKey, dataToMac, opts.mac); | ||
| if (!macGood && padding === false) { | ||
| return decrypt(privateKey, opts, true); | ||
@@ -244,6 +222,6 @@ } else if (!macGood && padding === true) { | ||
| } | ||
| const msg = await aesCbcDecrypt(opts.iv, Buffer.from(encryptionKey), opts.ciphertext); | ||
| return Buffer.from(new Uint8Array(msg)); | ||
| const msg = await aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext); | ||
| return msg; | ||
| }; | ||
| export { decrypt, derive, derivePadded, deriveUnpadded, encrypt, generatePrivate, getPublic, getPublicCompressed, sign, verify }; |
+19
-18
| { | ||
| "name": "@toruslabs/eccrypto", | ||
| "version": "6.2.0", | ||
| "version": "7.0.0", | ||
| "description": "JavaScript Elliptic curve cryptography library, includes fix to browser.js so that encrypt/decrypt works", | ||
@@ -11,3 +11,3 @@ "main": "./dist/lib.cjs/index.js", | ||
| "build": "torus-scripts build", | ||
| "lint": "eslint --fix 'src/**/*.ts'", | ||
| "lint": "torus-scripts lint --fix", | ||
| "release": "torus-scripts release", | ||
@@ -44,15 +44,16 @@ "test:ci": "npm run test:node && npm run test:browsers", | ||
| "devDependencies": { | ||
| "@babel/runtime": "^7.26.9", | ||
| "@toruslabs/config": "^3.1.0", | ||
| "@toruslabs/eslint-config-node": "^4.1.0", | ||
| "@toruslabs/eslint-config-typescript": "^4.1.0", | ||
| "@toruslabs/torus-scripts": "^7.1.1", | ||
| "@types/elliptic": "^6.4.18", | ||
| "@vitest/browser": "^3.0.7", | ||
| "@vitest/coverage-istanbul": "^3.0.7", | ||
| "browserify": "^17.0.1", | ||
| "eslint": "^9.21.0", | ||
| "playwright": "^1.50.1", | ||
| "typescript": "^5.7.3", | ||
| "vitest": "^3.0.7" | ||
| "@babel/runtime": "^7.28.6", | ||
| "@toruslabs/config": "^4.0.0", | ||
| "@toruslabs/eslint-config-node": "^5.0.0", | ||
| "@toruslabs/eslint-config-typescript": "^5.0.0", | ||
| "@toruslabs/torus-scripts": "^8.0.0", | ||
| "@types/node": "^25.1.0", | ||
| "@vitest/browser-playwright": "^4.0.17", | ||
| "@vitest/coverage-istanbul": "^4.0.17", | ||
| "buffer": "^6.0.3", | ||
| "eccrypto-old": "npm:@toruslabs/eccrypto@6.2.0", | ||
| "eslint": "^9.39.2", | ||
| "playwright": "^1.57.0", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.0.17" | ||
| }, | ||
@@ -63,8 +64,8 @@ "overrides": { | ||
| "engines": { | ||
| "node": ">=20.x", | ||
| "npm": ">=9.x" | ||
| "node": ">=22.x", | ||
| "npm": ">=10.x" | ||
| }, | ||
| "dependencies": { | ||
| "elliptic": "^6.6.1" | ||
| "@noble/curves": "^2.0.1" | ||
| } | ||
| } |
+92
-63
@@ -13,7 +13,13 @@ # eccrypto | ||
| There is currently no any isomorphic ECC library which provides ECDSA, ECDH and ECIES for both Node.js and Browser and uses the fastest implementation available (e.g. [secp256k1-node](https://github.com/wanderer/secp256k1-node) is much faster than other libraries but can be used only on Node.js). So `eccrypto` is an attempt to create one. | ||
| - ECDSA (sign/verify) | ||
| - ECDH (key agreement) | ||
| - ECIES (encrypt/decrypt) | ||
| - secp256k1 curve support | ||
| - Compressed and uncompressed public key support | ||
| - Works in both Node.js and browsers | ||
| - Uses `Uint8Array` for all binary data | ||
| ## Implementation details | ||
| With the help of browserify `eccrypto` provides different implementations for Browser and Node.js with the same API. Because WebCryptoAPI defines asynchronous promise-driven API, implementation for Node needs to use promises too. | ||
| This library uses [`@noble/curves`](https://github.com/paulmillr/noble-curves) for elliptic curve operations, which provides: | ||
@@ -30,3 +36,5 @@ - Use Node.js crypto module/library bindings where possible | ||
| ECDH only works in Node 0.11+ (see https://github.com/joyent/node/pull/5854), ECDSA only supports keys in PEM format (see https://github.com/joyent/node/issues/6904) and ECIES is not supported at all. | ||
| ```bash | ||
| npm install @toruslabs/eccrypto | ||
| ``` | ||
@@ -37,82 +45,103 @@ #### WebCryptoAPI | ||
| So we use [seck256k1](https://www.npmjs.com/package/secp256k1) library in Node for ECDSA, [elliptic](https://www.npmjs.com/package/elliptic) in Browser for ECDSA and ECDH and implement ECIES manually with the help of native crypto API. | ||
| ```ts | ||
| import * as eccrypto from "@toruslabs/eccrypto"; | ||
| ## Possible future goals | ||
| // Generate a new random 32-byte private key | ||
| const privateKey = eccrypto.generatePrivate(); | ||
| - Support other curves/KDF/MAC/symmetric encryption schemes | ||
| // Get the corresponding public key (65 bytes uncompressed) | ||
| const publicKey = eccrypto.getPublic(privateKey); | ||
| ## Usage | ||
| // Or get compressed public key (33 bytes) | ||
| const compressedPublicKey = eccrypto.getPublicCompressed(privateKey); | ||
| ### ECDSA | ||
| // Message must be 32 bytes or less (typically a hash) | ||
| const msgHash = new Uint8Array(32); // Your message hash here | ||
| ```js | ||
| var crypto = require("crypto"); | ||
| var eccrypto = require("eccrypto"); | ||
| // Sign the message | ||
| const signature = await eccrypto.sign(privateKey, msgHash); | ||
| console.log("Signature (DER format):", signature); | ||
| // A new random 32-byte private key. | ||
| var privateKey = eccrypto.generatePrivate(); | ||
| // Corresponding uncompressed (65-byte) public key. | ||
| var publicKey = eccrypto.getPublic(privateKey); | ||
| // Verify the signature | ||
| try { | ||
| await eccrypto.verify(publicKey, msgHash, signature); | ||
| console.log("Signature is valid"); | ||
| } catch (e) { | ||
| console.log("Signature is invalid"); | ||
| } | ||
| ``` | ||
| var str = "message to sign"; | ||
| // Always hash you message to sign! | ||
| var msg = crypto.createHash("sha256").update(str).digest(); | ||
| ### ECDH (Key Agreement) | ||
| eccrypto.sign(privateKey, msg).then(function (sig) { | ||
| console.log("Signature in DER format:", sig); | ||
| eccrypto | ||
| .verify(publicKey, msg, sig) | ||
| .then(function () { | ||
| console.log("Signature is OK"); | ||
| }) | ||
| .catch(function () { | ||
| console.log("Signature is BAD"); | ||
| }); | ||
| }); | ||
| ``` | ||
| ```ts | ||
| import * as eccrypto from "@toruslabs/eccrypto"; | ||
| ### ECDH | ||
| const privateKeyA = eccrypto.generatePrivate(); | ||
| const publicKeyA = eccrypto.getPublic(privateKeyA); | ||
| ```js | ||
| var eccrypto = require("eccrypto"); | ||
| const privateKeyB = eccrypto.generatePrivate(); | ||
| const publicKeyB = eccrypto.getPublic(privateKeyB); | ||
| var privateKeyA = eccrypto.generatePrivate(); | ||
| var publicKeyA = eccrypto.getPublic(privateKeyA); | ||
| var privateKeyB = eccrypto.generatePrivate(); | ||
| var publicKeyB = eccrypto.getPublic(privateKeyB); | ||
| // Both parties derive the same shared secret | ||
| const sharedSecretA = await eccrypto.derive(privateKeyA, publicKeyB); | ||
| const sharedSecretB = await eccrypto.derive(privateKeyB, publicKeyA); | ||
| eccrypto.derive(privateKeyA, publicKeyB).then(function (sharedKey1) { | ||
| eccrypto.derive(privateKeyB, publicKeyA).then(function (sharedKey2) { | ||
| console.log("Both shared keys are equal:", sharedKey1, sharedKey2); | ||
| }); | ||
| }); | ||
| // sharedSecretA and sharedSecretB are equal | ||
| console.log("Shared secrets match:", sharedSecretA.toString() === sharedSecretB.toString()); | ||
| ``` | ||
| ### ECIES | ||
| ### ECIES (Encrypt/Decrypt) | ||
| ```js | ||
| var eccrypto = require("eccrypto"); | ||
| ```ts | ||
| import * as eccrypto from "@toruslabs/eccrypto"; | ||
| var privateKeyA = eccrypto.generatePrivate(); | ||
| var publicKeyA = eccrypto.getPublic(privateKeyA); | ||
| var privateKeyB = eccrypto.generatePrivate(); | ||
| var publicKeyB = eccrypto.getPublic(privateKeyB); | ||
| const privateKeyA = eccrypto.generatePrivate(); | ||
| const publicKeyA = eccrypto.getPublic(privateKeyA); | ||
| // Encrypting the message for B. | ||
| eccrypto.encrypt(publicKeyB, Buffer.from("msg to b")).then(function (encrypted) { | ||
| // B decrypting the message. | ||
| eccrypto.decrypt(privateKeyB, encrypted).then(function (plaintext) { | ||
| console.log("Message to part B:", plaintext.toString()); | ||
| }); | ||
| }); | ||
| const privateKeyB = eccrypto.generatePrivate(); | ||
| const publicKeyB = eccrypto.getPublic(privateKeyB); | ||
| // Encrypting the message for A. | ||
| eccrypto.encrypt(publicKeyA, Buffer.from("msg to a")).then(function (encrypted) { | ||
| // A decrypting the message. | ||
| eccrypto.decrypt(privateKeyA, encrypted).then(function (plaintext) { | ||
| console.log("Message to part A:", plaintext.toString()); | ||
| }); | ||
| }); | ||
| // Encrypt a message for B | ||
| const message = new TextEncoder().encode("Hello, World!"); | ||
| const encrypted = await eccrypto.encrypt(publicKeyB, message); | ||
| // B decrypts the message | ||
| const decrypted = await eccrypto.decrypt(privateKeyB, encrypted); | ||
| console.log("Decrypted:", new TextDecoder().decode(decrypted)); | ||
| ``` | ||
| ## API | ||
| ### `generatePrivate(): Uint8Array` | ||
| Generate a new random 32-byte private key. | ||
| ### `getPublic(privateKey: Uint8Array): Uint8Array` | ||
| Get the 65-byte uncompressed public key from a private key. | ||
| ### `getPublicCompressed(privateKey: Uint8Array): Uint8Array` | ||
| Get the 33-byte compressed public key from a private key. | ||
| ### `sign(privateKey: Uint8Array, msg: Uint8Array): Promise<Uint8Array>` | ||
| Sign a message (max 32 bytes) with a private key. Returns DER-encoded signature. | ||
| ### `verify(publicKey: Uint8Array, msg: Uint8Array, sig: Uint8Array): Promise<null>` | ||
| Verify a signature. Throws an error if the signature is invalid. | ||
| ### `derive(privateKey: Uint8Array, publicKey: Uint8Array): Promise<Uint8Array>` | ||
| Derive a shared secret using ECDH. | ||
| ### `encrypt(publicKey: Uint8Array, msg: Uint8Array, opts?): Promise<Ecies>` | ||
| Encrypt a message using ECIES. Returns an object with `iv`, `ephemPublicKey`, `ciphertext`, and `mac`. | ||
| ### `decrypt(privateKey: Uint8Array, opts: Ecies): Promise<Uint8Array>` | ||
| Decrypt an ECIES encrypted message. | ||
| ## License | ||
@@ -119,0 +148,0 @@ |
Sorry, the diff of this file is too big to display
| /*! | ||
| * The buffer module from node.js, for the browser. | ||
| * | ||
| * @author Feross Aboukhadijeh <https://feross.org> | ||
| * @license MIT | ||
| */ | ||
| /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */ |
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Mixed license
LicensePackage contains multiple licenses.
0
-100%1
-50%153
23.39%32230
-83.38%14
7.69%6
-25%453
-8.67%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed