mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-19 19:55:51 +00:00
Remove message.js
This commit is contained in:
@@ -13,7 +13,6 @@ exports.Seed = require('./seed').Seed;
|
||||
exports.Meta = require('./meta').Meta;
|
||||
exports.SerializedObject = require('./serializedobject').SerializedObject;
|
||||
exports.RippleError = require('./rippleerror').RippleError;
|
||||
exports.Message = require('./message').Message;
|
||||
exports.binformat = require('./binformat');
|
||||
exports.utils = require('./utils');
|
||||
exports.Server = require('./server').Server;
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const async = require('async');
|
||||
const sjcl = require('./utils').sjcl;
|
||||
const Remote = require('./remote').Remote;
|
||||
const Seed = require('./seed').Seed;
|
||||
const KeyPair = require('./keypair').KeyPair;
|
||||
const Account = require('./account').Account;
|
||||
const UInt160 = require('./uint160').UInt160;
|
||||
|
||||
// Message class (static)
|
||||
const Message = {};
|
||||
|
||||
Message.hashFunction = sjcl.hash.sha512.hash;
|
||||
Message.MAGIC_BYTES = 'Ripple Signed Message:\n';
|
||||
|
||||
const REGEX_HEX = /^[0-9a-fA-F]+$/;
|
||||
const 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.
|
||||
* @return {Base64-encoded String} signature
|
||||
*/
|
||||
Message.signMessage = function(message, secret_key, account) {
|
||||
return Message.signHash(Message.hashFunction(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) {
|
||||
|
||||
const hash = typeof _hash === 'string' && /^[0-9a-fA-F]+$/.test(_hash) ?
|
||||
sjcl.codec.hex.toBits(_hash) : _hash;
|
||||
|
||||
if (typeof hash !== 'object' ||
|
||||
typeof hash[0] !== 'number' ||
|
||||
hash.length <= 0) {
|
||||
throw new Error('Hash must be a bitArray or hex-encoded string');
|
||||
}
|
||||
|
||||
const secret_key = !(secret_key_ instanceof sjcl.ecc.ecdsa.secretKey) ?
|
||||
Seed.from_json(secret_key_).get_key(account)._secret : secret_key_;
|
||||
|
||||
const PARANOIA_256_BITS = 6; // sjcl constant for ensuring 256 bits of entropy
|
||||
const signature_bits = secret_key.signWithRecoverablePublicKey(hash,
|
||||
PARANOIA_256_BITS);
|
||||
const 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.hashFunction(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) {
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Must supply callback function');
|
||||
}
|
||||
|
||||
const hash = REGEX_HEX.test(data.hash) ?
|
||||
sjcl.codec.hex.toBits(data.hash) :
|
||||
data.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'));
|
||||
}
|
||||
|
||||
const account = data.account || data.address;
|
||||
if (!account || !UInt160.from_json(account).is_valid()) {
|
||||
return callback(new Error('Account must be a valid ripple address'));
|
||||
}
|
||||
|
||||
const signature = data.signature;
|
||||
if (typeof signature !== 'string' || !REGEX_BASE64.test(signature)) {
|
||||
return callback(new Error('Signature must be a Base64-encoded string'));
|
||||
}
|
||||
|
||||
const signature_bits = 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) {
|
||||
let public_key;
|
||||
try {
|
||||
public_key =
|
||||
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature_bits);
|
||||
} 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
|
||||
const key_pair = new KeyPair();
|
||||
key_pair._pubkey = public_key;
|
||||
const public_key_hex = key_pair.to_hex_pub();
|
||||
|
||||
const account_class_instance = new Account(remote, account);
|
||||
account_class_instance.publicKeyIsActive(public_key_hex, async_callback);
|
||||
|
||||
}
|
||||
|
||||
const steps = [
|
||||
recoverPublicKey,
|
||||
checkPublicKeyIsValid
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
|
||||
};
|
||||
|
||||
exports.Message = Message;
|
||||
@@ -1,330 +0,0 @@
|
||||
/* eslint-disable max-len */
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const sjcl = require('ripple-lib').sjcl;
|
||||
const Message = require('ripple-lib').Message;
|
||||
const Seed = require('ripple-lib').Seed;
|
||||
const Remote = require('ripple-lib').Remote;
|
||||
|
||||
describe('Message', function() {
|
||||
|
||||
describe('signMessage', function() {
|
||||
|
||||
it('should prepend the MAGIC_BYTES, call the hashFunction, and then call signHash', function() {
|
||||
|
||||
const normal_signHash = Message.signHash;
|
||||
|
||||
const message_text = 'Hello World!';
|
||||
|
||||
let signHash_called = false;
|
||||
Message.signHash = function(hash) {
|
||||
signHash_called = true;
|
||||
assert.deepEqual(hash, Message.hashFunction(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() {
|
||||
|
||||
const normal_random = sjcl.random.randomWords;
|
||||
|
||||
sjcl.random.randomWords = function(num_words) {
|
||||
const words = [];
|
||||
for (let w = 0; w < num_words; w++) {
|
||||
words.push(sjcl.codec.hex.toBits('00000000'));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
const secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
// const address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
|
||||
const hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
const signature1 = Message.signHash(hash, secret_string);
|
||||
const 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() {
|
||||
|
||||
const normal_random = sjcl.random.randomWords;
|
||||
|
||||
sjcl.random.randomWords = function(num_words) {
|
||||
const words = [];
|
||||
for (let w = 0; w < num_words; w++) {
|
||||
words.push(sjcl.codec.hex.toBits('00000000'));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
const secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
// const address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
|
||||
const hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
const signature1 = Message.signHash(hash, secret_string);
|
||||
const 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() {
|
||||
// Annoyingly non hex can be fed to the BigInteger(s, 16) constructor and
|
||||
// it will parse as a number. Before the commit of this comment, this test
|
||||
// involved a fixture of 32 chars, which was assumed to be hex. The test
|
||||
// passed, but for the wrong wreasons. There was a bug in Seed.parse_json.
|
||||
|
||||
// Seed.from_json only creates invalid seeds from empty strings or invalid
|
||||
// base58 starting with an s, which it tries to base 58 decode/check sum.
|
||||
// The rest will be assumed to be a passphrase.
|
||||
|
||||
// This is a bad b58 seed
|
||||
const secret_string = 'sbadsafRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
const 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() {
|
||||
|
||||
const secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
const 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() {
|
||||
const REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/;
|
||||
|
||||
const normal_random = sjcl.random.randomWords;
|
||||
|
||||
sjcl.random.randomWords = function(num_words) {
|
||||
const words = [];
|
||||
for (let w = 0; w < num_words; w++) {
|
||||
words.push(sjcl.codec.hex.toBits('00000000'));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
const secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
// const address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
|
||||
const hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
const 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 hashFunction, and then call verifyHashSignature', function() {
|
||||
|
||||
const normal_verifyHashSignature = Message.verifyHashSignature;
|
||||
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I='
|
||||
};
|
||||
|
||||
let verifyHashSignature_called = false;
|
||||
Message.verifyHashSignature = function(vhs_data, remote, callback) {
|
||||
verifyHashSignature_called = true;
|
||||
|
||||
assert.deepEqual(vhs_data.hash, Message.hashFunction(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() {
|
||||
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
|
||||
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
|
||||
};
|
||||
|
||||
assert.throws(function() {
|
||||
Message.verifyHashSignature(data);
|
||||
}, /(?=.*callback\ function).*/);
|
||||
});
|
||||
|
||||
it('should respond with an error if the hash is missing or invalid', function(done) {
|
||||
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
|
||||
};
|
||||
|
||||
const test_remote = new Remote();
|
||||
test_remote.state = 'online';
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err) {
|
||||
assert(/hash/i.test(err.message));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond with an error if the account is missing or invalid', function(done) {
|
||||
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
|
||||
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk='
|
||||
};
|
||||
|
||||
const test_remote = new Remote();
|
||||
test_remote.state = 'online';
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err) {
|
||||
assert(/account|address/i.test(err.message));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond with an error if the signature is missing or invalid', function(done) {
|
||||
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
|
||||
};
|
||||
|
||||
const test_remote = new Remote();
|
||||
test_remote.state = 'online';
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err) {
|
||||
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) {
|
||||
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs='
|
||||
};
|
||||
|
||||
const test_remote = new Remote();
|
||||
test_remote.state = 'online';
|
||||
test_remote.requestAccountInfo = function(options, callback) {
|
||||
const account = options.account;
|
||||
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
|
||||
const data = {
|
||||
message: 'Hello world!',
|
||||
hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk='
|
||||
};
|
||||
|
||||
const test_remote = new Remote();
|
||||
test_remote.state = 'online';
|
||||
test_remote.requestAccountInfo = function(options, callback) {
|
||||
const account = options.account;
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user