@nlindley/eccrypto
Version:
JavaScript Elliptic curve cryptography library
790 lines (645 loc) • 21.3 kB
JavaScript
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);
}
},
};