diff --git a/src/api.js b/src/api.js index 0aeb2b04cc..a243cfbadd 100644 --- a/src/api.js +++ b/src/api.js @@ -57,6 +57,10 @@ const errors = require('./common').errors const generateAddress = require('./offline/generate-address').generateAddressAPI const computeLedgerHash = require('./offline/ledgerhash') +const signMessage = + require('./offline/sign-message') +const verifyMessage = + require('./offline/verify-message') const signPaymentChannelClaim = require('./offline/sign-payment-channel-claim') const verifyPaymentChannelClaim = @@ -152,6 +156,8 @@ _.assign(RippleAPI.prototype, { generateAddress, computeLedgerHash, + signMessage, + verifyMessage, signPaymentChannelClaim, verifyPaymentChannelClaim, errors diff --git a/src/common/index.js b/src/common/index.js index c79d69022b..e2b179177b 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -18,5 +18,7 @@ module.exports = { utils.convertKeysFromSnakeCaseToCamelCase, iso8601ToRippleTime: utils.iso8601ToRippleTime, rippleTimeToISO8601: utils.rippleTimeToISO8601, - isValidSecret: utils.isValidSecret + isValidSecret: utils.isValidSecret, + stringToUtf8ByteArray: utils.stringToUtf8ByteArray, + bytesToHex: utils.bytesToHex } diff --git a/src/common/schema-validator.js b/src/common/schema-validator.js index 1ad314ff46..8786a94c87 100644 --- a/src/common/schema-validator.js +++ b/src/common/schema-validator.js @@ -22,6 +22,7 @@ function loadSchemas() { require('./schemas/objects/max-adjustment.json'), require('./schemas/objects/memo.json'), require('./schemas/objects/memos.json'), + require('./schemas/objects/message.json'), require('./schemas/objects/public-key.json'), require('./schemas/objects/uint32.json'), require('./schemas/objects/value.json'), @@ -77,6 +78,8 @@ function loadSchemas() { require('./schemas/output/get-transaction.json'), require('./schemas/output/get-transactions.json'), require('./schemas/output/get-trustlines.json'), + require('./schemas/output/sign-message.json'), + require('./schemas/output/verify-message.json'), require('./schemas/output/sign-payment-channel-claim.json'), require('./schemas/output/verify-payment-channel-claim.json'), require('./schemas/input/get-balances.json'), @@ -107,6 +110,8 @@ function loadSchemas() { require('./schemas/input/sign.json'), require('./schemas/input/submit.json'), require('./schemas/input/generate-address.json'), + require('./schemas/input/sign-message.json'), + require('./schemas/input/verify-message.json'), require('./schemas/input/sign-payment-channel-claim.json'), require('./schemas/input/verify-payment-channel-claim.json'), require('./schemas/input/combine.json') diff --git a/src/common/schemas/input/sign-message.json b/src/common/schemas/input/sign-message.json new file mode 100644 index 0000000000..78b7e266b0 --- /dev/null +++ b/src/common/schemas/input/sign-message.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "signMessageParameters", + "type": "object", + "properties": { + "message": { + "$ref": "message", + "description": "Message text." + }, + "privateKey": { + "$ref": "publicKey", + "description": "The private key to sign the payment channel claim." + } + }, + "additionalProperties": false, + "required": ["message", "privateKey"] +} diff --git a/src/common/schemas/input/verify-message.json b/src/common/schemas/input/verify-message.json new file mode 100644 index 0000000000..30674a7399 --- /dev/null +++ b/src/common/schemas/input/verify-message.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "verifyMessageParameters", + "type": "object", + "properties": { + "message": { + "$ref": "message", + "description": "Message text." + }, + "signature": { + "$ref": "signature", + "description": "Signature of this message." + }, + "publicKey": { + "$ref": "publicKey", + "description": "Public key of the message's sender" + } + }, + "additionalProperties": false, + "required": ["message", "signature", "publicKey"] +} diff --git a/src/common/schemas/objects/message.json b/src/common/schemas/objects/message.json new file mode 100644 index 0000000000..a7c30f6755 --- /dev/null +++ b/src/common/schemas/objects/message.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "message", + "description": "Message objects represent arbitrary data that can be signed", + "type": "string", + "additionalProperties": false +} diff --git a/src/common/schemas/output/sign-message.json b/src/common/schemas/output/sign-message.json new file mode 100644 index 0000000000..67467e0ed8 --- /dev/null +++ b/src/common/schemas/output/sign-message.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "signMessage", + "type": "string", + "$ref": "signature", + "additionalProperties": false +} diff --git a/src/common/schemas/output/verify-message.json b/src/common/schemas/output/verify-message.json new file mode 100644 index 0000000000..dedf464eb6 --- /dev/null +++ b/src/common/schemas/output/verify-message.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "verifyMessage", + "type": "boolean", + "additionalProperties": false +} diff --git a/src/common/utils.js b/src/common/utils.js index cf806a4cd6..a7c001c438 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -81,6 +81,46 @@ function iso8601ToRippleTime(iso8601: string): number { return unixToRippleTimestamp(Date.parse(iso8601)) } +/** + * Converts a JS string to a UTF-8 "byte" array. + * From {@link https://git.io/v52py|Google's common JavaScript library}. + * @param {string} str 16-bit unicode string. + * @return {!Array} UTF-8 byte array. + */ +function stringToUtf8ByteArray(str) { + var out = [], p = 0; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if (c < 128) { + out[p++] = c; + } else if (c < 2048) { + out[p++] = (c >> 6) | 192; + out[p++] = (c & 63) | 128; + } else if ( + ((c & 0xFC00) == 0xD800) && (i + 1) < str.length && + ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) { + // Surrogate Pair + c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF); + out[p++] = (c >> 18) | 240; + out[p++] = ((c >> 12) & 63) | 128; + out[p++] = ((c >> 6) & 63) | 128; + out[p++] = (c & 63) | 128; + } else { + out[p++] = (c >> 12) | 224; + out[p++] = ((c >> 6) & 63) | 128; + out[p++] = (c & 63) | 128; + } + } + return out; +} + +function bytesToHex(a) { + return a.map(function(byteValue) { + const hex = byteValue.toString(16).toUpperCase(); + return hex.length > 1 ? hex : '0' + hex; + }).join(''); +} + module.exports = { dropsToXrp, xrpToDrops, @@ -89,5 +129,7 @@ module.exports = { removeUndefined, rippleTimeToISO8601, iso8601ToRippleTime, - isValidSecret + isValidSecret, + stringToUtf8ByteArray, + bytesToHex } diff --git a/src/common/validate.js b/src/common/validate.js index 53f6f3a6d4..b15e33668a 100644 --- a/src/common/validate.js +++ b/src/common/validate.js @@ -58,6 +58,10 @@ module.exports = { submit: _.partial(schemaValidate, 'submitParameters'), computeLedgerHash: _.partial(schemaValidate, 'computeLedgerHashParameters'), generateAddress: _.partial(schemaValidate, 'generateAddressParameters'), + signMessage: _.partial(schemaValidate, + 'signMessageParameters'), + verifyMessage: _.partial(schemaValidate, + 'verifyMessageParameters'), signPaymentChannelClaim: _.partial(schemaValidate, 'signPaymentChannelClaimParameters'), verifyPaymentChannelClaim: _.partial(schemaValidate, diff --git a/src/offline/sign-message.js b/src/offline/sign-message.js new file mode 100644 index 0000000000..2af5e2c58d --- /dev/null +++ b/src/offline/sign-message.js @@ -0,0 +1,16 @@ +/* @flow */ +'use strict' // eslint-disable-line strict +const common = require('../common') +const keypairs = require('ripple-keypairs') +const binary = require('ripple-binary-codec') +const {validate, stringToUtf8ByteArray, bytesToHex} = common + +function signMessage(message: string, + privateKey: string +): string { + validate.signMessage({message, privateKey}) + + return keypairs.sign(bytesToHex(stringToUtf8ByteArray(message)), privateKey) +} + +module.exports = signMessage diff --git a/src/offline/verify-message.js b/src/offline/verify-message.js new file mode 100644 index 0000000000..53b9432764 --- /dev/null +++ b/src/offline/verify-message.js @@ -0,0 +1,16 @@ +/* @flow */ +'use strict' // eslint-disable-line strict +const common = require('../common') +const keypairs = require('ripple-keypairs') +const binary = require('ripple-binary-codec') +const {validate, stringToUtf8ByteArray, bytesToHex} = common + +function verifyMessage(message: string, + signature: string, publicKey: string +): string { + validate.verifyMessage({message, signature, publicKey}) + + return keypairs.verify(bytesToHex(stringToUtf8ByteArray(message)), signature, publicKey) +} + +module.exports = verifyMessage diff --git a/test/api-test.js b/test/api-test.js index 1a35f9970d..700632c0a0 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -452,6 +452,35 @@ describe('RippleAPI', function() { }); }); + it('signMessage', function() { + const privateKey = + 'AC051121672EA8DC119D53AFBD0221607BE6B6B61A720BAB41C144B47E8782B3'; + const result = this.api.signMessage( + requests.signMessage.message, + privateKey); + checkResult(responses.signMessage, + 'signMessage', result) + }); + + it('verifyMessage', function() { + const publicKey = + '02DCAF8A5B6FBBC3582B87F326176C402B466EEE65FCC23189FC09F0834A78CE3C'; + const result = this.api.verifyMessage( + requests.signMessage.message, + responses.signMessage, publicKey); + checkResult(true, 'verifyMessage', result) + }); + + it('verifyMessage - invalid', function() { + const publicKey = + '02BB1EE6437B0EA8D5DB68EF4FC7CA77F919764A11649ED49EE9C5A0950BA8BF04'; + const result = this.api.verifyMessage( + requests.signMessage.message, + responses.signMessage, publicKey); + checkResult(false, + 'verifyMessage', result) + }); + it('signPaymentChannelClaim', function() { const privateKey = 'ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A'; diff --git a/test/fixtures/requests/index.js b/test/fixtures/requests/index.js index ab5b67a69d..26e62dcb2e 100644 --- a/test/fixtures/requests/index.js +++ b/test/fixtures/requests/index.js @@ -61,6 +61,7 @@ module.exports = { escrow: require('./sign-escrow.json'), signAs: require('./sign-as') }, + signMessage: require('./sign-message'), signPaymentChannelClaim: require('./sign-payment-channel-claim'), getPaths: { normal: require('./getpaths/normal'), diff --git a/test/fixtures/requests/sign-message.json b/test/fixtures/requests/sign-message.json new file mode 100644 index 0000000000..5df37aa1e2 --- /dev/null +++ b/test/fixtures/requests/sign-message.json @@ -0,0 +1,3 @@ +{ + "message": "This is a test message for the signMessage and verifyMessage tests." +} diff --git a/test/fixtures/responses/index.js b/test/fixtures/responses/index.js index fdf35151ba..e35243b54e 100644 --- a/test/fixtures/responses/index.js +++ b/test/fixtures/responses/index.js @@ -138,6 +138,7 @@ module.exports = { escrow: require('./sign-escrow.json'), signAs: require('./sign-as') }, + signMessage: require('./sign-message'), signPaymentChannelClaim: require('./sign-payment-channel-claim'), combine: { single: require('./combine.json') diff --git a/test/fixtures/responses/sign-message.json b/test/fixtures/responses/sign-message.json new file mode 100644 index 0000000000..8f1bfd1dbc --- /dev/null +++ b/test/fixtures/responses/sign-message.json @@ -0,0 +1 @@ +"3045022100E6D5B31AC54D3964DD016D5BC448CEF1D1643E7CB0190E6B289EBEAC8F92887802201A4EFD3B21C1792698D85770593B3503DD88C8BC513E19803F4AAA0951CEADB1"