diff --git a/Gruntfile.js b/Gruntfile.js index 462a485f..7c537060 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -36,12 +36,15 @@ module.exports = function(grunt) { "src/js/sjcl/core/bn.js", "src/js/sjcl/core/ecc.js", "src/js/sjcl/core/srp.js", + "src/js/sjcl-custom/sjcl-ecc-pointextras.js", "src/js/sjcl-custom/sjcl-secp256k1.js", "src/js/sjcl-custom/sjcl-ripemd160.js", "src/js/sjcl-custom/sjcl-extramath.js", "src/js/sjcl-custom/sjcl-montgomery.js", "src/js/sjcl-custom/sjcl-validecc.js", + "src/js/sjcl-custom/sjcl-ecdsa-canonical.js", "src/js/sjcl-custom/sjcl-ecdsa-der.js", + "src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js", "src/js/sjcl-custom/sjcl-jacobi.js" ], dest: 'build/sjcl.js' diff --git a/README.md b/README.md index 9eb999f4..d1758976 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ #The Ripple JavaScript Library +[![NPM](https://nodei.co/npm/ripple-lib.png)](https://www.npmjs.org/package/ripple-lib) + `ripple-lib` connects to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser. -**Use ripple-lib for** +###Use ripple-lib for: + Connecting to a local or remote rippled in JavaScript (Node.js or browser) + Issuing [rippled API](https://ripple.com/wiki/JSON_Messages) requests diff --git a/build/sjcl.js b/build/sjcl.js index 8e2c9a2a..5b4baf99 100644 --- a/build/sjcl.js +++ b/build/sjcl.js @@ -2914,8 +2914,16 @@ sjcl.ecc.point = function(curve,x,y) { if (x === undefined) { this.isIdentity = true; } else { + if (x instanceof sjcl.bn) { + x = new curve.field(x); + } + if (y instanceof sjcl.bn) { + y = new curve.field(y); + } + this.x = x; this.y = y; + this.isIdentity = false; } this.curve = curve; @@ -3428,6 +3436,90 @@ sjcl.keyexchange.srp = { }; +/** + * Check that the point is valid based on the method described in + * SEC 1: Elliptic Curve Cryptography, section 3.2.2.1: + * Elliptic Curve Public Key Validation Primitive + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isValidPoint = function() { + + var self = this; + + var field_modulus = self.curve.field.modulus; + + if (self.isIdentity) { + return false; + } + + // Check that coordinatres are in bounds + // Return false if x < 1 or x > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.x)) && + !self.x.equals(1)) || + (self.x.greaterEquals(field_modulus.sub(1))) && + !self.x.equals(1)) { + + return false; + } + + // Return false if y < 1 or y > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.y)) && + !self.y.equals(1)) || + (self.y.greaterEquals(field_modulus.sub(1))) && + !self.y.equals(1)) { + + return false; + } + + if (!self.isOnCurve()) { + return false; + } + + // TODO check to make sure point is a scalar multiple of base_point + + return true; + +}; + +/** + * Check that the point is on the curve + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isOnCurve = function() { + + var self = this; + + var field_order = self.curve.r; + var component_a = self.curve.a; + var component_b = self.curve.b; + var field_modulus = self.curve.field.modulus; + + var y_squared_mod_field_order = self.y.mul(self.y).mod(field_modulus); + var x_cubed_plus_ax_plus_b = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus); + + return y_squared_mod_field_order.equals(x_cubed_plus_ax_plus_b); + +}; + + +sjcl.ecc.point.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + + ')'; +}; + +sjcl.ecc.pointJac.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + ', ' + + this.z.toString() + + ')'; +}; + // ----- for secp256k1 ------ // Overwrite NIST-P256 with secp256k1 @@ -3492,7 +3584,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () { var f = e.square(); var x = f.sub(d.copy().doubleM()); var y = e.mul(d.sub(x)).subM(c.doubleM().doubleM().doubleM()); - var z = this.y.mul(this.z).doubleM(); + var z = this.z.mul(this.y).doubleM(); return new sjcl.ecc.pointJac(this.curve, x, y, z); }; @@ -4012,37 +4104,63 @@ sjcl.bn.prototype.powermodMontgomery = function (e, m) return z.revert(r); } -sjcl.ecc.ecdsa.secretKey.prototype = { - sign: function(hash, paranoia) { - var R = this._curve.r, - l = R.bitLength(), - k = sjcl.bn.random(R.sub(1), paranoia).add(1), - r = this._curve.G.mult(k).x.mod(R), - s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); +sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) { + var R = this._curve.r, + l = R.bitLength(); - return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); + // k_for_testing should ONLY BE SPECIFIED FOR TESTING + // specifying it will make the signature INSECURE + var k; + if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') { + k = k_for_testing; + } else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) { + k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing)); + } else { + // This is the only option that should be used in production + k = sjcl.bn.random(R.sub(1), paranoia).add(1); } + + var r = this._curve.G.mult(k).x.mod(R); + var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); + + return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); }; -sjcl.ecc.ecdsa.publicKey.prototype = { - verify: function(hash, rs) { - var w = sjcl.bitArray, - R = this._curve.r, - l = R.bitLength(), - r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), - s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), - sInv = s.inverseMod(R), - hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), - hA = r.mul(sInv).mod(R), - r2 = this._curve.G.mult2(hG, hA, this._point).x; +sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(), + r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), + sInv = s.inverseMod(R), + hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), + hA = r.mul(sInv).mod(R), + r2 = this._curve.G.mult2(hG, hA, this._point).x; - if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { - throw (new sjcl.exception.corrupt("signature didn't check out")); - } - return true; + if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { + throw (new sjcl.exception.corrupt("signature didn't check out")); } + return true; }; +sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(); + + var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)); + + // For a canonical signature we want the lower of two possible values for s + // 0 < s <= n/2 + if (!R.copy().halveM().greaterEquals(s)) { + s = R.sub(s); + } + + return w.concat(r.toBits(l), s.toBits(l)); +}; + + sjcl.ecc.ecdsa.secretKey.prototype.signDER = function(hash, paranoia) { return this.encodeDER(this.sign(hash, paranoia)); }; @@ -4078,6 +4196,313 @@ sjcl.ecc.ecdsa.secretKey.prototype.encodeDER = function(rs) { }; +/** + * This module uses the public key recovery method + * described in SEC 1: Elliptic Curve Cryptography, + * section 4.1.6, "Public Key Recovery Operation". + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * Implementation based on: + * https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js + */ + +// Defined here so that this value only needs to be calculated once +var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR; + +/** + * Sign the given hash such that the public key, prepending an extra byte + * so that the public key will be recoverable from the signature + * + * @param {bitArray} hash + * @param {Number} paranoia + * @returns {bitArray} Signature formatted as bitArray + */ +sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) { + + var self = this; + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + // Sign hash with standard, canonicalized method + var standard_signature = self.sign(hash_bits, paranoia, k_for_testing); + var canonical_signature = self.canonicalizeSignature(standard_signature); + + // Extract r and s signature components from canonical signature + var r_and_s = getRandSFromSignature(self._curve, canonical_signature); + + // Rederive public key + var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get())); + + // Determine recovery factor based on which possible value + // returns the correct public key + var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key); + + // Prepend recovery_factor to signature and encode in DER + // The value_to_prepend should be 4 bytes total + var value_to_prepend = recovery_factor + 27; + + var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature); + + // Return value in bits + return final_signature_bits; + +}; + + +/** + * Recover the public key from a signature created with the + * signWithRecoverablePublicKey method in this module + * + * @static + * + * @param {bitArray} hash + * @param {bitArray} signature + * @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve + * @returns {sjcl.ecc.ecdsa.publicKey} Public key + */ +sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) { + + if (!signature || signature instanceof sjcl.ecc.curve) { + throw new sjcl.exception.invalid('must supply hash and signature to recover public key'); + } + + if (!curve) { + curve = sjcl.ecc.curves['c256']; + } + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + var signature_bits; + if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') { + signature_bits = signature; + } else { + throw new sjcl.exception.invalid('signature. Must be a bitArray'); + } + + // Extract recovery_factor from first 4 bytes + var recovery_factor = signature_bits[0] - 27; + + if (recovery_factor < 0 || recovery_factor > 3) { + throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' + + 'that prepends the recovery factor in order to recover the public key'); + } + + // Separate r and s values + var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1)); + var signature_r = r_and_s.r; + var signature_s = r_and_s.s; + + // Recover public key using recovery_factor + var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor); + var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point); + + return recovered_public_key; + +}; + + +/** + * Retrieve the r and s components of a signature + * + * @param {sjcl.ecc.curve} curve + * @param {bitArray} signature + * @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn + */ +function getRandSFromSignature(curve, signature) { + + var r_length = curve.r.bitLength(); + + return { + r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)), + s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature))) + }; +}; + + +/** + * Determine the recovery factor by trying all four + * possibilities and figuring out which results in the + * correct public key + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {sjcl.ecc.point} original_public_key_point + * @returns {Number, 0-3} Recovery factor + */ +function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) { + + var original_public_key_point_bits = original_public_key_point.toBits(); + + // TODO: verify that it is possible for the recovery_factor to be 2 or 3, + // we may only need 1 bit because the canonical signature might remove the + // possibility of us needing to "use the second candidate key" + for (var possible_factor = 0; possible_factor < 4; possible_factor++) { + + var resulting_public_key_point; + try { + resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor); + } catch (err) { + // console.log(err, err.stack); + continue; + } + + if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) { + return possible_factor; + } + + } + + throw new sjcl.exception.bug('unable to calculate recovery factor from signature'); + +}; + + +/** + * Recover the public key from the signature. + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {Number, 0-3} recovery_factor + * @returns {sjcl.point} Public key corresponding to signature + */ +function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) { + + var field_order = curve.r; + var field_modulus = curve.field.modulus; + + // Reduce the recovery_factor to the two bits used + recovery_factor = recovery_factor & 3; + + // The less significant bit specifies whether the y coordinate + // of the compressed point is even or not. + var compressed_point_y_coord_is_even = recovery_factor & 1; + + // The more significant bit specifies whether we should use the + // first or second candidate key. + var use_second_candidate_key = recovery_factor >> 1; + + // Calculate (field_order + 1) / 4 + if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) { + FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4); + } + + // In the paper they write "1. For j from 0 to h do the following..." + // That is not necessary here because we are given the recovery_factor + // step 1.1 Let x = r + jn + // Here "j" is either 0 or 1 + var x; + if (use_second_candidate_key) { + x = signature_r.add(field_order); + } else { + x = signature_r; + } + + // step 1.2 and 1.3 convert x to an elliptic curve point + // Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion + var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus); + var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus); + + // If beta is even but y isn't or + // if beta is odd and y is even + // then subtract beta from the field_modulus + var y; + var beta_is_even = beta.mod(2).equals(0); + if (beta_is_even && !compressed_point_y_coord_is_even || + !beta_is_even && compressed_point_y_coord_is_even) { + y = beta; + } else { + y = field_modulus.sub(beta); + } + + // generated_point_R is the point generated from x and y + var generated_point_R = new sjcl.ecc.point(curve, x, y); + + // step 1.4 check that R is valid and R x field_order !== infinity + // TODO: add check for R x field_order === infinity + if (!generated_point_R.isValidPoint()) { + throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key'); + } + + // step 1.5 Compute e from M + var message_e = sjcl.bn.fromBits(hash_bits); + var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order); + + // step 1.6 Compute Q = r^-1 (sR - eG) + // console.log('r: ', signature_r); + var signature_r_inv = signature_r.inverseMod(field_order); + var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv); + + // Validate public key point + if (!public_key_point.isValidPoint()) { + throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key'); + } + + // Verify that this public key matches the signature + if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) { + throw new sjcl.exception.corrupt('cannot recover public key'); + } + + return public_key_point; + +}; + + +/** + * Verify a signature given the raw components + * using method defined in section 4.1.5: + * "Alternative Verifying Operation" + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} e + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {sjcl.ecc.point} public_key_point + * @returns {Boolean} + */ +function verify_raw(curve, e, r, s, public_key_point) { + + var field_order = curve.r; + + // Return false if r is out of bounds + if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Return false if s is out of bounds + if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Check that r = (u1 + u2)G + // u1 = e x s^-1 (mod field_order) + // u2 = r x s^-1 (mod field_order) + var s_mod_inverse_field_order = s.inverseMod(field_order); + var u1 = e.mul(s_mod_inverse_field_order).mod(field_order); + var u2 = r.mul(s_mod_inverse_field_order).mod(field_order); + + var point_computed = curve.G.mult2(u1, u2, public_key_point); + + return r.equals(point_computed.x.mod(field_order)); + +}; + + sjcl.bn.prototype.jacobi = function (that) { var a = this; that = new sjcl.bn(that); diff --git a/package.json b/package.json index 88bea9b5..732bb086 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ripple-lib", - "version": "0.7.35", + "version": "0.7.36", "description": "Ripple JavaScript client library", "files": [ "src/js/*", diff --git a/src/js/ripple/account.js b/src/js/ripple/account.js index 4f221068..b1a0f144 100644 --- a/src/js/ripple/account.js +++ b/src/js/ripple/account.js @@ -10,13 +10,15 @@ // // var network = require("./network.js"); - +var async = require('async'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var extend = require('extend'); var Amount = require('./amount').Amount; var UInt160 = require('./uint160').UInt160; var TransactionManager = require('./transactionmanager').TransactionManager; +var sjcl = require('./utils').sjcl; +var Base = require('./base').Base; /** * @constructor Account @@ -278,6 +280,124 @@ Account.prototype.submit = function(transaction) { this._transactionManager.submit(transaction); }; + +/** + * Check whether the given public key is valid for this account + * + * @param {Hex-encoded String|RippleAddress} public_key + * @param {Function} callback + * + * @callback + * @param {Error} err + * @param {Boolean} true if the public key is valid and active, false otherwise + */ +Account.prototype.publicKeyIsActive = function(public_key, callback) { + + var self = this; + + var public_key_as_uint160; + try { + public_key_as_uint160 = Account._publicKeyToAddress(public_key); + } catch (err) { + return callback(err); + } + + function getAccountInfo(async_callback) { + self.getInfo(function(err, account_info_res){ + + // If the remote responds with an Account Not Found error then the account + // is unfunded and thus we can assume that the master key is active + if (err && err.remote && err.remote.error === 'actNotFound') { + async_callback(null, null); + } else { + async_callback(err, account_info_res); + } + }); + }; + + function publicKeyIsValid(account_info_res, async_callback) { + // Catch the case of unfunded accounts + if (!account_info_res) { + + if (public_key_as_uint160 === self._account_id) { + async_callback(null, true); + } else { + async_callback(null, false); + } + + return; + } + + var account_info = account_info_res.account_data; + + // Respond with true if the RegularKey is set and matches the given public key or + // if the public key matches the account address and the lsfDisableMaster is not set + if (account_info.RegularKey && + account_info.RegularKey === public_key_as_uint160) { + + async_callback(null, true); + + } else if (account_info.Account === public_key_as_uint160 && + ((account_info.Flags & 0x00100000) === 0)) { + + async_callback(null, true); + + } else { + + async_callback(null, false); + + } + + }; + + var steps = [ + getAccountInfo, + publicKeyIsValid + ]; + + async.waterfall(steps, callback); + +}; + +/** + * Convert a hex-encoded public key to a Ripple Address + * + * @static + * + * @param {Hex-encoded string|RippleAddress} public_key + * @returns {RippleAddress} + */ +Account._publicKeyToAddress = function(public_key) { + + // Based on functions in /src/js/ripple/keypair.js + function hexToUInt160(public_key) { + + var bits = sjcl.codec.hex.toBits(public_key); + var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); + var address = UInt160.from_bits(hash); + address.set_version(Base.VER_ACCOUNT_ID); + + return address.to_json(); + + } + + if (UInt160.is_valid(public_key)) { + + return public_key; + + } else if (/^[0-9a-fA-F]+$/.test(public_key)) { + + return hexToUInt160(public_key); + + } else { + + throw(new Error('Public key is invalid. Must be a UInt160 or a hex string')); + + } +}; + + + exports.Account = Account; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index d5dfc5ba..1d4440ad 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -71,8 +71,8 @@ Amount.from_json = function (j) { return (new Amount()).parse_json(j); }; -Amount.from_quality = function (q, c, i) { - return (new Amount()).parse_quality(q, c, i); +Amount.from_quality = function (quality, currency, issuer, opts) { + return (new Amount()).parse_quality(quality, currency, issuer, opts); }; Amount.from_human = function (j, opts) { @@ -159,6 +159,29 @@ Amount.prototype.add = function (v) { return result; }; +/** + * Turn this amount into its inverse. + * + * @private + */ +Amount.prototype._invert = function () { + this._value = consts.bi_1e32.divide(this._value); + this._offset = -32 - this._offset; + this.canonicalize(); + + return this; +}; + +/** + * Return the inverse of this amount. + * + * @return {Amount} New Amount object with same currency and issuer, but the + * inverse of the value. + */ +Amount.prototype.invert = function () { + return this.copy()._invert(); +}; + Amount.prototype.canonicalize = function () { if (!(this._value instanceof BigInteger)) { // NaN. @@ -349,9 +372,14 @@ Amount.prototype.divide = function (d) { * * @this {Amount} The numerator (top half) of the fraction. * @param {Amount} denominator The denominator (bottom half) of the fraction. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The resulting ratio. Unit will be the same as numerator. */ -Amount.prototype.ratio_human = function (denominator) { +Amount.prototype.ratio_human = function (denominator, opts) { + opts = opts || {}; + if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) { // Special handling of integer arguments denominator = Amount.from_json('' + denominator + '.0'); @@ -367,6 +395,14 @@ Amount.prototype.ratio_human = function (denominator) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + denominator = denominator.applyInterest(opts.reference_date); + } + // Special case: The denominator is a native (XRP) amount. // // In that case, it's going to be expressed as base units (1 XRP = @@ -402,9 +438,14 @@ Amount.prototype.ratio_human = function (denominator) { * * @this {Amount} The first factor of the product. * @param {Amount} factor The second factor of the product. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The product. Unit will be the same as the first factor. */ -Amount.prototype.product_human = function (factor) { +Amount.prototype.product_human = function (factor, opts) { + opts = opts || {}; + if (typeof factor === 'number' && parseInt(factor, 10) === factor) { // Special handling of integer arguments factor = Amount.from_json(String(factor) + '.0'); @@ -417,6 +458,14 @@ Amount.prototype.product_human = function (factor) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + factor = factor.applyInterest(opts.reference_date); + } + var product = this.multiply(factor); // Special case: The second factor is a native (XRP) amount expressed as base @@ -618,17 +667,81 @@ Amount.prototype.parse_issuer = function (issuer) { return this; }; -// --> h: 8 hex bytes quality or 32 hex bytes directory index. -Amount.prototype.parse_quality = function (q, c, i) { +/** + * Decode a price from a BookDirectory index. + * + * BookDirectory ledger entries each encode the offer price in their index. This + * method can decode that information and populate an Amount object with it. + * + * It is possible not to provide a currency or issuer, but be aware that Amount + * objects behave differently based on the currency, so you may get incorrect + * results. + * + * Prices involving demurraging currencies are tricky, since they depend on the + * base and counter currencies. + * + * @param quality {String} 8 hex bytes quality or 32 hex bytes BookDirectory + * index. + * @param counterCurrency {Currency|String} Currency of the resulting Amount + * object. + * @param counterIssuer {Issuer|String} Issuer of the resulting Amount object. + * @param opts Additional options + * @param opts.inverse {Boolean} If true, return the inverse of the price + * encoded in the quality. + * @param opts.base_currency {Currency|String} The other currency. This plays a + * role with interest-bearing or demurrage currencies. In that case the + * demurrage has to be applied when the quality is decoded, otherwise the + * price will be false. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @param opts.xrp_as_drops {Boolean} Whether XRP amount should be treated as + * drops. When the base currency is XRP, the quality is calculated in drops. + * For human use however, we want to think of 1000000 drops as 1 XRP and + * prices as per-XRP instead of per-drop. + */ +Amount.prototype.parse_quality = function (quality, counterCurrency, counterIssuer, opts) +{ + opts = opts || {}; + + var baseCurrency = Currency.from_json(opts.base_currency); + this._is_negative = false; - this._value = new BigInteger(q.substring(q.length-14), 16); - this._offset = parseInt(q.substring(q.length-16, q.length-14), 16)-100; - this._currency = Currency.from_json(c); - this._issuer = UInt160.from_json(i); + this._value = new BigInteger(quality.substring(quality.length-14), 16); + this._offset = parseInt(quality.substring(quality.length-16, quality.length-14), 16)-100; + this._currency = Currency.from_json(counterCurrency); + this._issuer = UInt160.from_json(counterIssuer); this._is_native = this._currency.is_native(); + // Correct offset if xrp_as_drops option is not set and base currency is XRP + if (!opts.xrp_as_drops && + baseCurrency.is_valid() && + baseCurrency.is_native()) { + if (opts.inverse) { + this._offset -= 6; + } else { + this._offset += 6; + } + } + + if (opts.inverse) { + this._invert(); + } + this.canonicalize(); + if (opts.reference_date && baseCurrency.is_valid() && baseCurrency.has_interest()) { + var interest = baseCurrency.get_interest_at(opts.reference_date); + + // XXX If we had better math utilities, we wouldn't need this hack. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + var v = this.divide(interestTempAmount); + this._value = v._value; + this._offset = v._offset; + } + } + return this; } @@ -850,6 +963,39 @@ Amount.prototype.to_text = function (allow_nan) { return result; }; +/** + * Calculate present value based on currency and a reference date. + * + * This only affects demurraging and interest-bearing currencies. + * + * User should not store amount objects after the interest is applied. This is + * intended by display functions such as toHuman(). + * + * @param referenceDate {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @return {Amount} The amount with interest applied. + */ +Amount.prototype.applyInterest = function (referenceDate) { + if (this._currency.has_interest()) { + var interest = this._currency.get_interest_at(referenceDate); + + // XXX Because the Amount parsing routines don't support some of the things + // that JavaScript can output when casting a float to a string, the + // following call sometimes does not produce a valid Amount. + // + // The correct way to solve this is probably to switch to a proper + // BigDecimal for our internal representation and then use that across + // the board instead of instantiating these dummy Amount objects. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + return this.multiply(interestTempAmount); + } + } else { + return this; + } +}; + /** * Format only value in a human-readable format. * @@ -885,21 +1031,8 @@ Amount.prototype.to_human = function (opts) { // Apply demurrage/interest var ref = this; - if (opts.reference_date && this._currency.has_interest()) { - var interest = this._currency.get_interest_at(opts.reference_date); - - // XXX Because the Amount parsing routines don't support some of the things - // that JavaScript can output when casting a float to a string, the - // following call sometimes does not produce a valid Amount. - // - // The correct way to solve this is probably to switch to a proper - // BigDecimal for our internal representation and then use that across - // the board instead of instantiating these dummy Amount objects. - var interestTempAmount = Amount.from_json(""+interest+"/1/1"); - - if (interestTempAmount.is_valid()) { - ref = this.multiply(interestTempAmount); - } + if (opts.reference_date) { + ref = this.applyInterest(opts.reference_date); } var order = ref._is_native ? consts.xns_precision : -ref._offset; diff --git a/src/js/ripple/index.js b/src/js/ripple/index.js index 92ae258a..a6cfa2ce 100644 --- a/src/js/ripple/index.js +++ b/src/js/ripple/index.js @@ -11,6 +11,7 @@ exports.Seed = require('./seed').Seed; exports.Meta = require('./meta').Meta; exports.SerializedObject = require('./serializedobject').SerializedObject; exports.RippleError = require('./rippleerror').RippleError; +exports.Message = require('./message'); exports.binformat = require('./binformat'); exports.utils = require('./utils'); diff --git a/src/js/ripple/keypair.js b/src/js/ripple/keypair.js index db82e79d..518ae70d 100644 --- a/src/js/ripple/keypair.js +++ b/src/js/ripple/keypair.js @@ -89,8 +89,10 @@ KeyPair.prototype.get_address = function () { }; KeyPair.prototype.sign = function (hash) { - var hash = UInt256.from_json(hash); - return this._secret.signDER(hash.to_bits(), 0); + hash = UInt256.from_json(hash); + var sig = this._secret.sign(hash.to_bits(), 0); + sig = this._secret.canonicalizeSignature(sig); + return this._secret.encodeDER(sig); }; exports.KeyPair = KeyPair; diff --git a/src/js/ripple/message.js b/src/js/ripple/message.js new file mode 100644 index 00000000..a6043740 --- /dev/null +++ b/src/js/ripple/message.js @@ -0,0 +1,203 @@ +var async = require('async'); +var crypto = require('crypto'); +var sjcl = require('./utils').sjcl; +var Remote = require('./remote').Remote; +var Seed = require('./seed').Seed; +var KeyPair = require('./keypair').KeyPair; +var Account = require('./account').Account; +var UInt160 = require('./uint160').UInt160; + +// Message class (static) +var Message = {}; + +Message.HASH_FUNCTION = sjcl.hash.sha512.hash; +Message.MAGIC_BYTES = 'Ripple Signed Message:\n'; + +var REGEX_HEX = /^[0-9a-fA-F]+$/; +var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/; + +/** + * Produce a Base64-encoded signature on the given message with + * the string 'Ripple Signed Message:\n' prepended. + * + * Note that this signature uses the signing function that includes + * a recovery_factor to be able to extract the public key from the signature + * without having to pass the public key along with the signature. + * + * @static + * + * @param {String} message + * @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key + * @param {RippleAddress} [The first key] account Field to specify the signing account. + * If this is omitted the first account produced by the secret generator will be used. + * @returns {Base64-encoded String} signature + */ +Message.signMessage = function(message, secret_key, account) { + + return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key, account); + +}; + +/** + * Produce a Base64-encoded signature on the given hex-encoded hash. + * + * Note that this signature uses the signing function that includes + * a recovery_factor to be able to extract the public key from the signature + * without having to pass the public key along with the signature. + * + * @static + * + * @param {bitArray|Hex-encoded String} hash + * @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key + * @param {RippleAddress} [The first key] account Field to specify the signing account. + * If this is omitted the first account produced by the secret generator will be used. + * @returns {Base64-encoded String} signature + */ +Message.signHash = function(hash, secret_key, account) { + + if (typeof hash === 'string' && /^[0-9a-fA-F]+$/.test(hash)) { + hash = sjcl.codec.hex.toBits(hash); + } + + if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') { + throw new Error('Hash must be a bitArray or hex-encoded string'); + } + + if (!(secret_key instanceof sjcl.ecc.ecdsa.secretKey)) { + secret_key = Seed.from_json(secret_key).get_key(account)._secret; + } + + var signature_bits = secret_key.signWithRecoverablePublicKey(hash); + var signature_base64 = sjcl.codec.base64.fromBits(signature_bits); + + return signature_base64; + +}; + + +/** + * Verify the signature on a given message. + * + * Note that this function is asynchronous. + * The ripple-lib remote is used to check that the public + * key extracted from the signature corresponds to one that is currently + * active for the given account. + * + * @static + * + * @param {String} data.message + * @param {RippleAddress} data.account + * @param {Base64-encoded String} data.signature + * @param {ripple-lib Remote} remote + * @param {Function} callback + * + * @callback callback + * @param {Error} error + * @param {boolean} is_valid true if the signature is valid, false otherwise + */ +Message.verifyMessageSignature = function(data, remote, callback) { + + if (typeof data.message === 'string') { + data.hash = Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message); + } else { + return callback(new Error('Data object must contain message field to verify signature')); + } + + return Message.verifyHashSignature(data, remote, callback); + +}; + + +/** + * Verify the signature on a given hash. + * + * Note that this function is asynchronous. + * The ripple-lib remote is used to check that the public + * key extracted from the signature corresponds to one that is currently + * active for the given account. + * + * @static + * + * @param {bitArray|Hex-encoded String} data.hash + * @param {RippleAddress} data.account + * @param {Base64-encoded String} data.signature + * @param {ripple-lib Remote} remote + * @param {Function} callback + * + * @callback callback + * @param {Error} error + * @param {boolean} is_valid true if the signature is valid, false otherwise + */ +Message.verifyHashSignature = function(data, remote, callback) { + + var hash, + account, + signature; + + if(typeof callback !== 'function') { + throw new Error('Must supply callback function'); + } + + hash = data.hash; + if (hash && typeof hash === 'string' && REGEX_HEX.test(hash)) { + hash = sjcl.codec.hex.toBits(hash); + } + + if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') { + return callback(new Error('Hash must be a bitArray or hex-encoded string')); + } + + account = data.account || data.address; + if (!account || !UInt160.from_json(account).is_valid()) { + return callback(new Error('Account must be a valid ripple address')); + } + + signature = data.signature; + if (typeof signature !== 'string' || !REGEX_BASE64.test(signature)) { + return callback(new Error('Signature must be a Base64-encoded string')); + } + signature = sjcl.codec.base64.toBits(signature); + + if (!(remote instanceof Remote) || remote.state !== 'online') { + return callback(new Error('Must supply connected Remote to verify signature')); + } + + function recoverPublicKey (async_callback) { + + var public_key; + try { + public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + } catch (err) { + return async_callback(err); + } + + if (public_key) { + async_callback(null, public_key); + } else { + async_callback(new Error('Could not recover public key from signature')); + } + + }; + + function checkPublicKeyIsValid (public_key, async_callback) { + + // Get hex-encoded public key + var key_pair = new KeyPair(); + key_pair._pubkey = public_key; + var public_key_hex = key_pair.to_hex_pub(); + + var account_class_instance = new Account(remote, account); + account_class_instance.publicKeyIsActive(public_key_hex, async_callback); + + }; + + var steps = [ + recoverPublicKey, + checkPublicKeyIsValid + ]; + + async.waterfall(steps, callback); + +}; + +module.exports = Message; diff --git a/src/js/ripple/orderbook.js b/src/js/ripple/orderbook.js index d51f3e72..d667289b 100644 --- a/src/js/ripple/orderbook.js +++ b/src/js/ripple/orderbook.js @@ -225,11 +225,12 @@ OrderBook.prototype.notify = function (message) { break; case 'CreatedNode': - var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets); + // XXX Should use Amount#from_quality + var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets, {reference_date: new Date()}); for (i = 0, l = self._offers.length; i < l; i++) { offer = self._offers[i]; - var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets); + var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets, {reference_date: new Date()}); if (price.compareTo(priceItem) <= 0) { var obj = an.fields; diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index afe54293..85c715b9 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -46,6 +46,7 @@ var log = require('./log').internal.sub('remote'); max_fee : Maximum acceptable transaction fee fee_cushion : Extra fee multiplier to account for async fee changes. servers : Array of server objects with the following form + canonical_signing : Signatures should be canonicalized and the "canonical" flag set { host: @@ -78,10 +79,11 @@ function Remote(opts, trace) { var self = this; - this.trusted = opts.trusted; - this.local_sequence = opts.local_sequence; // Locally track sequence numbers + this.trusted = Boolean(opts.trusted); + this.local_sequence = Boolean(opts.local_sequence); // Locally track sequence numbers this.local_fee = (typeof opts.local_fee === 'undefined') ? true : opts.local_fee; // Locally set fees this.local_signing = (typeof opts.local_signing === 'undefined') ? true : opts.local_signing; + this.canonical_signing = (typeof opts.canonical_signing === 'undefined') ? true : opts.canonical_signing; this.fee_cushion = (typeof opts.fee_cushion === 'undefined') ? 1.2 : opts.fee_cushion; this.max_fee = (typeof opts.max_fee === 'undefined') ? Infinity : opts.max_fee; this.id = 0; @@ -239,20 +241,20 @@ function Remote(opts, trace) { self.storage.getPendingTransactions(function(err, transactions) { if (err || !Array.isArray(transactions)) return; - var properties = [ - 'submittedIDs', - 'clientID', - 'submitIndex' - ]; - function resubmitTransaction(tx) { var transaction = self.transaction(); transaction.parseJson(tx.tx_json); - properties.forEach(function(prop) { - if (typeof tx[prop] !== 'undefined') { - transaction[prop] = tx[prop]; + + Object.keys(tx).forEach(function(prop) { + switch (prop) { + case 'submittedIDs': + case 'clientID': + case 'submitIndex': + transaction[prop] = tx[prop]; + break; } }); + transaction.submit(); }; @@ -344,16 +346,16 @@ Remote.prototype.addServer = function(opts) { server.on('message', serverMessage); function serverConnect() { + self._connection_count += 1; + if (opts.primary || !self._primary_server) { self._setPrimaryServer(server); } - switch (++self._connection_count) { - case 1: - self._setState('online'); - break; - case self._servers.length: - self.emit('ready'); - break; + if (self._connection_count === 1) { + self._setState('online'); + } + if (self._connection_count === self._servers.length) { + self.emit('ready'); } }; @@ -529,7 +531,7 @@ Remote.prototype._handleMessage = function(message, server) { // De-duplicate transactions that are immediately following each other var hash = message.transaction.hash; - if (this._received_tx.hasOwnProperty(hash)) { + if (this._received_tx.get(hash)) { break; } @@ -699,20 +701,18 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { request.message.ledger = ledger; } - var requestFields = [ - 'full', - 'expand', - 'transactions', - 'accounts' - ]; - switch (typeof options) { case 'object': - for (var key in options) { - if (~requestFields.indexOf(key)) { - request.message[key] = true; + Object.keys(options).forEach(function(o) { + switch (o) { + case 'full': + case 'expand': + case 'transactions': + case 'accounts': + request.message[o] = true; + break; } - } + }, options); break; case 'function': @@ -732,7 +732,7 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { return request; }; -// Only for unit testing. +Remote.prototype.requestLedgerClosed = Remote.prototype.requestLedgerHash = function(callback) { //utils.assert(this.trusted); // If not trusted, need to check proof. return new Request(this, 'ledger_closed').callback(callback); @@ -975,26 +975,24 @@ Remote.prototype.requestAccountTx = function(options, callback) { var request = new Request(this, 'account_tx'); - var requestFields = [ - 'account', - 'ledger_index_min', //earliest - 'ledger_index_max', //latest - 'binary', //false - 'count', //false - 'descending', //false - 'offset', //0 - 'limit', + Object.keys(options).forEach(function(o) { + switch (o) { + case 'account': + case 'ledger_index_min': //earliest + case 'ledger_index_max': //latest + case 'binary': //false + case 'count': //false + case 'descending': //false + case 'offset': //0 + case 'limit': - //extended account_tx - 'forward', //false - 'marker' - ]; - - for (var key in options) { - if (~requestFields.indexOf(key)) { - request.message[key] = options[key]; + //extended account_tx + case 'forward': //false + case 'marker': + request.message[o] = this[o]; + break; } - } + }, options); function propertiesFilter(obj, transaction) { var properties = Object.keys(obj); @@ -1259,6 +1257,7 @@ Remote.accountRootRequest = function(type, responseFilter, account, ledger, call } var request = this.requestLedgerEntry('account_root'); + request.accountRoot(account); request.ledgerChoose(ledger); @@ -1314,7 +1313,7 @@ Remote.prototype.getAccount = function(accountID) { Remote.prototype.addAccount = function(accountID) { var account = new Account(this, accountID); - if (account.is_valid()) { + if (account.isValid()) { this._accounts[accountID] = account; } @@ -1666,7 +1665,7 @@ Remote.prototype.transaction = function(source, options, callback) { break; case 'string': - transactionType = source.toLowerCase(); + transactionType = transactionTypes[source.toLowerCase()]; if (!transactionType) { throw new Error('Invalid transaction type: ' + transactionType); diff --git a/src/js/ripple/request.js b/src/js/ripple/request.js index 6aacb966..c7261bc6 100644 --- a/src/js/ripple/request.js +++ b/src/js/ripple/request.js @@ -7,6 +7,7 @@ var Account = require('./account').Account; var Meta = require('./meta').Meta; var OrderBook = require('./orderbook').OrderBook; var RippleError = require('./rippleerror').RippleError; +var Server = require('./server').Server; // Request events emitted: // 'success' : Request successful. @@ -17,12 +18,9 @@ var RippleError = require('./rippleerror').RippleError; function Request(remote, command) { EventEmitter.call(this); - this.remote = remote; - this.requested = false; - this.message = { - command : command, - id : void(0) - }; + this.remote = remote; + this.requested = false; + this.message = { command: command, id: void(0) }; }; util.inherits(Request, EventEmitter); @@ -37,6 +35,7 @@ Request.prototype.request = function(remote) { if (this.requested) return; this.requested = true; + this.on('error', new Function); this.emit('request', remote); @@ -44,7 +43,7 @@ Request.prototype.request = function(remote) { this.remote._servers.forEach(function(server) { this.setServer(server); this.remote.request(this); - }, this ); + }, this); } else { this.remote.request(this); } @@ -53,37 +52,40 @@ Request.prototype.request = function(remote) { }; Request.prototype.callback = function(callback, successEvent, errorEvent) { - if (callback && typeof callback === 'function') { - var self = this; + var self = this; - function request_success(message) { - callback.call(self, null, message); - } - - function request_error(error) { - if (!(error instanceof RippleError)) { - error = new RippleError(error); - } - callback.call(self, error); - } - - this.once(successEvent || 'success', request_success); - this.once(errorEvent || 'error' , request_error); - this.request(); + if (this.requestsed || typeof callback !== 'function') { + return this; } + function requestSuccess(message) { + callback.call(self, null, message); + }; + + function requestError(error) { + if (!(error instanceof RippleError)) { + error = new RippleError(error); + } + callback.call(self, error); + }; + + this.once(successEvent || 'success', requestSuccess); + this.once(errorEvent || 'error' , requestError); + this.request(); + return this; }; Request.prototype.timeout = function(duration, callback) { var self = this; + function requested() { + self.timeout(duration, callback); + }; + if (!this.requested) { - function requested() { - self.timeout(duration, callback); - } - this.once('request', requested); - return; + // Defer until requested + return this.once('request', requested); } var emit = this.emit; @@ -112,8 +114,11 @@ Request.prototype.setServer = function(server) { case 'object': selected = server; break; + case 'string': + // Find server with hostname string var servers = this.remote._servers; + for (var i=0, s; s=servers[i]; i++) { if (s._host === server) { selected = s; @@ -123,18 +128,19 @@ Request.prototype.setServer = function(server) { break; }; - this.server = selected; + if (selected instanceof Server) { + this.server = selected; + } return this; }; Request.prototype.buildPath = function(build) { - if (this.remote.local_signing) { throw new Error( - '`build_path` is completely ignored when doing local signing as ' + - '`Paths` is a component of the signed blob. The `tx_blob` is signed,' + - 'sealed and delivered, and the txn unmodified after' ); + '`build_path` is completely ignored when doing local signing as ' + + '`Paths` is a component of the signed blob. The `tx_blob` is signed,' + + 'sealed and delivered, and the txn unmodified after' ); } if (build) { @@ -144,6 +150,7 @@ Request.prototype.buildPath = function(build) { // value being `truthy` delete this.message.build_path } + return this; }; @@ -153,6 +160,7 @@ Request.prototype.ledgerChoose = function(current) { } else { this.message.ledger_hash = this.remote._ledger_hash; } + return this; }; @@ -171,8 +179,8 @@ Request.prototype.ledgerIndex = function(ledger_index) { return this; }; -Request.prototype.ledgerSelect = function(ledger_spec) { - switch (ledger_spec) { +Request.prototype.ledgerSelect = function(ledger) { + switch (ledger) { case 'current': case 'closed': case 'verified': @@ -180,10 +188,10 @@ Request.prototype.ledgerSelect = function(ledger_spec) { break; default: - if (Number(ledger_spec)) { - this.message.ledger_index = ledger_spec; - } else { - this.message.ledger_hash = ledger_spec; + if (isNaN(ledger)) { + this.message.ledger_hash = ledger; + } else if (ledger = Number(ledger)) { + this.message.ledger_index = ledger; } break; } @@ -196,8 +204,8 @@ Request.prototype.accountRoot = function(account) { return this; }; -Request.prototype.index = function(hash) { - this.message.index = hash; +Request.prototype.index = function(index) { + this.message.index = index; return this; }; @@ -305,7 +313,7 @@ Request.prototype.books = function(books, snapshot) { Request.prototype.addBook = function (book, snapshot) { if (!Array.isArray(this.message.books)) { - this.message.books = []; + this.message.books = [ ]; } var json = { }; diff --git a/src/js/ripple/server.js b/src/js/ripple/server.js index 1b393503..f7846e86 100644 --- a/src/js/ripple/server.js +++ b/src/js/ripple/server.js @@ -36,8 +36,17 @@ function Server(remote, opts) { throw new Error('Server host is malformed, use "host" and "port" server configuration'); } - if (typeof opts.port !== 'number') { - throw new TypeError('Server configuration "port" is not a Number'); + // We want to allow integer strings as valid port numbers for backward + // compatibility. + if (typeof opts.port === 'string') { + opts.port = parseFloat(opts.port); + } + + if (typeof opts.port !== 'number' || + opts.port >>> 0 !== parseFloat(opts.port) || // is integer? + opts.port < 1 || + opts.port > 65535) { + throw new TypeError('Server "port" must be an integer in range 1-65535'); } if (typeof opts.secure !== 'boolean') { diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 58dcc131..6ea4055f 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -44,6 +44,7 @@ var EventEmitter = require('events').EventEmitter; var util = require('util'); +var utils = require('./utils'); var sjcl = require('./utils').sjcl; var Amount = require('./amount').Amount; var Currency = require('./amount').Currency; @@ -73,34 +74,25 @@ function Transaction(remote) { // Index at which transaction was submitted this.submitIndex = void(0); + // Canonical signing setting defaults to the Remote's configuration + this.canonical = "object" === typeof remote ? !!remote.canonical_signing : true; + // We aren't clever enough to eschew preventative measures so we keep an array // of all submitted transactionIDs (which can change due to load_factor // effecting the Fee amount). This should be populated with a transactionID // any time it goes on the network - this.submittedIDs = [ ] - - function finalize(message) { - if (self.result) { - self.result.ledger_index = message.ledger_index; - self.result.ledger_hash = message.ledger_hash; - } else { - self.result = message; - self.result.tx_json = self.tx_json; - } - - self.emit('cleanup', message); - }; + this.submittedIDs = [ ]; this.once('success', function(message) { - self.finalized = true; + self.finalize(message); self.setState('validated'); - finalize(message); + self.emit('cleanup', message); }); this.once('error', function(message) { - self.finalized = true; + self.finalize(message); self.setState('failed'); - finalize(message); + self.emit('cleanup', message); }); this.once('submitted', function() { @@ -120,6 +112,11 @@ Transaction.fee_units = { }; Transaction.flags = { + // Universal flags can apply to any transaction type + Universal: { + FullyCanonicalSig: 0x80000000 + }, + AccountSet: { RequireDestTag: 0x00010000, OptionalDestTag: 0x00020000, @@ -149,6 +146,18 @@ Transaction.flags = { } }; +// The following are integer (as opposed to bit) flags +// that can be set for particular transactions in the +// SetFlag or ClearFlag field +Transaction.set_clear_flags = { + AccountSet: { + asfRequireDest: 1, + asfRequireAuth: 2, + asfDisallowXRP: 3, + asfDisableMaster: 4 + } +}; + Transaction.formats = require('./binformat').tx; Transaction.prototype.consts = { @@ -205,6 +214,20 @@ Transaction.prototype.setState = function(state) { } }; +Transaction.prototype.finalize = function(message) { + this.finalized = true; + + if (this.result) { + this.result.ledger_index = message.ledger_index; + this.result.ledger_hash = message.ledger_hash; + } else { + this.result = message; + this.result.tx_json = this.tx_json; + } + + return this; +}; + Transaction.prototype._accountSecret = function(account) { return this.remote.secrets[account]; }; @@ -258,9 +281,28 @@ Transaction.prototype._getServer = function() { */ Transaction.prototype.complete = function() { + if (this.remote) { + if (!this.remote.trusted && !this.remote.local_signing) { + this.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server')); + return false; + } + } + // Try to auto-fill the secret - if (!this._secret && !(this._secret = this._account_secret(this.tx_json.Account))) { - return this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); + if (!this._secret && !(this._secret = this._accountSecret(this.tx_json.Account))) { + this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); + return false; + } + + if (typeof this.tx_json.SigningPubKey === 'undefined') { + try { + var seed = Seed.from_json(this._secret); + var key = seed.get_key(this.tx_json.Account); + this.tx_json.SigningPubKey = key.to_hex_pub(); + } catch(e) { + this.emit('error', new RippleError('tejSecretInvalid', 'Invalid secret')); + return false; + } } // If the Fee hasn't been set, one needs to be computed by @@ -272,10 +314,18 @@ Transaction.prototype.complete = function() { } } - if (typeof this.tx_json.SigningPubKey === 'undefined') { - var seed = Seed.from_json(this._secret); - var key = seed.get_key(this.tx_json.Account); - this.tx_json.SigningPubKey = key.to_hex_pub(); + if (Number(this.tx_json.Fee) > this._maxFee) { + tx.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded')); + return false; + } + + // Set canonical flag - this enables canonicalized signature checking + if (this.remote && this.remote.local_signing && this.canonical) { + this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig; + + // JavaScript converts operands to 32-bit signed ints before doing bitwise + // operations. We need to convert it back to an unsigned int. + this.tx_json.Flags = this.tx_json.Flags >>> 0; } return this.tx_json; @@ -304,7 +354,8 @@ Transaction.prototype.hash = function(prefix, as_uint256) { return as_uint256 ? hash : hash.to_hex(); }; -Transaction.prototype.sign = function() { +Transaction.prototype.sign = function(callback) { + var callback = typeof callback === 'function' ? callback : function(){}; var seed = Seed.from_json(this._secret); var prev_sig = this.tx_json.TxnSignature; @@ -315,6 +366,7 @@ Transaction.prototype.sign = function() { // If the hash is the same, we can re-use the previous signature if (prev_sig && hash === this.previousSigningHash) { this.tx_json.TxnSignature = prev_sig; + callback(); return this; } @@ -325,6 +377,8 @@ Transaction.prototype.sign = function() { this.tx_json.TxnSignature = hex; this.previousSigningHash = hash; + callback(); + return this; }; @@ -481,20 +535,16 @@ Transaction.prototype.transferRate = function(rate) { Transaction.prototype.setFlags = function(flags) { if (!flags) return this; - var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; var flag_set = Array.isArray(flags) ? flags : Array.prototype.slice.call(arguments); - - // We plan to not define this field on new Transaction. - if (this.tx_json.Flags === void(0)) { - this.tx_json.Flags = 0; - } + var transaction_flags = Transaction.flags[this.tx_json.TransactionType] || { }; for (var i=0, l=flag_set.length; i= tx.tx_json.Sequence) return; + + submitFill(sequence, function() { if (++submitted === sequenceDif) { callback(); + } else { + nextFill(sequence + 1); } }); - } + })(sequence); }; this._loadSequence(sequenceLoaded); @@ -319,43 +328,6 @@ TransactionManager.prototype._request = function(tx) { if (tx.finalized) return; - tx.submitIndex = this._remote._ledger_current_index; - - if (tx.attempts === 0) { - tx.initialSubmitIndex = tx.submitIndex; - } - - if (!tx._setLastLedger) { - // Honor LastLedgerSequence set by user of API. If - // left unset by API, bump LastLedgerSequence - tx.tx_json.LastLedgerSequence = tx.submitIndex + 8; - } - - tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence; - - var submitRequest = remote.requestSubmit(); - - if (remote.local_signing) { - tx.sign(); - // TODO: We are serializing twice, when we could/should be feeding the - // tx_blob to `tx.hash()` which rebuilds it to sign it. - submitRequest.tx_blob(tx.serialize().to_hex()); - - // ND: ecdsa produces a random `TxnSignature` field value, a component of - // the hash. Attempting to identify a transaction via a hash synthesized - // locally while using remote signing is inherently flawed. - tx.addId(tx.hash()); - } else { - // ND: `build_path` is completely ignored when doing local signing as - // `Paths` is a component of the signed blob, the `tx_blob` is signed, - // sealed and delivered, and the txn unmodified. - // TODO: perhaps an exception should be raised if build_path is attempted - // while local signing - submitRequest.build_path(tx._build_path); - submitRequest.secret(tx._secret); - submitRequest.tx_json(tx.tx_json); - } - remote._trace('transactionmanager: submit:', tx.tx_json); function transactionProposed(message) { @@ -378,6 +350,7 @@ TransactionManager.prototype._request = function(tx) { function transactionRetry(message) { if (tx.finalized) return; + self._fillSequence(tx, function() { self._resubmit(1, tx); }); @@ -453,24 +426,48 @@ TransactionManager.prototype._request = function(tx) { } }; + var submitRequest = remote.requestSubmit(); + submitRequest.once('error', submitted); submitRequest.once('success', submitted); - if (tx._server) { - submitRequest.server = tx._server; - } + function prepareSubmit() { + if (remote.local_signing) { + // TODO: We are serializing twice, when we could/should be feeding the + // tx_blob to `tx.hash()` which rebuilds it to sign it. + submitRequest.tx_blob(tx.serialize().to_hex()); - if (typeof tx._iff !== 'function') { - submitTransaction(); - } else { - return tx._iff(tx.summary(), function(err, proceed) { + // ND: ecdsa produces a random `TxnSignature` field value, a component of + // the hash. Attempting to identify a transaction via a hash synthesized + // locally while using remote signing is inherently flawed. + tx.addId(tx.hash()); + } else { + // ND: `build_path` is completely ignored when doing local signing as + // `Paths` is a component of the signed blob, the `tx_blob` is signed, + // sealed and delivered, and the txn unmodified. + // TODO: perhaps an exception should be raised if build_path is attempted + // while local signing + submitRequest.build_path(tx._build_path); + submitRequest.secret(tx._secret); + submitRequest.tx_json(tx.tx_json); + } + + if (tx._server) { + submitRequest.server = tx._server; + } + + if (typeof tx._iff !== 'function') { + return submitTransaction(); + } + + tx._iff(tx.summary(), function(err, proceed) { if (err || !proceed) { tx.emit('abort'); } else { submitTransaction(); } }); - } + }; function requestTimeout() { // ND: What if the response is just slow and we get a response that @@ -503,6 +500,26 @@ TransactionManager.prototype._request = function(tx) { tx.emit('postsubmit'); }; + tx.submitIndex = this._remote._ledger_current_index; + + if (tx.attempts === 0) { + tx.initialSubmitIndex = tx.submitIndex; + } + + if (!tx._setLastLedger) { + // Honor LastLedgerSequence set by user of API. If + // left unset by API, bump LastLedgerSequence + tx.tx_json.LastLedgerSequence = tx.submitIndex + 8; + } + + tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence; + + if (remote.local_signing) { + tx.sign(prepareSubmit); + } else { + prepareSubmit(); + } + return submitRequest; }; @@ -580,27 +597,22 @@ TransactionManager.prototype.submit = function(tx) { tx.tx_json.Sequence = this._nextSequence++; } + // Attach secret, associate transaction with a server, attach fee. + // If the transaction can't complete, decrement sequence so that + // subsequent transactions + if (!tx.complete()) { + this._nextSequence--; + return; + } + tx.attempts = 0; - // Attach secret, associate transaction with a server, attach fee - tx.complete(); - - var fee = Number(tx.tx_json.Fee); - - if (!tx._secret && !tx.tx_json.TxnSignature) { - tx.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); - } else if (!remote.trusted && !remote.local_signing) { - tx.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server')); - } else if (fee && fee > this._maxFee) { - tx.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded')); - } else { - // ND: this is the ONLY place we put the tx into the queue. The - // TransactionQueue queue is merely a list, so any mutations to tx._hash - // will cause subsequent look ups (eg. inside 'transaction-outbound' - // validated transaction clearing) to fail. - this._pending.push(tx); - this._request(tx); - } + // ND: this is the ONLY place we put the tx into the queue. The + // TransactionQueue queue is merely a list, so any mutations to tx._hash + // will cause subsequent look ups (eg. inside 'transaction-outbound' + // validated transaction clearing) to fail. + this._pending.push(tx); + this._request(tx); }; exports.TransactionManager = TransactionManager; diff --git a/src/js/ripple/transactionqueue.js b/src/js/ripple/transactionqueue.js index 4b3758d3..b9968dd9 100644 --- a/src/js/ripple/transactionqueue.js +++ b/src/js/ripple/transactionqueue.js @@ -4,46 +4,17 @@ */ var Transaction = require('./transaction').Transaction; +var LRU = require('lru-cache'); function TransactionQueue() { var self = this; this._queue = [ ]; - this._idCache = { }; - this._sequenceCache = { }; + this._idCache = LRU({ max: 100 }); + this._sequenceCache = LRU({ max: 100 }); this._save = void(0); }; -TransactionQueue.prototype.clearCache = function() { - this._idCache = { }; - this._sequenceCache = { }; -}; - -TransactionQueue.prototype.getMinLedger = function() { - var minLedger = Infinity; - - for (var i=0; i 0) { + j >= 0) { // XXX Better, faster way to get BigInteger from JS int? this._value = new BigInteger(""+j); } diff --git a/src/js/ripple/utils.js b/src/js/ripple/utils.js index 4304f661..a678feab 100644 --- a/src/js/ripple/utils.js +++ b/src/js/ripple/utils.js @@ -135,7 +135,12 @@ function fromTimestamp(rpepoch) { rpepoch = rpepoch.getTime(); } - return Math.round(rpepoch/1000) - 0x386D4380; + return Math.round(rpepoch / 1000) - 0x386D4380; +}; + +exports.time = { + fromRipple: toTimestamp, + toRipple: fromTimestamp }; exports.trace = trace; diff --git a/src/js/sjcl-custom/sjcl-ecc-pointextras.js b/src/js/sjcl-custom/sjcl-ecc-pointextras.js new file mode 100644 index 00000000..057e9069 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecc-pointextras.js @@ -0,0 +1,83 @@ +/** + * Check that the point is valid based on the method described in + * SEC 1: Elliptic Curve Cryptography, section 3.2.2.1: + * Elliptic Curve Public Key Validation Primitive + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isValidPoint = function() { + + var self = this; + + var field_modulus = self.curve.field.modulus; + + if (self.isIdentity) { + return false; + } + + // Check that coordinatres are in bounds + // Return false if x < 1 or x > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.x)) && + !self.x.equals(1)) || + (self.x.greaterEquals(field_modulus.sub(1))) && + !self.x.equals(1)) { + + return false; + } + + // Return false if y < 1 or y > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.y)) && + !self.y.equals(1)) || + (self.y.greaterEquals(field_modulus.sub(1))) && + !self.y.equals(1)) { + + return false; + } + + if (!self.isOnCurve()) { + return false; + } + + // TODO check to make sure point is a scalar multiple of base_point + + return true; + +}; + +/** + * Check that the point is on the curve + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isOnCurve = function() { + + var self = this; + + var field_order = self.curve.r; + var component_a = self.curve.a; + var component_b = self.curve.b; + var field_modulus = self.curve.field.modulus; + + var left_hand_side = self.y.mul(self.y).mod(field_modulus); + var right_hand_side = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus); + + return left_hand_side.equals(right_hand_side); + +}; + + +sjcl.ecc.point.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + + ')'; +}; + +sjcl.ecc.pointJac.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + ', ' + + this.z.toString() + + ')'; +}; diff --git a/src/js/sjcl-custom/sjcl-ecdsa-canonical.js b/src/js/sjcl-custom/sjcl-ecdsa-canonical.js new file mode 100644 index 00000000..d56c5114 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecdsa-canonical.js @@ -0,0 +1,17 @@ +sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(); + + var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)); + + // For a canonical signature we want the lower of two possible values for s + // 0 < s <= n/2 + if (!R.copy().halveM().greaterEquals(s)) { + s = R.sub(s); + } + + return w.concat(r.toBits(l), s.toBits(l)); +}; + diff --git a/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js b/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js new file mode 100644 index 00000000..3365342c --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js @@ -0,0 +1,306 @@ +/** + * This module uses the public key recovery method + * described in SEC 1: Elliptic Curve Cryptography, + * section 4.1.6, "Public Key Recovery Operation". + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * Implementation based on: + * https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js + */ + +// Defined here so that this value only needs to be calculated once +var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR; + +/** + * Sign the given hash such that the public key, prepending an extra byte + * so that the public key will be recoverable from the signature + * + * @param {bitArray} hash + * @param {Number} paranoia + * @returns {bitArray} Signature formatted as bitArray + */ +sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) { + + var self = this; + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + // Sign hash with standard, canonicalized method + var standard_signature = self.sign(hash_bits, paranoia, k_for_testing); + var canonical_signature = self.canonicalizeSignature(standard_signature); + + // Extract r and s signature components from canonical signature + var r_and_s = getRandSFromSignature(self._curve, canonical_signature); + + // Rederive public key + var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get())); + + // Determine recovery factor based on which possible value + // returns the correct public key + var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key); + + // Prepend recovery_factor to signature and encode in DER + // The value_to_prepend should be 4 bytes total + var value_to_prepend = recovery_factor + 27; + + var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature); + + // Return value in bits + return final_signature_bits; + +}; + + +/** + * Recover the public key from a signature created with the + * signWithRecoverablePublicKey method in this module + * + * @static + * + * @param {bitArray} hash + * @param {bitArray} signature + * @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve + * @returns {sjcl.ecc.ecdsa.publicKey} Public key + */ +sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) { + + if (!signature || signature instanceof sjcl.ecc.curve) { + throw new sjcl.exception.invalid('must supply hash and signature to recover public key'); + } + + if (!curve) { + curve = sjcl.ecc.curves['c256']; + } + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + var signature_bits; + if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') { + signature_bits = signature; + } else { + throw new sjcl.exception.invalid('signature. Must be a bitArray'); + } + + // Extract recovery_factor from first 4 bytes + var recovery_factor = signature_bits[0] - 27; + + if (recovery_factor < 0 || recovery_factor > 3) { + throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' + + 'that prepends the recovery factor in order to recover the public key'); + } + + // Separate r and s values + var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1)); + var signature_r = r_and_s.r; + var signature_s = r_and_s.s; + + // Recover public key using recovery_factor + var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor); + var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point); + + return recovered_public_key; + +}; + + +/** + * Retrieve the r and s components of a signature + * + * @param {sjcl.ecc.curve} curve + * @param {bitArray} signature + * @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn + */ +function getRandSFromSignature(curve, signature) { + + var r_length = curve.r.bitLength(); + + return { + r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)), + s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature))) + }; +}; + + +/** + * Determine the recovery factor by trying all four + * possibilities and figuring out which results in the + * correct public key + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {sjcl.ecc.point} original_public_key_point + * @returns {Number, 0-3} Recovery factor + */ +function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) { + + var original_public_key_point_bits = original_public_key_point.toBits(); + + // TODO: verify that it is possible for the recovery_factor to be 2 or 3, + // we may only need 1 bit because the canonical signature might remove the + // possibility of us needing to "use the second candidate key" + for (var possible_factor = 0; possible_factor < 4; possible_factor++) { + + var resulting_public_key_point; + try { + resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor); + } catch (err) { + // console.log(err, err.stack); + continue; + } + + if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) { + return possible_factor; + } + + } + + throw new sjcl.exception.bug('unable to calculate recovery factor from signature'); + +}; + + +/** + * Recover the public key from the signature. + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {Number, 0-3} recovery_factor + * @returns {sjcl.point} Public key corresponding to signature + */ +function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) { + + var field_order = curve.r; + var field_modulus = curve.field.modulus; + + // Reduce the recovery_factor to the two bits used + recovery_factor = recovery_factor & 3; + + // The less significant bit specifies whether the y coordinate + // of the compressed point is even or not. + var compressed_point_y_coord_is_even = recovery_factor & 1; + + // The more significant bit specifies whether we should use the + // first or second candidate key. + var use_second_candidate_key = recovery_factor >> 1; + + // Calculate (field_order + 1) / 4 + if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) { + FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4); + } + + // In the paper they write "1. For j from 0 to h do the following..." + // That is not necessary here because we are given the recovery_factor + // step 1.1 Let x = r + jn + // Here "j" is either 0 or 1 + var x; + if (use_second_candidate_key) { + x = signature_r.add(field_order); + } else { + x = signature_r; + } + + // step 1.2 and 1.3 convert x to an elliptic curve point + // Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion + var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus); + var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus); + + // If beta is even but y isn't or + // if beta is odd and y is even + // then subtract beta from the field_modulus + var y; + var beta_is_even = beta.mod(2).equals(0); + if (beta_is_even && !compressed_point_y_coord_is_even || + !beta_is_even && compressed_point_y_coord_is_even) { + y = beta; + } else { + y = field_modulus.sub(beta); + } + + // generated_point_R is the point generated from x and y + var generated_point_R = new sjcl.ecc.point(curve, x, y); + + // step 1.4 check that R is valid and R x field_order !== infinity + // TODO: add check for R x field_order === infinity + if (!generated_point_R.isValidPoint()) { + throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key'); + } + + // step 1.5 Compute e from M + var message_e = sjcl.bn.fromBits(hash_bits); + var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order); + + // step 1.6 Compute Q = r^-1 (sR - eG) + // console.log('r: ', signature_r); + var signature_r_inv = signature_r.inverseMod(field_order); + var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv); + + // Validate public key point + if (!public_key_point.isValidPoint()) { + throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key'); + } + + // Verify that this public key matches the signature + if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) { + throw new sjcl.exception.corrupt('cannot recover public key'); + } + + return public_key_point; + +}; + + +/** + * Verify a signature given the raw components + * using method defined in section 4.1.5: + * "Alternative Verifying Operation" + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} e + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {sjcl.ecc.point} public_key_point + * @returns {Boolean} + */ +function verify_raw(curve, e, r, s, public_key_point) { + + var field_order = curve.r; + + // Return false if r is out of bounds + if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Return false if s is out of bounds + if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Check that r = (u1 + u2)G + // u1 = e x s^-1 (mod field_order) + // u2 = r x s^-1 (mod field_order) + var s_mod_inverse_field_order = s.inverseMod(field_order); + var u1 = e.mul(s_mod_inverse_field_order).mod(field_order); + var u2 = r.mul(s_mod_inverse_field_order).mod(field_order); + + var point_computed = curve.G.mult2(u1, u2, public_key_point); + + return r.equals(point_computed.x.mod(field_order)); + +}; + diff --git a/src/js/sjcl-custom/sjcl-secp256k1.js b/src/js/sjcl-custom/sjcl-secp256k1.js index d87bcc6f..a17eccf0 100755 --- a/src/js/sjcl-custom/sjcl-secp256k1.js +++ b/src/js/sjcl-custom/sjcl-secp256k1.js @@ -62,7 +62,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () { var f = e.square(); var x = f.sub(d.copy().doubleM()); var y = e.mul(d.sub(x)).subM(c.doubleM().doubleM().doubleM()); - var z = this.y.mul(this.z).doubleM(); + var z = this.z.mul(this.y).doubleM(); return new sjcl.ecc.pointJac(this.curve, x, y, z); }; diff --git a/src/js/sjcl-custom/sjcl-validecc.js b/src/js/sjcl-custom/sjcl-validecc.js index 7d24fd3b..f894dd65 100644 --- a/src/js/sjcl-custom/sjcl-validecc.js +++ b/src/js/sjcl-custom/sjcl-validecc.js @@ -1,30 +1,38 @@ -sjcl.ecc.ecdsa.secretKey.prototype = { - sign: function(hash, paranoia) { - var R = this._curve.r, - l = R.bitLength(), - k = sjcl.bn.random(R.sub(1), paranoia).add(1), - r = this._curve.G.mult(k).x.mod(R), - s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); +sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) { + var R = this._curve.r, + l = R.bitLength(); - return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); + // k_for_testing should ONLY BE SPECIFIED FOR TESTING + // specifying it will make the signature INSECURE + var k; + if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') { + k = k_for_testing; + } else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) { + k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing)); + } else { + // This is the only option that should be used in production + k = sjcl.bn.random(R.sub(1), paranoia).add(1); } + + var r = this._curve.G.mult(k).x.mod(R); + var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); + + return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); }; -sjcl.ecc.ecdsa.publicKey.prototype = { - verify: function(hash, rs) { - var w = sjcl.bitArray, - R = this._curve.r, - l = R.bitLength(), - r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), - s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), - sInv = s.inverseMod(R), - hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), - hA = r.mul(sInv).mod(R), - r2 = this._curve.G.mult2(hG, hA, this._point).x; +sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(), + r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), + sInv = s.inverseMod(R), + hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), + hA = r.mul(sInv).mod(R), + r2 = this._curve.G.mult2(hG, hA, this._point).x; - if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { - throw (new sjcl.exception.corrupt("signature didn't check out")); - } - return true; + if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { + throw (new sjcl.exception.corrupt("signature didn't check out")); } + return true; }; diff --git a/src/js/sjcl/core/ecc.js b/src/js/sjcl/core/ecc.js index b36e7221..9a5a9b88 100644 --- a/src/js/sjcl/core/ecc.js +++ b/src/js/sjcl/core/ecc.js @@ -11,8 +11,16 @@ sjcl.ecc.point = function(curve,x,y) { if (x === undefined) { this.isIdentity = true; } else { + if (x instanceof sjcl.bn) { + x = new curve.field(x); + } + if (y instanceof sjcl.bn) { + y = new curve.field(y); + } + this.x = x; this.y = y; + this.isIdentity = false; } this.curve = curve; diff --git a/test/account-test.js b/test/account-test.js new file mode 100644 index 00000000..78e4fb48 --- /dev/null +++ b/test/account-test.js @@ -0,0 +1,181 @@ +var assert = require('assert'); +var Account = require('../src/js/ripple/account').Account; + +describe('Account', function(){ + + describe('._publicKeyToAddress()', function(){ + + it('should throw an error if the key is invalid', function(){ + try { + Account._publicKeyToAddress('not a real key'); + } catch (e) { + assert(e); + } + }); + + it('should return unchanged a valid UINT160', function(){ + assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz')); + }); + + it('should parse a hex-encoded public key as a UINT160', function(){ + assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332')); + + assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === Account._publicKeyToAddress('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23')); + + assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === Account._publicKeyToAddress('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7')); + + assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === Account._publicKeyToAddress('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F')); + }); + + }); + + describe('.publicKeyIsActive()', function(){ + + it('should respond true if the public key corresponds to the account address and the master key IS NOT disabled', function(){ + + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: 65536, + LedgerEntryType: 'AccountRoot' + }}); + } + } + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ + assert(err === null); + assert(is_valid === true); + }); + + }); + + it('should respond false if the public key corresponds to the account address and the master key IS disabled', function(){ + + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot' + }}); + } + } + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ + assert(err === null); + assert(is_valid === false); + }); + + }); + + it('should respond true if the public key corresponds to the regular key', function(){ + + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot', + RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r' + }}); + } + } + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('02BE53B7ACBB0900E0BB7729C9CAC1033A0137993B17800BD1191BBD1B29D96A8C', function(err, is_valid){ + assert(err === null); + assert(is_valid === true); + }); + + }); + + it('should respond false if the public key does not correspond to an active public key for the account', function(){ + + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot', + RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r' + }}); + } + } + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){ + assert(err === null); + assert(is_valid === false); + }); + + }); + + it('should respond false if the public key is invalid', function(){ + + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot', + RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r' + }}); + } + } + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('not a real public key', function(err, is_valid){ + assert(err); + }); + + }); + + it('should assume the master key is valid for unfunded accounts', function(){ + + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { + if (address === 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ') { + callback({ error: 'remoteError', + error_message: 'Remote reported an error.', + remote: + { account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ', + error: 'actNotFound', + error_code: 15, + error_message: 'Account not found.', + id: 3, + ledger_current_index: 6391106, + request: + { account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ', + command: 'account_info', + id: 3, + ident: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' }, + status: 'error', + type: 'response' }, + result: 'remoteError', + engine_result: 'remoteError', + result_message: 'Remote reported an error.', + engine_result_message: 'Remote reported an error.', + message: 'Remote reported an error.' + }); + } + } + }, 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ'); + account.publicKeyIsActive('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){ + assert(!err); + assert(is_valid); + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/test/amount-test.js b/test/amount-test.js index 51c40282..0630470c 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -431,4 +431,130 @@ describe('Amount', function() { assert.strictEqual(a.not_equals_why(b), 'Native mismatch.'); }); }); + + describe('product_human', function() { + it('Multiply 0 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 1 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 0 XRP with 1 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 USD with 1 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 XRP with 1 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.equal('0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('0.2/XRP', Amount.from_json('20000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('20/XRP', Amount.from_json('2000000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg', function () { + assert.strictEqual('-0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('-10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg, frac', function () { + assert.strictEqual('-0.222/XRP', Amount.from_json('-6000').product_human(Amount.from_json('37/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('20000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('200000000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('100000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, result < 1', function () { + assert.strictEqual('100000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, neg', function () { + assert.strictEqual(Amount.from_json('-24000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-48000000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with USD, neg, <1', function () { + assert.strictEqual(Amount.from_json('0.1/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('-1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, factor < 1', function () { + assert.strictEqual(Amount.from_json('0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg', function () { + assert.strictEqual(Amount.from_json('-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('5')).to_text_full(), '-0.0005/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg, <1', function () { + assert.strictEqual(Amount.from_json('-0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '-0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply XRP with XRP', function () { + assert.strictEqual(Amount.from_json('10000000').product_human(Amount.from_json('10')).to_text_full(), '0.0001/XRP'); + }); + it('Multiply USD with XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '19900.00316303882/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('ratio_human', function() { + it('Divide USD by XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('_invert', function() { + it('Invert 1', function () { + assert.strictEqual(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Invert 20', function () { + assert.strictEqual(Amount.from_json('20/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '0.05/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Invert 0.02', function () { + assert.strictEqual(Amount.from_json('0.02/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '50/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('from_quality', function() { + it('BTC/XRP', function () { + assert.strictEqual(Amount.from_quality('7B73A610A009249B0CC0D4311E8BA7927B5A34D86634581C5F0FF9FF678E1000', 'XRP', NaN, {base_currency: 'BTC'}).to_text_full(), '44,970/XRP'); + }); + it('BTC/XRP inverse', function () { + assert.strictEqual(Amount.from_quality('37AAC93D336021AE94310D0430FFA090F7137C97D473488C4A0918D0DEF8624E', 'XRP', NaN, {inverse: true, base_currency: 'BTC'}).to_text_full(), '39,053.954453/XRP'); + }); + it('XRP/USD', function () { + assert.strictEqual(Amount.from_quality('DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4D05DCAA8FE12000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'XRP'}).to_text_full(), '0.0165/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('XRP/USD inverse', function () { + assert.strictEqual(Amount.from_quality('4627DFFCFF8B5A265EDBD8AE8C14A52325DBFEDAF4F5C32E5C22A840E27DCA9B', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'XRP'}).to_text_full(), '0.010251/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('BTC/USD', function () { + assert.strictEqual(Amount.from_quality('6EAB7C172DEFA430DBFAD120FDC373B5F5AF8B191649EC9858038D7EA4C68000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'BTC'}).to_text_full(), '1000/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('BTC/USD inverse', function () { + assert.strictEqual(Amount.from_quality('20294C923E80A51B487EB9547B3835FD483748B170D2D0A455071AFD498D0000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'BTC'}).to_text_full(), '0.5/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('XAU(dem)/XRP', function () { + assert.strictEqual(Amount.from_quality('587322CCBDE0ABD01704769A73A077C32FB39057D813D4165F1FF973CAF997EF', 'XRP', NaN, {base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,452.246928/XRP'); + }); + it('XAU(dem)/XRP inverse', function () { + assert.strictEqual(Amount.from_quality('F72C7A9EAE4A45ED1FB547AD037D07B9B965C6E662BEBAFA4A03F2A976804235', 'XRP', NaN, {inverse: true, base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,442.196677/XRP'); + }); + it('USD/XAU(dem)', function () { + assert.strictEqual(Amount.from_quality('4743E58E44974B325D42FD2BB683A6E36950F350EE46DD3A521B644B99782F5F', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007710100231303007/015841551A748AD2C1F76FF6ECB0CCCD00000000/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN'); + }); + it('USD/XAU(dem) inverse', function () { + assert.strictEqual(Amount.from_quality('CDFD3AFB2F8C5DBEF75B081F7C957FF5509563266F28F36C5704A0FB0BAD8800', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {inverse: true, base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007675186123263489/015841551A748AD2C1F76FF6ECB0CCCD00000000/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN'); + }); + }); }); diff --git a/test/message-test.js b/test/message-test.js new file mode 100644 index 00000000..c6e8d29d --- /dev/null +++ b/test/message-test.js @@ -0,0 +1,327 @@ +var assert = require('assert'); +var sjcl = require('../build/sjcl'); +var Message = require('../src/js/ripple/message'); +var Seed = require('../src/js/ripple/seed').Seed; +var Remote = require('../src/js/ripple/remote').Remote; + +describe('Message', function(){ + + describe('signMessage', function(){ + + it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call signHash', function(){ + + var normal_signHash = Message.signHash; + + var message_text = 'Hello World!'; + + var signHash_called = false; + Message.signHash = function(hash) { + signHash_called = true; + assert.deepEqual(hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + message_text)); + }; + + Message.signMessage(message_text); + assert(signHash_called); + + Message.signHash = normal_signHash; + + }); + + }); + + describe('signHash', function(){ + + it('should accept the hash as either a hex string or a bitArray', function(){ + + var normal_random = sjcl.random.randomWords; + + sjcl.random.randomWords = function(num_words){ + var words = []; + for (var w = 0; w < num_words; w++) { + words.push(sjcl.codec.hex.toBits('00000000')); + } + return words; + }; + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + // var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + var signature1 = Message.signHash(hash, secret_string); + var signature2 = Message.signHash(sjcl.codec.hex.toBits(hash), secret_string); + + assert.strictEqual(signature1, signature2); + + sjcl.random.randomWords = normal_random; + + }); + + it('should accept the secret as a string or scjl.ecc.ecdsa.secretKey object', function(){ + + var normal_random = sjcl.random.randomWords; + + sjcl.random.randomWords = function(num_words){ + var words = []; + for (var w = 0; w < num_words; w++) { + words.push(sjcl.codec.hex.toBits('00000000')); + } + return words; + }; + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + // var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + var signature1 = Message.signHash(hash, secret_string); + var signature2 = Message.signHash(hash, Seed.from_json(secret_string).get_key()._secret); + + assert.strictEqual(signature1, signature2); + + sjcl.random.randomWords = normal_random; + + }); + + it('should throw an error if given an invalid secret key', function(){ + + var secret_string = 'badsafRpB5euNL52PZPTSqrE9gvuFwTC'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + assert.throws(function(){ + Message.signHash(hash, secret_string); + }, /Cannot\ generate\ keys\ from\ invalid\ seed/); + + }); + + it('should throw an error if the parameters are reversed', function(){ + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + assert.throws(function(){ + Message.signHash(secret_string, hash); + }, Error); + + assert.throws(function(){ + Message.signHash(secret_string, sjcl.codec.hex.toBits(hash)); + }, Error); + + assert.throws(function(){ + Message.signHash(Seed.from_json(secret_string).get_key()._secret, hash); + }, Error); + + assert.throws(function(){ + Message.signHash(Seed.from_json(secret_string).get_key()._secret, sjcl.codec.hex.toBits(hash)); + }, Error); + + }); + + it('should produce a base64-encoded signature', function(){ + var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/; + + var normal_random = sjcl.random.randomWords; + + sjcl.random.randomWords = function(num_words){ + var words = []; + for (var w = 0; w < num_words; w++) { + words.push(sjcl.codec.hex.toBits('00000000')); + } + return words; + }; + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + // var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + var signature = Message.signHash(hash, secret_string); + + assert(REGEX_BASE64.test(signature)); + + sjcl.random.randomWords = normal_random; + }); + + }); + + describe('verifyMessageSignature', function(){ + + it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call verifyHashSignature', function(){ + + var normal_verifyHashSignature = Message.verifyHashSignature; + + var data = { + message: 'Hello world!', + signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I=' + }; + + var verifyHashSignature_called = false; + Message.verifyHashSignature = function(vhs_data, remote, callback) { + verifyHashSignature_called = true; + + assert.deepEqual(vhs_data.hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message)); + assert.strictEqual(vhs_data.signature, data.signature); + callback(); + + }; + + Message.verifyMessageSignature(data, {}, function(err){ + assert(!err); + }); + assert(verifyHashSignature_called); + + Message.verifyHashSignature = normal_verifyHashSignature; + + }); + + }); + + describe('verifyHashSignature', function(){ + + it('should throw an error if a callback function is not supplied', function(){ + + var data = { + message: 'Hello world!', + hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8', + signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + + assert.throws(function(){ + Message.verifyHashSignature(data); + }, /(?=.*callback\ function).*/); + + + }); + + it('should respond with an error if the hash is missing or invalid', function(done){ + + var data = { + message: 'Hello world!', + signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(/hash/i.test(err.message)); + done(); + }); + + }); + + it('should respond with an error if the account is missing or invalid', function(done){ + + var data = { + message: 'Hello world!', + hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8', + signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(/account|address/i.test(err.message)); + done(); + }); + + }); + + it('should respond with an error if the signature is missing or invalid', function(done){ + + var data = { + message: 'Hello world!', + hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(/signature/i.test(err.message)); + done(); + }); + + }); + + it('should respond true if the signature is valid and corresponds to an active public key for the account', function(done){ + + var data = { + message: 'Hello world!', + hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs=' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + test_remote.request_account_info = function(account, callback) { + if (account === data.account) { + callback(null, { + "account_data": { + "Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz", + "Flags": 1114112, + "LedgerEntryType": "AccountRoot", + "RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf" + } + }); + } else { + callback(new Error('wrong account')); + } + }; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(!err); + assert(valid); + done(); + }); + + }); + + it('should respond false if a key can be recovered from the signature but it does not correspond to an active public key', function(done){ + + // Signature created by disabled master key + var data = { + message: 'Hello world!', + hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk=' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + test_remote.request_account_info = function(account, callback) { + if (account === data.account) { + callback(null, { + "account_data": { + "Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz", + "Flags": 1114112, + "LedgerEntryType": "AccountRoot", + "RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf" + } + }); + } else { + callback(new Error('wrong account')); + } + }; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(!err); + assert(!valid); + done(); + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/test/sjcl-ecdsa-canonical-test.js b/test/sjcl-ecdsa-canonical-test.js new file mode 100644 index 00000000..d0a270b7 --- /dev/null +++ b/test/sjcl-ecdsa-canonical-test.js @@ -0,0 +1,28 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var sjcl = require('../build/sjcl'); +var Seed = require('../src/js/ripple/seed').Seed; + +describe('SJCL ECDSA Canonicalization', function() { + describe('canonicalizeSignature', function() { + it('should canonicalize non-canonical signatures', function () { + var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk'); + var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ'); + + var rs = sjcl.codec.hex.toBits("27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0fb0a0747ecc7b4ca46e45b3b32b6b2a066aa0249c027ef11e5bce93dab756549c"); + rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs); + assert.strictEqual(sjcl.codec.hex.fromBits(rs), "27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0f4f5f8b813384b35b91ba4c4cd494d5f8500eb84aacc9af1d6403cab218dfeca5"); + }); + + it('should not touch canonical signatures', function () { + var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk'); + var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ'); + + var rs = sjcl.codec.hex.toBits("5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c"); + rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs); + assert.strictEqual(sjcl.codec.hex.fromBits(rs), "5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c"); + }); + }); +}); + +// vim:sw=2:sts=2:ts=8:et diff --git a/test/sjcl-ecdsa-recoverablepublickey-test.js b/test/sjcl-ecdsa-recoverablepublickey-test.js new file mode 100644 index 00000000..03590d16 --- /dev/null +++ b/test/sjcl-ecdsa-recoverablepublickey-test.js @@ -0,0 +1,245 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var sjcl = require('../build/sjcl'); + +describe('ECDSA signing with recoverable public key', function(){ + + describe('Sign and recover public key from signature', function(){ + + it('should recover public keys from signatures it generates', function(){ + + var messages = [{ + message: 'Hello world!', + secret_hex: '9931c08f61f127d5735fa3c60e702212ce7ed9a2ac90d5dbade99c689728cd9b', + random_value: '5473a3dbdc13ec9efbad7f7f929fbbea404af556a48041dd9d41d29fdbc989ad', + hash_function: sjcl.hash.sha512.hash + // signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I=' + }, { + // Correct recovery value for this one is 0 + message: 'ua5pdcG0I1JuhSr9Fwai2UoZ9ll5leUtHE5NzSSNnPkw8nSPH5mT1gE1fe0sn', + secret_hex: '84814318ffe6e612694ad59b9084b7b66d68b6979567c619171a67b05e2b654b', + random_value: '14261d30b319709c10ab13cabe595313b99dd2d5c76b8b38d7eb445f0b81cc9a', + hash_function: sjcl.hash.sha512.hash + // signature: 'AAAAHGjpBM7wnTHbPGo0TXsxKbr+d7KvACuJ/eGQsp3ZJfOOQHszaciRo3ClenwKixcquFcBlaVfHlOc3JWOZq1RjpQ=' + }, { + // Correct recovery value for this one is 1 + message: 'rxc76UnmVTp', + secret_hex: '37eac47c212be8ea8372f506b11673c281cd9ea29a035c2c9e90d027c3dbecc6', + random_value: '61b53ca6de0543f911765ae216a3a4d851918a0733fba9ac80cf29de5bec8032', + hash_function: sjcl.hash.sha256.hash + // signature: 'AAAAG8L/yOA3nNqK4aOiQWJmOaWvkvr3NoTk6wCdX97U3qowdgFd98UK3evWV16qO3RHgFMEnUW/Vt4+kcidqW6hMo0=' + }]; + + var curve = sjcl.ecc.curves['c256']; + + for (var m = 0; m < messages.length; m++) { + + var message = messages[m].message; + var secret_hex = messages[m].secret_hex; + var random_value = messages[m].random_value; + var hash_function = messages[m].hash_function; + + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + + var pub_val_point = secret_key._curve.G.mult(secret_key._exponent); + var public_key = new sjcl.ecc.ecdsa.publicKey(curve, pub_val_point); + var hash = hash_function(message); + + var recoverable_signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, recoverable_signature); + + assert.deepEqual(public_key.get().x, recovered_public_key.get().x, 'The x value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().x.toString() + '. Actual: ' + recovered_public_key.get().x.toString()); + assert.deepEqual(public_key.get().y, recovered_public_key.get().y, 'The y value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().y.toString() + '. Actual: ' + recovered_public_key.get().y.toString()); + + } + + }); + + }); + + describe('signWithRecoverablePublicKey', function(){ + + // it('should produce the same values as bitcoinjs-lib\'s implementation', function(){ + + // // TODO: figure out why bitcoinjs-lib and this produce different signature values + + // var curve = sjcl.ecc.curves['c256']; + + // var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + // var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + // var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + + // // var public_key = '0217b9f5b3ba8d550f19fdfb5233818cd27d19aaea029b667f547f5918c307ed3b'; + // var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + // var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + // var bitcoin_signature_base64 = 'IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM='; + + // var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + // var signature_base64 = sjcl.codec.base64.fromBits(signature); + + // assert.equal(signature_base64, bitcoin_signature_base64); + + // }); + + it('should produce an error if the hash is not given as a bitArray', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + assert.throws(function(){ + secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + }, /(?=.*hash)(?=.*bitArray).+/); + + }); + + it('should return a bitArray', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + assert(typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number'); + + }); + + it('should return a bitArray where the first word contains the recovery factor', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + var recovery_factor = signature[0] - 27; + + assert(recovery_factor >= 0 && recovery_factor < 4); + + }); + + }); + + describe('recoverFromSignature', function(){ + + // it('should be able to recover public keys from bitcoinjs-lib\'s implementation', function(){ + + // // TODO: figure out why bitcoinjs-lib and this produce different signature values + + // var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + // var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM='); + + // var public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + + // }); + + it('should produce an error if the signature given does not have the recovery factor prefix', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.sign(hash, 0, random_value); + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + }, /(?=.*signature)(?=.*recovery factor)(?=.*public key).*/); + + }); + + it('should produce an error if it is not given both the hash and the signature', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash); + }, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/); + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(signature); + }, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/); + + }); + + it('should produce an error if it cannot generate a valid public key from the the signature', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM='); + signature[0] = 27; + + signature[3] = 0 - signature[3]; + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + }, /(?=.*Cannot\ recover\ public\ key).*/); + + }); + + it('should return a publicKey object', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + + var key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + + assert(key instanceof sjcl.ecc.ecdsa.publicKey); + + }); + + it('tampering with the signature should produce a different public key, if it produces a valid one at all', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + + signature[3]++; + + var original_public_key = new sjcl.ecc.ecdsa.publicKey(curve, curve.G.mult(secret_key._exponent)); + var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + + assert.notDeepEqual(original_public_key.get().x, recovered_public_key.get().x); + assert.notDeepEqual(original_public_key.get().y, recovered_public_key.get().y); + + }); + + }); + +}); + + diff --git a/test/uint-test.js b/test/uint-test.js new file mode 100644 index 00000000..9075f51e --- /dev/null +++ b/test/uint-test.js @@ -0,0 +1,25 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var UInt128 = utils.load_module('uint128').UInt128; +var config = require('./testutils').get_config(); + +describe('UInt', function() { + describe('128', function() { + describe('#parse_number', function () { + it('should create 00000000000000000000000000000000 when called with 0', function () { + var val = UInt128.from_number(0); + assert.strictEqual(val.to_hex(), '00000000000000000000000000000000'); + }); + it('should create 00000000000000000000000000000001 when called with 1', function () { + var val = UInt128.from_number(0); + assert.strictEqual(val.to_hex(), '00000000000000000000000000000000'); + }); + it('should create 000000000000000000000000FFFFFFFF when called with 0xFFFFFFFF', function () { + var val = UInt128.from_number(0xFFFFFFFF); + assert.strictEqual(val.to_hex(), '000000000000000000000000FFFFFFFF'); + }); + }); + }); +}); + +// vim:sw=2:sts=2:ts=8:et