UNPKG

@nlindley/eccrypto

Version:

JavaScript Elliptic curve cryptography library

790 lines (645 loc) 21.3 kB
const EC = require("elliptic").ec; const ec = new EC("secp256k1"); const ecparams = ec.curve; // Hack, we can not use bn.js@5, while elliptic uses bn.js@4 // See https://github.com/indutny/elliptic/issues/191#issuecomment-569888758 const BN = ecparams.n.constructor; function loadCompressedPublicKey(first, xbuf) { let x = new BN(xbuf); // overflow if (x.cmp(ecparams.p) >= 0) return null; x = x.toRed(ecparams.red); // compute corresponding Y let y = x.redSqr().redIMul(x).redIAdd(ecparams.b).redSqrt(); if ((first === 0x03) !== y.isOdd()) y = y.redNeg(); return ec.keyPair({ pub: { x: x, y: y } }); } function loadUncompressedPublicKey(first, xbuf, ybuf) { let x = new BN(xbuf); let y = new BN(ybuf); // overflow if (x.cmp(ecparams.p) >= 0 || y.cmp(ecparams.p) >= 0) return null; x = x.toRed(ecparams.red); y = y.toRed(ecparams.red); // is odd flag if ((first === 0x06 || first === 0x07) && y.isOdd() !== (first === 0x07)) return null; // x*x*x + b = y*y const x3 = x.redSqr().redIMul(x); if (!y.redSqr().redISub(x3.redIAdd(ecparams.b)).isZero()) return null; return ec.keyPair({ pub: { x: x, y: y } }); } function loadPublicKey(pubkey) { // length should be validated in interface const first = pubkey[0]; switch (first) { case 0x02: case 0x03: if (pubkey.length !== 33) return null; return loadCompressedPublicKey(first, pubkey.subarray(1, 33)); case 0x04: case 0x06: case 0x07: if (pubkey.length !== 65) return null; return loadUncompressedPublicKey( first, pubkey.subarray(1, 33), pubkey.subarray(33, 65) ); default: return null; } } function savePublicKey(output, point) { const pubkey = point.encode(null, output.length === 33); // Loop should be faster because we do not need create extra Uint8Array // output.set(new Uint8Array(pubkey)) for (let i = 0; i < output.length; ++i) output[i] = pubkey[i]; } const secp256k1 = { contextRandomize() { return 0; }, privateKeyVerify(seckey) { const bn = new BN(seckey); return bn.cmp(ecparams.n) < 0 && !bn.isZero() ? 0 : 1; }, privateKeyNegate(seckey) { const bn = new BN(seckey); const negate = ecparams.n .sub(bn) .umod(ecparams.n) .toArrayLike(Uint8Array, "be", 32); seckey.set(negate); return 0; }, privateKeyTweakAdd(seckey, tweak) { const bn = new BN(tweak); if (bn.cmp(ecparams.n) >= 0) return 1; bn.iadd(new BN(seckey)); if (bn.cmp(ecparams.n) >= 0) bn.isub(ecparams.n); if (bn.isZero()) return 1; const tweaked = bn.toArrayLike(Uint8Array, "be", 32); seckey.set(tweaked); return 0; }, privateKeyTweakMul(seckey, tweak) { let bn = new BN(tweak); if (bn.cmp(ecparams.n) >= 0 || bn.isZero()) return 1; bn.imul(new BN(seckey)); if (bn.cmp(ecparams.n) >= 0) bn = bn.umod(ecparams.n); const tweaked = bn.toArrayLike(Uint8Array, "be", 32); seckey.set(tweaked); return 0; }, publicKeyVerify(pubkey) { const pair = loadPublicKey(pubkey); return pair === null ? 1 : 0; }, publicKeyCreate(output, seckey) { const bn = new BN(seckey); if (bn.cmp(ecparams.n) >= 0 || bn.isZero()) return 1; const point = ec.keyFromPrivate(seckey).getPublic(); savePublicKey(output, point); return 0; }, publicKeyConvert(output, pubkey) { const pair = loadPublicKey(pubkey); if (pair === null) return 1; const point = pair.getPublic(); savePublicKey(output, point); return 0; }, publicKeyNegate(output, pubkey) { const pair = loadPublicKey(pubkey); if (pair === null) return 1; const point = pair.getPublic(); point.y = point.y.redNeg(); savePublicKey(output, point); return 0; }, publicKeyCombine(output, pubkeys) { const pairs = new Array(pubkeys.length); for (let i = 0; i < pubkeys.length; ++i) { pairs[i] = loadPublicKey(pubkeys[i]); if (pairs[i] === null) return 1; } let point = pairs[0].getPublic(); for (let i = 1; i < pairs.length; ++i) point = point.add(pairs[i].pub); if (point.isInfinity()) return 2; savePublicKey(output, point); return 0; }, publicKeyTweakAdd(output, pubkey, tweak) { const pair = loadPublicKey(pubkey); if (pair === null) return 1; tweak = new BN(tweak); if (tweak.cmp(ecparams.n) >= 0) return 2; const point = pair.getPublic().add(ecparams.g.mul(tweak)); if (point.isInfinity()) return 2; savePublicKey(output, point); return 0; }, publicKeyTweakMul(output, pubkey, tweak) { const pair = loadPublicKey(pubkey); if (pair === null) return 1; tweak = new BN(tweak); if (tweak.cmp(ecparams.n) >= 0 || tweak.isZero()) return 2; const point = pair.getPublic().mul(tweak); savePublicKey(output, point); return 0; }, signatureNormalize(sig) { const r = new BN(sig.subarray(0, 32)); const s = new BN(sig.subarray(32, 64)); if (r.cmp(ecparams.n) >= 0 || s.cmp(ecparams.n) >= 0) return 1; if (s.cmp(ec.nh) === 1) { sig.set(ecparams.n.sub(s).toArrayLike(Uint8Array, "be", 32), 32); } return 0; }, // Copied 1-to-1 from https://github.com/bitcoinjs/bip66/blob/master/index.js // Adapted for Uint8Array instead Buffer signatureExport(obj, sig) { const sigR = sig.subarray(0, 32); const sigS = sig.subarray(32, 64); if (new BN(sigR).cmp(ecparams.n) >= 0) return 1; if (new BN(sigS).cmp(ecparams.n) >= 0) return 1; const { output } = obj; // Prepare R let r = output.subarray(4, 4 + 33); r[0] = 0x00; r.set(sigR, 1); let lenR = 33; let posR = 0; for ( ; lenR > 1 && r[posR] === 0x00 && !(r[posR + 1] & 0x80); --lenR, ++posR ); r = r.subarray(posR); if (r[0] & 0x80) return 1; if (lenR > 1 && r[0] === 0x00 && !(r[1] & 0x80)) return 1; // Prepare S let s = output.subarray(6 + 33, 6 + 33 + 33); s[0] = 0x00; s.set(sigS, 1); let lenS = 33; let posS = 0; for ( ; lenS > 1 && s[posS] === 0x00 && !(s[posS + 1] & 0x80); --lenS, ++posS ); s = s.subarray(posS); if (s[0] & 0x80) return 1; if (lenS > 1 && s[0] === 0x00 && !(s[1] & 0x80)) return 1; // Set output length for return obj.outputlen = 6 + lenR + lenS; // Output in specified format // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] output[0] = 0x30; output[1] = obj.outputlen - 2; output[2] = 0x02; output[3] = r.length; output.set(r, 4); output[4 + lenR] = 0x02; output[5 + lenR] = s.length; output.set(s, 6 + lenR); return 0; }, // Copied 1-to-1 from https://github.com/bitcoinjs/bip66/blob/master/index.js // Adapted for Uint8Array instead Buffer signatureImport(output, sig) { if (sig.length < 8) return 1; if (sig.length > 72) return 1; if (sig[0] !== 0x30) return 1; if (sig[1] !== sig.length - 2) return 1; if (sig[2] !== 0x02) return 1; const lenR = sig[3]; if (lenR === 0) return 1; if (5 + lenR >= sig.length) return 1; if (sig[4 + lenR] !== 0x02) return 1; const lenS = sig[5 + lenR]; if (lenS === 0) return 1; if (6 + lenR + lenS !== sig.length) return 1; if (sig[4] & 0x80) return 1; if (lenR > 1 && sig[4] === 0x00 && !(sig[5] & 0x80)) return 1; if (sig[lenR + 6] & 0x80) return 1; if (lenS > 1 && sig[lenR + 6] === 0x00 && !(sig[lenR + 7] & 0x80)) return 1; let sigR = sig.subarray(4, 4 + lenR); if (sigR.length === 33 && sigR[0] === 0x00) sigR = sigR.subarray(1); if (sigR.length > 32) return 1; let sigS = sig.subarray(6 + lenR); if (sigS.length === 33 && sigS[0] === 0x00) sigS = sigS.slice(1); if (sigS.length > 32) throw new Error("S length is too long"); let r = new BN(sigR); if (r.cmp(ecparams.n) >= 0) r = new BN(0); let s = new BN(sig.subarray(6 + lenR)); if (s.cmp(ecparams.n) >= 0) s = new BN(0); output.set(r.toArrayLike(Uint8Array, "be", 32), 0); output.set(s.toArrayLike(Uint8Array, "be", 32), 32); return 0; }, ecdsaSign(obj, message, seckey, data, noncefn) { if (noncefn) { const _noncefn = noncefn; noncefn = (counter) => { const nonce = _noncefn(message, seckey, null, data, counter); const isValid = nonce instanceof Uint8Array && nonce.length === 32; if (!isValid) throw new Error("This is the way"); return new BN(nonce); }; } const d = new BN(seckey); if (d.cmp(ecparams.n) >= 0 || d.isZero()) return 1; let sig; try { sig = ec.sign(message, seckey, { canonical: true, k: noncefn, pers: data, }); } catch (err) { return 1; } obj.signature.set(sig.r.toArrayLike(Uint8Array, "be", 32), 0); obj.signature.set(sig.s.toArrayLike(Uint8Array, "be", 32), 32); obj.recid = sig.recoveryParam; return 0; }, ecdsaVerify(sig, msg32, pubkey) { const sigObj = { r: sig.subarray(0, 32), s: sig.subarray(32, 64) }; const sigr = new BN(sigObj.r); const sigs = new BN(sigObj.s); if (sigr.cmp(ecparams.n) >= 0 || sigs.cmp(ecparams.n) >= 0) return 1; if (sigs.cmp(ec.nh) === 1 || sigr.isZero() || sigs.isZero()) return 3; const pair = loadPublicKey(pubkey); if (pair === null) return 2; const point = pair.getPublic(); const isValid = ec.verify(msg32, sigObj, point); return isValid ? 0 : 3; }, ecdsaRecover(output, sig, recid, msg32) { const sigObj = { r: sig.slice(0, 32), s: sig.slice(32, 64) }; const sigr = new BN(sigObj.r); const sigs = new BN(sigObj.s); if (sigr.cmp(ecparams.n) >= 0 || sigs.cmp(ecparams.n) >= 0) return 1; if (sigr.isZero() || sigs.isZero()) return 2; // Can throw `throw new Error('Unable to find sencond key candinate');` let point; try { point = ec.recoverPubKey(msg32, sigObj, recid); } catch (err) { return 2; } savePublicKey(output, point); return 0; }, ecdh(output, pubkey, seckey, data, hashfn, xbuf, ybuf) { const pair = loadPublicKey(pubkey); if (pair === null) return 1; const scalar = new BN(seckey); if (scalar.cmp(ecparams.n) >= 0 || scalar.isZero()) return 2; const point = pair.getPublic().mul(scalar); if (hashfn === undefined) { const data = point.encode(null, true); const sha256 = ec.hash().update(data).digest(); for (let i = 0; i < 32; ++i) output[i] = sha256[i]; } else { if (!xbuf) xbuf = new Uint8Array(32); const x = point.getX().toArray("be", 32); for (let i = 0; i < 32; ++i) xbuf[i] = x[i]; if (!ybuf) ybuf = new Uint8Array(32); const y = point.getY().toArray("be", 32); for (let i = 0; i < 32; ++i) ybuf[i] = y[i]; const hash = hashfn(xbuf, ybuf, data); const isValid = hash instanceof Uint8Array && hash.length === output.length; if (!isValid) return 2; output.set(hash); } return 0; }, }; const errors = { IMPOSSIBLE_CASE: "Impossible case. Please create issue.", TWEAK_ADD: "The tweak was out of range or the resulted private key is invalid", TWEAK_MUL: "The tweak was out of range or equal to zero", CONTEXT_RANDOMIZE_UNKNOW: "Unknow error on context randomization", SECKEY_INVALID: "Private Key is invalid", PUBKEY_PARSE: "Public Key could not be parsed", PUBKEY_SERIALIZE: "Public Key serialization error", PUBKEY_COMBINE: "The sum of the public keys is not valid", SIG_PARSE: "Signature could not be parsed", SIGN: "The nonce generation function failed, or the private key was invalid", RECOVER: "Public key could not be recover", ECDH: "Scalar was invalid (zero or overflow)", }; function assert(cond, msg) { if (!cond) throw new Error(msg); } function isUint8Array(name, value, length) { assert(value instanceof Uint8Array, `Expected ${name} to be an Uint8Array`); if (length !== undefined) { if (Array.isArray(length)) { const numbers = length.join(", "); const msg = `Expected ${name} to be an Uint8Array with length [${numbers}]`; assert(length.includes(value.length), msg); } else { const msg = `Expected ${name} to be an Uint8Array with length ${length}`; assert(value.length === length, msg); } } } function isCompressed(value) { assert( toTypeString(value) === "Boolean", "Expected compressed to be a Boolean" ); } function getAssertedOutput(output = (len) => new Uint8Array(len), length) { if (typeof output === "function") output = output(length); isUint8Array("output", output, length); return output; } function toTypeString(value) { return Object.prototype.toString.call(value).slice(8, -1); } module.exports = { contextRandomize(seed) { assert( seed === null || seed instanceof Uint8Array, "Expected seed to be an Uint8Array or null" ); if (seed !== null) isUint8Array("seed", seed, 32); switch (secp256k1.contextRandomize(seed)) { case 1: throw new Error(errors.CONTEXT_RANDOMIZE_UNKNOW); } }, privateKeyVerify(seckey) { isUint8Array("private key", seckey, 32); return secp256k1.privateKeyVerify(seckey) === 0; }, privateKeyNegate(seckey) { isUint8Array("private key", seckey, 32); switch (secp256k1.privateKeyNegate(seckey)) { case 0: return seckey; case 1: throw new Error(errors.IMPOSSIBLE_CASE); } }, privateKeyTweakAdd(seckey, tweak) { isUint8Array("private key", seckey, 32); isUint8Array("tweak", tweak, 32); switch (secp256k1.privateKeyTweakAdd(seckey, tweak)) { case 0: return seckey; case 1: throw new Error(errors.TWEAK_ADD); } }, privateKeyTweakMul(seckey, tweak) { isUint8Array("private key", seckey, 32); isUint8Array("tweak", tweak, 32); switch (secp256k1.privateKeyTweakMul(seckey, tweak)) { case 0: return seckey; case 1: throw new Error(errors.TWEAK_MUL); } }, publicKeyVerify(pubkey) { isUint8Array("public key", pubkey, [33, 65]); return secp256k1.publicKeyVerify(pubkey) === 0; }, publicKeyCreate(seckey, compressed = true, output) { isUint8Array("private key", seckey, 32); isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.publicKeyCreate(output, seckey)) { case 0: return output; case 1: throw new Error(errors.SECKEY_INVALID); case 2: throw new Error(errors.PUBKEY_SERIALIZE); } }, publicKeyConvert(pubkey, compressed = true, output) { isUint8Array("public key", pubkey, [33, 65]); isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.publicKeyConvert(output, pubkey)) { case 0: return output; case 1: throw new Error(errors.PUBKEY_PARSE); case 2: throw new Error(errors.PUBKEY_SERIALIZE); } }, publicKeyNegate(pubkey, compressed = true, output) { isUint8Array("public key", pubkey, [33, 65]); isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.publicKeyNegate(output, pubkey)) { case 0: return output; case 1: throw new Error(errors.PUBKEY_PARSE); case 2: throw new Error(errors.IMPOSSIBLE_CASE); case 3: throw new Error(errors.PUBKEY_SERIALIZE); } }, publicKeyCombine(pubkeys, compressed = true, output) { assert(Array.isArray(pubkeys), "Expected public keys to be an Array"); assert( pubkeys.length > 0, "Expected public keys array will have more than zero items" ); for (const pubkey of pubkeys) { isUint8Array("public key", pubkey, [33, 65]); } isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.publicKeyCombine(output, pubkeys)) { case 0: return output; case 1: throw new Error(errors.PUBKEY_PARSE); case 2: throw new Error(errors.PUBKEY_COMBINE); case 3: throw new Error(errors.PUBKEY_SERIALIZE); } }, publicKeyTweakAdd(pubkey, tweak, compressed = true, output) { isUint8Array("public key", pubkey, [33, 65]); isUint8Array("tweak", tweak, 32); isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.publicKeyTweakAdd(output, pubkey, tweak)) { case 0: return output; case 1: throw new Error(errors.PUBKEY_PARSE); case 2: throw new Error(errors.TWEAK_ADD); } }, publicKeyTweakMul(pubkey, tweak, compressed = true, output) { isUint8Array("public key", pubkey, [33, 65]); isUint8Array("tweak", tweak, 32); isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.publicKeyTweakMul(output, pubkey, tweak)) { case 0: return output; case 1: throw new Error(errors.PUBKEY_PARSE); case 2: throw new Error(errors.TWEAK_MUL); } }, signatureNormalize(sig) { isUint8Array("signature", sig, 64); switch (secp256k1.signatureNormalize(sig)) { case 0: return sig; case 1: throw new Error(errors.SIG_PARSE); } }, signatureExport(sig, output) { isUint8Array("signature", sig, 64); output = getAssertedOutput(output, 72); const obj = { output, outputlen: 72 }; switch (secp256k1.signatureExport(obj, sig)) { case 0: return output.slice(0, obj.outputlen); case 1: throw new Error(errors.SIG_PARSE); case 2: throw new Error(errors.IMPOSSIBLE_CASE); } }, signatureImport(sig, output) { isUint8Array("signature", sig); output = getAssertedOutput(output, 64); switch (secp256k1.signatureImport(output, sig)) { case 0: return output; case 1: throw new Error(errors.SIG_PARSE); case 2: throw new Error(errors.IMPOSSIBLE_CASE); } }, ecdsaSign(msg32, seckey, options = {}, output) { isUint8Array("message", msg32, 32); isUint8Array("private key", seckey, 32); assert( toTypeString(options) === "Object", "Expected options to be an Object" ); if (options.data !== undefined) isUint8Array("options.data", options.data); if (options.noncefn !== undefined) assert( toTypeString(options.noncefn) === "Function", "Expected options.noncefn to be a Function" ); output = getAssertedOutput(output, 64); const obj = { signature: output, recid: null }; switch ( secp256k1.ecdsaSign(obj, msg32, seckey, options.data, options.noncefn) ) { case 0: return obj; case 1: throw new Error(errors.SIGN); case 2: throw new Error(errors.IMPOSSIBLE_CASE); } }, ecdsaVerify(sig, msg32, pubkey) { isUint8Array("signature", sig, 64); isUint8Array("message", msg32, 32); isUint8Array("public key", pubkey, [33, 65]); switch (secp256k1.ecdsaVerify(sig, msg32, pubkey)) { case 0: return true; case 3: return false; case 1: throw new Error(errors.SIG_PARSE); case 2: throw new Error(errors.PUBKEY_PARSE); } }, ecdsaRecover(sig, recid, msg32, compressed = true, output) { isUint8Array("signature", sig, 64); assert( toTypeString(recid) === "Number" && recid >= 0 && recid <= 3, "Expected recovery id to be a Number within interval [0, 3]" ); isUint8Array("message", msg32, 32); isCompressed(compressed); output = getAssertedOutput(output, compressed ? 33 : 65); switch (secp256k1.ecdsaRecover(output, sig, recid, msg32)) { case 0: return output; case 1: throw new Error(errors.SIG_PARSE); case 2: throw new Error(errors.RECOVER); case 3: throw new Error(errors.IMPOSSIBLE_CASE); } }, ecdh(pubkey, seckey, options = {}, output) { isUint8Array("public key", pubkey, [33, 65]); isUint8Array("private key", seckey, 32); assert( toTypeString(options) === "Object", "Expected options to be an Object" ); if (options.data !== undefined) isUint8Array("options.data", options.data); if (options.hashfn !== undefined) { assert( toTypeString(options.hashfn) === "Function", "Expected options.hashfn to be a Function" ); if (options.xbuf !== undefined) isUint8Array("options.xbuf", options.xbuf, 32); if (options.ybuf !== undefined) isUint8Array("options.ybuf", options.ybuf, 32); isUint8Array("output", output); } else { output = getAssertedOutput(output, 32); } switch ( secp256k1.ecdh( output, pubkey, seckey, options.data, options.hashfn, options.xbuf, options.ybuf ) ) { case 0: return output; case 1: throw new Error(errors.PUBKEY_PARSE); case 2: throw new Error(errors.ECDH); } }, };