UNPKG

hw-app-eos-test

Version:

Ledger Hardware Wallet Stellar Application API

311 lines (276 loc) 10.2 kB
'use strict' /* eslint new-cap: ["error", { "properties": false }] */ var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck') var _classCallCheck3 = _interopRequireDefault(_classCallCheck2) var _createClass2 = require('babel-runtime/helpers/createClass') var _createClass3 = _interopRequireDefault(_createClass2) var _promise = require('babel-runtime/core-js/promise') var _promise2 = _interopRequireDefault(_promise) var _sha = require('sha.js') function _interopRequireDefault (obj) { return obj && obj.__esModule ? obj : { default: obj } } var CLA = 0xe0 var INS_GET_PK = 0x02 var INS_SIGN_TX = 0x04 var INS_GET_CONF = 0x06 var INS_SIGN_TX_HASH = 0x08 var INS_KEEP_ALIVE = 0x10 var APDU_MAX_SIZE = 150 var P1_FIRST_APDU = 0x00 var P1_MORE_APDU = 0x80 var P2_LAST_APDU = 0x00 var P2_MORE_APDU = 0x80 var SW_OK = 0x9000 var SW_CANCEL = 0x6985 var SW_UNKNOWN_OP = 0x6c24 var SW_MULTI_OP = 0x6c25 var SW_NOT_ALLOWED = 0x6c66 var SW_UNSUPPORTED = 0x6d00 var SW_KEEP_ALIVE = 0x6e02 var TX_MAX_SIZE = 1540 /** * EosLedger API * * @example * import EosLedger from "@ledgerhq/hw-app-eos"; * const eosLedger = new EosLedger(transport) */ function hash (data) { var hasher = new _sha.sha256() hasher.update(data, 'utf8') return hasher.digest() } var EosLedger = (function () { function EosLedger (transport) { (0, _classCallCheck3.default)(this, EosLedger) this.transport = transport transport.decorateAppAPIMethods(this, ['getAppConfiguration', 'getPublicKey', 'signTransaction', 'signHash'], 'l0v') } (0, _createClass3.default)(EosLedger, [{ key: 'getAppConfiguration', value: function getAppConfiguration () { return this.transport.send(CLA, INS_GET_CONF, 0x00, 0x00).then(function (response) { var multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02 var version = '' + response[1] + '.' + response[2] + '.' + response[3] return { version: version, multiOpsEnabled: multiOpsEnabled } }) } /** * get EosLedger public key for a given BIP 32 path. * @param path a path in BIP 32 format * @option boolValidate optionally enable key pair validation * @option boolDisplay optionally enable or not the display * @return an object with the publicKey * @example * eosLedger.getPublicKey("44'/60'/0'").then(o => o.publicKey) */ }, { key: 'getPublicKey', value: function getPublicKey (path, boolValidate, boolDisplay) { var _this = this; (0, checkEosBip32Path)(path) var apdus = [] var response = void 0 var pathElts = (0, splitPath)(path) var buffer = Buffer.alloc(1 + pathElts.length * 4) buffer[0] = pathElts.length pathElts.forEach(function (element, index) { buffer.writeUInt32BE(element, 1 + 4 * index) }) apdus.push(buffer) var keepAlive = false return (0, foreach)(apdus, function (data) { return _this.transport.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_GET_PK, boolValidate ? 0x01 : 0x00, boolDisplay ? 0x01 : 0x00, data, [SW_OK, SW_KEEP_ALIVE]).then(function (apduResponse) { var status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0) if (status === SW_KEEP_ALIVE) { keepAlive = true apdus.push(Buffer.alloc(0)) } response = apduResponse }) }).then(function () { // response = Buffer.from(response, 'hex'); var offset = 0 var publicKey = response.slice(offset, offset + 50) return { publicKey: publicKey } }) } /** * sign a EosLedger transaction. * @param path a path in BIP 32 format * @param transaction signature base of the transaction to sign * @return an object with the signature and the status * @example * eosLedger.signTransaction("44'/60'/0'", signatureBase).then(o => o.signature) */ }, { key: 'signTransaction', value: function signTransaction (path, transaction) { var _this2 = this; (0, checkEosBip32Path)(path) if (transaction.length > TX_MAX_SIZE) { throw new Error('Transaction too large: max = ' + TX_MAX_SIZE + '; actual = ' + transaction.length) } var apdus = [] var response = void 0 var pathElts = (0, splitPath)(path) var bufferSize = 1 + pathElts.length * 4 var buffer = Buffer.alloc(bufferSize) buffer[0] = pathElts.length pathElts.forEach(function (element, index) { buffer.writeUInt32BE(element, 1 + 4 * index) }) var chunkSize = APDU_MAX_SIZE - bufferSize if (transaction.length <= chunkSize) { // it fits in a single apdu apdus.push(Buffer.concat([buffer, transaction])) } else { // we need to send multiple apdus to transmit the entire transaction var chunk = Buffer.alloc(chunkSize) var offset = 0 transaction.copy(chunk, 0, offset, chunkSize) apdus.push(Buffer.concat([buffer, chunk])) offset += chunkSize while (offset < transaction.length) { var remaining = transaction.length - offset chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE chunk = Buffer.alloc(chunkSize) transaction.copy(chunk, 0, offset, offset + chunkSize) offset += chunkSize apdus.push(chunk) } } var keepAlive = false return (0, foreach)(apdus, function (data, i) { return _this2.transport.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX, i === 0 ? P1_FIRST_APDU : P1_MORE_APDU, i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU, data, [SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE]).then(function (apduResponse) { var status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0) if (status === SW_KEEP_ALIVE) { keepAlive = true apdus.push(Buffer.alloc(0)) } response = apduResponse }) }).then(function () { var status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0) if (status === SW_OK) { var _signature2 = Buffer.from(response.slice(0, response.length - 2)) return { signature: _signature2 } } else if (status === SW_UNKNOWN_OP) { return _this2.signHashPrivate(path, (0, hash)(transaction)) } else if (status === SW_MULTI_OP) { // multi-operation transaction: attempt hash signing return _this2.signHashPrivate(path, (0, hash)(transaction)) } else { throw new Error('Transaction approval request was rejected') } }) } /** * sign a EosLedger transaction hash. * @param path a path in BIP 32 format * @param hash hash of the transaction to sign * @return an object with the signature * @example * eosLedger.signHash("44'/60'/0'", hash).then(o => o.signature) */ }, { key: 'signHash', value: function signHash (path, hash) { (0, checkEosBip32Path)(path) return this.signHashPrivate(path, hash) } }, { key: 'signHashPrivate', value: function signHashPrivate (path, hash) { var _this3 = this var apdus = [] var response = void 0 var pathElts = (0, splitPath)(path) var buffer = Buffer.alloc(1 + pathElts.length * 4) buffer[0] = pathElts.length pathElts.forEach(function (element, index) { buffer.writeUInt32BE(element, 1 + 4 * index) }) apdus.push(Buffer.concat([buffer, hash])) var keepAlive = false return (0, foreach)(apdus, function (data) { return _this3.transport.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH, 0x00, 0x00, data, [SW_OK, SW_CANCEL, SW_NOT_ALLOWED, SW_UNSUPPORTED, SW_KEEP_ALIVE]).then(function (apduResponse) { var status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0) if (status === SW_KEEP_ALIVE) { keepAlive = true apdus.push(Buffer.alloc(0)) } response = apduResponse }) }).then(function () { var status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0) if (status === SW_OK) { var _signature3 = Buffer.from(response.slice(0, response.length - 2)) return { signature: _signature3 } } else if (status === SW_CANCEL) { throw new Error('Transaction approval request was rejected') } else if (status === SW_UNSUPPORTED) { throw new Error('Hash signing is not supported') } else { throw new Error('Hash signing not allowed. Have you enabled it in the app settings?') } }) } }]) return EosLedger }()) // // TODO use bip32-path library function splitPath (path) { var result = [] var components = path.split('/') components.forEach(function (element) { var number = parseInt(element, 10) if (isNaN(number)) { return // FIXME shouldn't it throws instead? } if (element.length > 1 && element[element.length - 1] === "'") { number += 0x80000000 } result.push(number) }) return result } function foreach (arr, callback) { function iterate (index, array, result) { if (index >= array.length) { return result } else { return callback(array[index], index).then(function (res) { result.push(res) return iterate(index + 1, array, result) }) } } return _promise2.default.resolve().then(function () { return iterate(0, arr, []) }) } function checkEosBip32Path (path) { path.split('/').forEach(function (element) { if (!element.toString().endsWith("'")) { throw new Error('Detected a non-hardened path element in requested BIP32 path.' + ' Non-hardended paths are not supported at this time. Please use an all-hardened path.' + " Example: 44'/60'/0'") } }) } export default EosLedger