diff --git a/src/core/index.js b/src/core/index.js index aac4913dea..19e27bcaec 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -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; diff --git a/src/core/message.js b/src/core/message.js deleted file mode 100644 index d3b39a8d9d..0000000000 --- a/src/core/message.js +++ /dev/null @@ -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; diff --git a/test/message-test.js b/test/message-test.js deleted file mode 100644 index 75e8474971..0000000000 --- a/test/message-test.js +++ /dev/null @@ -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(); - }); - - }); - - }); - -});