Compare commits

...

47 Commits

Author SHA1 Message Date
Stefan Thomas
e4f9be5af8 Merge branch 'develop' 2014-05-02 14:10:09 -07:00
Stefan Thomas
473d8a8d8c [CHORE] Bump version to 0.7.36 2014-05-02 14:06:53 -07:00
Stefan Thomas
41ea820ae0 [FEATURE] Transaction: Allow canonical signing to be disabled via config. 2014-05-02 14:06:53 -07:00
wltsmrz
0558ad689a Async transaction sign 2014-05-02 12:45:26 -07:00
wltsmrz
3199aa438a Allow remote signing - broken options 2014-05-02 12:41:52 -07:00
Stefan Thomas
c3f630c27f Merge pull request #72 from emschwartz/message-signing
[FEATURE] New Message class for sigs on arbitrary data
2014-05-02 10:59:25 -07:00
Evan Schwartz
cf3a21a712 [CHORE] Merged PubKeyValidator into Account class 2014-05-01 20:28:55 -07:00
Evan Schwartz
d8504a3001 [CHORE] Changed variable name to make Stefan happier 2014-05-01 19:37:59 -07:00
Evan Schwartz
a2b07d5edd [FIX] Handling public key validation for unfunded accounts 2014-05-01 17:22:20 -07:00
Evan Schwartz
13a6a2c335 [CHORE] Added pre-built sjcl with additional functions included 2014-05-01 16:29:23 -07:00
Evan Schwartz
e19be192bd [FIX] Point coordinates should be converted to psuedo mersenne primes 2014-05-01 13:14:55 -07:00
Evan Schwartz
c32216c9e5 [CHORE] Added account param to signing functions 2014-05-01 12:08:14 -07:00
Evan Schwartz
904082a86c [FEATURE] New Message class for sigs on arbitrary data
This includes supporting files that can sign arbitrary data
with a signature that enables public key recovery. It also
includes the PublicKeyValidator class that can verify whether
a given public key is active for an account by looking in its
AccountRoot.
2014-04-30 18:41:18 -07:00
Evan Schwartz
f56a20d697 [FIX] Replaced Account.is_valid() with Account.isValid() 2014-04-30 18:41:18 -07:00
Evan Schwartz
8275e036c9 [CHORE] Modified functions not to overwrite entire prototype 2014-04-30 18:38:36 -07:00
wltsmrz
903e480130 Fix for missing transaction.remote 2014-04-25 01:38:01 -07:00
wltsmrz
30fd0e7bff Finalize before setting final transaction state 2014-04-25 00:08:26 -07:00
Stefan Thomas
fbdef6eea0 [BUG] UInt#parse_number should support zero. Add tests. 2014-04-24 09:01:18 -07:00
wltsmrz
5a04ce9629 Use binary sequence increment 2014-04-23 14:42:29 -07:00
wltsmrz
693e2aaae7 Decrement transaction sequence if transaction.complete fails 2014-04-23 14:39:30 -07:00
wltsmrz
43deeaf5fb Remove logging 2014-04-23 14:20:41 -07:00
wltsmrz
cbba7727f2 Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-04-23 14:19:19 -07:00
wltsmrz
52e1665e72 Fixes for per-transaction defined secret and offline errors 2014-04-23 14:19:02 -07:00
sublimator
66ea770287 Fix README
1000th
2014-04-23 18:23:25 +07:00
wltsmrz
18efa5d742 Emit an error on invalid secret, cleanup 2014-04-23 03:56:36 -07:00
wltsmrz
802212bbdc Merge pull request #71 from emschwartz/develop
[FEATURE] Added setRegularKey transaction and more accountSet flags
2014-04-22 19:06:42 -07:00
Evan Schwartz
7f59fb917c [FEATURE] Added setRegularKey transaction and more accountSet flags 2014-04-22 14:57:13 -07:00
wltsmrz
6ebaec31a5 Merge pull request #67 from lid/patch-1
Update options list and function names
2014-04-18 23:11:50 -07:00
wltsmrz
14f409ff56 Properly convert JS time to Ripple time in OfferCreate transactions 2014-04-18 23:08:19 -07:00
wltsmrz
8ffd0b13a3 Cleanup 2014-04-17 15:19:08 -07:00
wltsmrz
969873441e Recognize account option as equivalent to source in transaction construction 2014-04-15 12:47:21 -07:00
wltsmrz
282ac6d8ab Fix transaction constructor 2014-04-15 12:38:57 -07:00
wltsmrz
1e3c96b14f Fix transaction finalize 2014-04-15 12:27:33 -07:00
wltsmrz
b14fab8aa7 Check remote exists in transaction.complete 2014-04-10 23:29:16 -07:00
Stefan Thomas
be33b1be60 [BUG] Don't set canonical flag when remote signing. 2014-04-10 08:16:03 -07:00
wltsmrz
06288e798e Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-03-25 17:23:31 -07:00
wltsmrz
0de7d84862 Use LRU cache API to prevent multiple transaction events for the same transaction 2014-03-25 17:23:28 -07:00
Stefan Thomas
58afce517a [DOCS] Add npm badge to README. 2014-03-24 04:47:37 +01:00
lid
250e987fd9 Update options list and function names
Functions changed from snake_case to lowerCamelCase
2014-03-23 20:44:31 -04:00
Stefan Thomas
87ba2abc9a [BUG] Undo previous commit making append_byte_array too loose.
This commit introduces an alternative way of setting the canonical signature
flag, without compromising the strictness of append_byte_array input
sanitizing.
2014-03-22 03:18:11 -07:00
Stefan Thomas
716fd0b938 [CHORE] Improved Amount#parse_quality w/ demurrage support, drops->XRP, etc.
Amount#parse_quality is made currency-aware. This allows it to adjust for XRP as
the base currency, as well as for interest-bearing or demurring base currencies.
2014-03-22 02:38:03 -07:00
Stefan Thomas
893fc4c168 [CHORE] Add Amount#invert mathematical utility function. 2014-03-22 00:47:41 -07:00
Stefan Thomas
6f5cf8506f [CHORE] Better variable names in Amount#parse_quality. 2014-03-21 23:56:13 -07:00
Stefan Thomas
c808cb0a1c [CHORE] Add ability to apply demurrage at the time of product/ratio calculation. 2014-03-21 18:43:22 -07:00
Stefan Thomas
5f677a86a7 [CHORE] Update SJCL. 2014-03-20 17:38:46 -07:00
Stefan Thomas
11540f8cd9 [CHORE] Allow integer strings for server "port" setting. 2014-03-20 17:38:46 -07:00
Stefan Thomas
9d6ccdcab1 [CHORE] Enable signature canonicalization. 2014-03-20 17:38:46 -07:00
31 changed files with 2726 additions and 370 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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);

View File

@@ -52,7 +52,7 @@ A new `Remote` can be created with the following options:
, secure: <boolean>
}
```
+ `local_signing`
#2. `Remote` functions
@@ -60,7 +60,7 @@ A new `Remote` can be created with the following options:
##Server info functions
**[request_server_info([callback])](https://ripple.com/wiki/RPC_API#server_info)**
**[requestServerInfo([callback])](https://ripple.com/wiki/RPC_API#server_info)**
Returns information about the state of the server. If you are connected to multiple servers and want to select by a particular host, use `request.set_server`. Example:
@@ -73,34 +73,34 @@ request.callback(function(err, res) {
request.request();
```
**[request_unl_list([callback])](https://ripple.com/wiki/RPC_API#unl_list)**
**[requestUnlList([callback])](https://ripple.com/wiki/RPC_API#unl_list)**
**[request_unl_add(addr, comment, [callback])](https://ripple.com/wiki/RPC_API#unl_add)**
**[requestUnlAdd(addr, comment, [callback])](https://ripple.com/wiki/RPC_API#unl_add)**
**[request_unl_delete(node, [callback])](https://ripple.com/wiki/RPC_API#unl_delete)**
**[requestUnlDelete(node, [callback])](https://ripple.com/wiki/RPC_API#unl_delete)**
**[request_peers([callback])](https://ripple.com/wiki/RPC_API#peers)**
**[requestPeers([callback])](https://ripple.com/wiki/RPC_API#peers)**
**[request_connect(ip, port, [callback])](https://ripple.com/wiki/RPC_API#connect)**
**[requestConnect(ip, port, [callback])](https://ripple.com/wiki/RPC_API#connect)**
##Ledger query functions
**[request_ledger(ledger, [opts], [callback])](https://ripple.com/wiki/RPC_API#ledger)**
**[requestLedger(ledger, [opts], [callback])](https://ripple.com/wiki/RPC_API#ledger)**
**request_ledger_header([callback])**
**requestLedgerHeader([callback])**
**[request_ledger_current([callback])](https://ripple.com/wiki/RPC_API#ledger_current)**
**[requestLedgerCurrent([callback])](https://ripple.com/wiki/RPC_API#ledger_current)**
**[request_ledger_entry(type, [callback])](https://ripple.com/wiki/RPC_API#ledger_entry)**
**[requestLedgerEntry(type, [callback])](https://ripple.com/wiki/RPC_API#ledger_entry)**
**[request_subscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#subscribe)**
**[requestSubscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#subscribe)**
Start receiving selected streams from the server.
**[request_unsubscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#unsubscribe)**
**[requestUnsubscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#unsubscribe)**
Stop receiving selected streams from the server.
@@ -109,11 +109,11 @@ Stop receiving selected streams from the server.
##Transaction query functions
**[request_transaction_entry(hash, [ledger_hash], [callback])](https://ripple.com/wiki/RPC_API#transaction_entry)**
**[requestTransactionEntry(hash, [ledger_hash], [callback])](https://ripple.com/wiki/RPC_API#transaction_entry)**
Searches a particular ledger for a transaction hash. Default ledger is the open ledger.
**[request_tx(hash, [callback])](https://ripple.com/wiki/RPC_API#tx)**
**[requestTx(hash, [callback])](https://ripple.com/wiki/RPC_API#tx)**
Searches ledger history for validated transaction hashes.
@@ -122,7 +122,7 @@ Searches ledger history for validated transaction hashes.
##Account query functions
**[request_account_info(account, [callback])](https://ripple.com/wiki/RPC_API#account_info)**
**[requestAccountInfo(account, [callback])](https://ripple.com/wiki/RPC_API#account_info)**
Return information about the specified account.
@@ -143,13 +143,13 @@ Return information about the specified account.
}
```
**[request_account_lines(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_lines)**
**[requestAccountLines(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_lines)**
**[request_account_offers(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_offers)**
**[requestAccountOffers(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_offers)**
Return the specified account's outstanding offers.
**[request_account_tx(opts, [callback])](https://ripple.com/wiki/RPC_API#account_tx)**
**[requestAccountTx(opts, [callback])](https://ripple.com/wiki/RPC_API#account_tx)**
Fetch a list of transactions that applied to this account.
@@ -167,25 +167,25 @@ Options:
+ `fwd_marker`
+ `rev_marker`
**[request_wallet_accounts(seed, [callback])](https://ripple.com/wiki/RPC_API#wallet_accounts)**
**[requestWalletAccounts(seed, [callback])](https://ripple.com/wiki/RPC_API#wallet_accounts)**
Return a list of accounts for a wallet.
+ requires trusted remote
**request_account_balance(account, ledger, [callback])**
**requestAccountBalance(account, ledger, [callback])**
Get the balance for an account. Returns an [Amount](https://github.com/ripple/ripple-lib/blob/develop/src/js/ripple/amount.js) object.
**request_account_flags(account, current, [callback])**
**requestAccountFlags(account, current, [callback])**
Return the flags for an account.
**request_owner_count(account, current, [callback])**
**requestOwnerCount(account, current, [callback])**
Return the owner count for an account.
**request_ripple_balance(account, issuer, currency, current, [callback])**
**requestRippleBalance(account, issuer, currency, current, [callback])**
Return a request to get a ripple balance
@@ -194,7 +194,7 @@ Return a request to get a ripple balance
##Order book query functions
**[request_book_offers(gets, pays, taker, [callback])](https://ripple.com/wiki/RPC_API#book_offers)**
**[requestBookOffers(gets, pays, taker, [callback])](https://ripple.com/wiki/RPC_API#book_offers)**
Return the offers for an order book as one or more pages.
@@ -217,18 +217,18 @@ request.request();
##Transaction submission functions
**[request_sign(secret, tx_json, [callback])](https://ripple.com/wiki/RPC_API#sign)**
**[requestSign(secret, tx_json, [callback])](https://ripple.com/wiki/RPC_API#sign)**
Sign a transaction.
+ requires trusted remote
**[request_submit([callback])](https://ripple.com/wiki/RPC_API#submit)**
**[requestSubmit([callback])](https://ripple.com/wiki/RPC_API#submit)**
Submit a transaction to the network. This command is used internally to submit transactions with a greater degree of reliability. See [Submitting a payment to the network](GUIDES.md#3-submitting-a-payment-to-the-network) for details.
**[request_ripple_path_find(src_account, dst_account, dst_amount, src_currencies, [callback])](https://ripple.com/wiki/RPC_API#path_find)**
**[requestRipplePathFind(src_account, dst_account, dst_amount, src_currencies, [callback])](https://ripple.com/wiki/RPC_API#path_find)**
**transaction([destination], [source], [amount], [callback])**

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "0.7.35",
"version": "0.7.36",
"description": "Ripple JavaScript client library",
"files": [
"src/js/*",

View File

@@ -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

View File

@@ -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;

View File

@@ -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');

View File

@@ -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;

203
src/js/ripple/message.js Normal file
View File

@@ -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;

View File

@@ -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;

View File

@@ -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: <string>
@@ -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);

View File

@@ -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 = { };

View File

@@ -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') {

View File

@@ -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<l; i++) {
var flag = flag_set[i];
if (transaction_flags.hasOwnProperty(flag)) {
this.tx_json.Flags += transaction_flags[flag];
} else {
// XXX Immediately report an error or mark it.
return this.emit('error', new RippleError('tejInvalidFlag'));
}
}
@@ -508,10 +558,30 @@ Transaction.prototype.setFlags = function(flags) {
// .transfer_rate()
// .wallet_locator() NYI
// .wallet_size() NYI
Transaction.prototype.accountSet = function(src) {
/**
* Construct an 'AccountSet' transaction.
*
* Note that bit flags can be set using the .setFlags() method
* but for 'AccountSet' transactions there is an additional way to
* modify AccountRoot flags. The values available for the SetFlag
* and ClearFlag are as follows:
*
* "asfRequireDest"
* Require a destination tag
* "asfRequireAuth"
* Authorization is required to extend trust
* "asfDisallowXRP"
* XRP should not be sent to this account
* "asfDisableMaster"
* Disallow use of the master key
*/
Transaction.prototype.accountSet = function(src, set_flag, clear_flag) {
if (typeof src === 'object') {
var options = src;
src = options.source || options.from;
src = options.source || options.from || options.account;
set_flag = options.set_flag || options.set;
clear_flag = options.clear_flag || options.clear;
}
if (!UInt160.is_valid(src)) {
@@ -520,6 +590,21 @@ Transaction.prototype.accountSet = function(src) {
this.tx_json.TransactionType = 'AccountSet';
this.tx_json.Account = UInt160.json_rewrite(src);
var SetClearFlags = Transaction.set_clear_flags.AccountSet;
function prepareFlag(flag) {
return (typeof flag === 'number') ? flag : (SetClearFlags[flag] || SetClearFlags['asf' + flag]);
};
if (set_flag && (set_flag = prepareFlag(set_flag))) {
this.tx_json.SetFlag = set_flag;
}
if (clear_flag && (clear_flag = prepareFlag(clear_flag))) {
this.tx_json.ClearFlag = clear_flag;
}
return this;
};
@@ -529,13 +614,14 @@ Transaction.prototype.claim = function(src, generator, public_key, signature) {
signature = options.signature;
public_key = options.public_key;
generator = options.generator;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
this.tx_json.TransactionType = 'Claim';
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
@@ -543,7 +629,7 @@ Transaction.prototype.offerCancel = function(src, sequence) {
if (typeof src === 'object') {
var options = src;
sequence = options.sequence;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -553,6 +639,7 @@ Transaction.prototype.offerCancel = function(src, sequence) {
this.tx_json.TransactionType = 'OfferCancel';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.OfferSequence = Number(sequence);
return this;
};
@@ -567,7 +654,7 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira
expiration = options.expiration;
taker_gets = options.taker_gets || options.sell;
taker_pays = options.taker_pays || options.buy;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -580,15 +667,7 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira
this.tx_json.TakerGets = Amount.json_rewrite(taker_gets);
if (expiration) {
switch (expiration.constructor) {
case Date:
//offset = (new Date(2000, 0, 1).getTime()) - (new Date(1970, 0, 1).getTime());
this.tx_json.Expiration = expiration.getTime() - 946684800000;
break;
case Number:
this.tx_json.Expiration = expiration;
break;
}
this.tx_json.Expiration = utils.time.toRipple(expiration);
}
if (cancel_sequence) {
@@ -611,6 +690,7 @@ Transaction.prototype.passwordFund = function(src, dst) {
this.tx_json.TransactionType = 'PasswordFund';
this.tx_json.Destination = UInt160.json_rewrite(dst);
return this;
};
@@ -621,7 +701,7 @@ Transaction.prototype.passwordSet = function(src, authorized_key, generator, pub
public_key = options.public_key;
generator = options.generator;
authorized_key = options.authorized_key;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -633,9 +713,39 @@ Transaction.prototype.passwordSet = function(src, authorized_key, generator, pub
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
/**
* Construct a 'SetRegularKey' transaction.
* If the RegularKey is set, the private key that corresponds to
* it can be used to sign transactions instead of the master key
*
* The RegularKey must be a valid Ripple Address, or a Hash160 of
* the public key corresponding to the new private signing key.
*/
Transaction.prototype.setRegularKey = function(src, regular_key) {
if (typeof src === 'object') {
var options = src;
src = options.address || options.account || options.from;
regular_key = options.regular_key;
}
if (!UInt160.is_valid(src)) {
throw new Error('Source address invalid');
}
if (!UInt160.is_valid(regular_key)) {
throw new Error('RegularKey must be a valid Ripple Address (a Hash160 of the public key)');
}
this.tx_json.TransactionType = 'SetRegularKey';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.RegularKey = UInt160.json_rewrite(regular_key);
};
// Construct a 'payment' transaction.
//
// When a transaction is submitted:
@@ -658,10 +768,11 @@ Transaction.prototype.payment = function(src, dst, amount) {
var options = src;
amount = options.amount;
dst = options.destination || options.to;
src = options.source || options.from;
if (options.invoiceID) {
this.invoiceID(options.invoiceID);
}
src = options.source || options.from || options.account;
}
if (src.invoiceID) {
this.invoiceID(src.invoiceID);
}
if (!UInt160.is_valid(src)) {
@@ -691,7 +802,7 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o
quality_out = options.quality_out;
quality_in = options.quality_in;
limit = options.limit;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -725,7 +836,7 @@ Transaction.prototype.walletAdd = function(src, amount, authorized_key, public_k
public_key = options.public_key;
authorized_key = options.authorized_key;
amount = options.amount;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -737,6 +848,7 @@ Transaction.prototype.walletAdd = function(src, amount, authorized_key, public_k
this.tx_json.RegularKey = authorized_key;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
@@ -766,12 +878,12 @@ Transaction.prototype.submit = function(callback) {
var account = this.tx_json.Account;
if (typeof account !== 'string') {
this.emit('error', new RippleError('tejInvalidAccount', 'Account is unspecified'));
} else {
// YYY Might check paths for invalid accounts.
this.remote.account(account).submit(this);
return this.emit('error', new RippleError('tejInvalidAccount', 'Account is unspecified'));
}
// YYY Might check paths for invalid accounts.
this.remote.account(account).submit(this);
return this;
};

View File

@@ -184,25 +184,34 @@ TransactionManager.prototype._fillSequence = function(tx, callback) {
fill.account_set(self._accountID);
fill.tx_json.Sequence = sequence;
fill.once('submitted', callback);
// Secrets may be set on a per-transaction basis
if (tx._secret) {
fill.secret(tx._secret);
}
fill.submit();
};
function sequenceLoaded(err, sequence) {
if (typeof sequence !== 'number') {
callback(new Error('Failed to fetch account transaction sequence'));
return;
return callback(new Error('Failed to fetch account transaction sequence'));
}
var sequenceDif = tx.tx_json.Sequence - sequence;
var submitted = 0;
for (var i=sequence; i<tx.tx_json.Sequence; i++) {
submitFill(i, function() {
;(function nextFill(sequence) {
if (sequence >= 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;

View File

@@ -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<this._queue.length; i++) {
var submitIndex = this._queue[i].submitIndex;
if (typeof submitIndex !== 'number') {
// If any pending transactions don't have a submit index,
// return -1 for scanning all previous transactions
minLedger = -1;
break;
}
if (submitIndex < minLedger) {
minLedger = submitIndex;
}
};
if (!isFinite(minLedger)) minLedger = -1;
if (minLedger !== -1) minLedger -= 1;
return minLedger;
};
TransactionQueue.prototype.save = function() {
if (typeof this._save !== 'function') return;
@@ -60,7 +31,7 @@ TransactionQueue.prototype.save = function() {
*/
TransactionQueue.prototype.addReceivedSequence = function(sequence) {
this._sequenceCache[sequence] = true;
this._sequenceCache.set(String(sequence), true);
};
/**
@@ -68,7 +39,7 @@ TransactionQueue.prototype.addReceivedSequence = function(sequence) {
*/
TransactionQueue.prototype.addReceivedId = function(id, transaction) {
this._idCache[id] = transaction;
this._idCache.set(id, transaction);
};
/**
@@ -76,7 +47,7 @@ TransactionQueue.prototype.addReceivedId = function(id, transaction) {
*/
TransactionQueue.prototype.getReceived = function(id) {
return this._idCache[id];
return this._idCache.get(id);
};
/**
@@ -85,7 +56,7 @@ TransactionQueue.prototype.getReceived = function(id) {
*/
TransactionQueue.prototype.hasSequence = function(sequence) {
return this._sequenceCache[sequence] || false;
return this._sequenceCache.has(String(sequence));
};
/**
@@ -121,10 +92,6 @@ TransactionQueue.prototype.remove = function(tx) {
}
}
if (!this._queue.length) {
this.clearCache();
}
this.save();
};

View File

@@ -234,7 +234,7 @@ UInt.prototype.parse_number = function (j) {
if ("number" === typeof j &&
j === +j &&
j > 0) {
j >= 0) {
// XXX Better, faster way to get BigInteger from JS int?
this._value = new BigInteger(""+j);
}

View File

@@ -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;

View File

@@ -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() +
')';
};

View File

@@ -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));
};

View File

@@ -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));
};

View File

@@ -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);
};

View File

@@ -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;
};

View File

@@ -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;

181
test/account-test.js Normal file
View File

@@ -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);
});
});
});
});

View File

@@ -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');
});
});
});

327
test/message-test.js Normal file
View File

@@ -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();
});
});
});
});

View File

@@ -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

View File

@@ -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);
});
});
});

25
test/uint-test.js Normal file
View File

@@ -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