'use strict'; angular.module('copayApp.services') .factory('ledger', function($log, bwcService, gettext, hwWallet) { var root = {}; var LEDGER_CHROME_ID = "kkdpmhnladdopljabkgpacgpliggeeaf"; root.callbacks = {}; root.hasSession = function() { root._message({ command: "has_session" }); } root.getEntropySource = function(isMultisig, account, callback) { root.getXPubKey(hwWallet.getEntropyPath('ledger', isMultisig, account), function(data) { if (!data.success) return callback(hwWallet._err(data)); return callback(null, hwWallet.pubKeyToEntropySource(data.xpubkey)); }); }; root.getXPubKey = function(path, callback) { $log.debug('Ledger deriving xPub path:', path); root.callbacks["get_xpubkey"] = callback; root._messageAfterSession({ command: "get_xpubkey", path: path }) }; root.getInfoForNewWallet = function(isMultisig, account, callback) { var opts = {}; root.getEntropySource(isMultisig, account, function(err, entropySource) { if (err) return callback(err); opts.entropySource = entropySource; root.getXPubKey(hwWallet.getAddressPath('ledger', isMultisig, account), function(data) { if (!data.success) { $log.warn(data.message); return callback(data); } opts.extendedPublicKey = data.xpubkey; opts.externalSource = 'ledger'; opts.account = account; // Old ledger compat opts.derivationStrategy = account ? 'BIP48' : 'BIP44'; return callback(null, opts); }); }); }; root._signP2SH = function(txp, account, isMultisig, callback) { root.callbacks["sign_p2sh"] = callback; var redeemScripts = []; var paths = []; var tx = bwcService.getUtils().buildTx(txp); for (var i = 0; i < tx.inputs.length; i++) { redeemScripts.push(new ByteString(tx.inputs[i].redeemScript.toBuffer().toString('hex'), GP.HEX).toString()); paths.push(hwWallet.getAddressPath('ledger', isMultisig, account) + txp.inputs[i].path.substring(1)); } var splitTransaction = root._splitTransaction(new ByteString(tx.toString(), GP.HEX)); var inputs = []; for (var i = 0; i < splitTransaction.inputs.length; i++) { var input = splitTransaction.inputs[i]; inputs.push([ root._reverseBytestring(input.prevout.bytes(0, 32)).toString(), root._reverseBytestring(input.prevout.bytes(32)).toString() ]); } $log.debug('Ledger signing paths:', paths); root._messageAfterSession({ command: "sign_p2sh", inputs: inputs, scripts: redeemScripts, outputs_number: splitTransaction.outputs.length, outputs_script: splitTransaction.outputScript.toString(), paths: paths }); }; root.signTx = function(txp, account, callback) { // TODO Compat var isMultisig = true; if (txp.addressType == 'P2PKH') { var msg = 'P2PKH wallets are not supported with ledger'; $log.error(msg); return callback(msg); } else { root._signP2SH(txp, account, isMultisig, callback); } } root._message = function(data) { chrome.runtime.sendMessage( LEDGER_CHROME_ID, { request: data }, function(response) { root._callback(response); } ); } root._messageAfterSession = function(data) { root._after_session = data; root._message({ command: "launch" }); root._should_poll_session = true; root._do_poll_session(); } root._do_poll_session = function() { root.hasSession(); if (root._should_poll_session) { setTimeout(root._do_poll_session, 500); } } root._callback = function(data) { if (typeof data == "object") { if (data.command == "has_session" && data.success) { root._message(root._after_session); root._after_session = null; root._should_poll_session = false; } else if (typeof root.callbacks[data.command] == "function") { root.callbacks[data.command](data); } } else { root._should_poll_session = false; Object.keys(root.callbacks).forEach(function(key) { root.callbacks[key]({ success: false, message: gettext("The Ledger Chrome application is not installed"), }); }); } } root._splitTransaction = function(transaction) { var result = {}; var inputs = []; var outputs = []; var offset = 0; var version = transaction.bytes(offset, 4); offset += 4; var varint = root._getVarint(transaction, offset); var numberInputs = varint[0]; offset += varint[1]; for (var i = 0; i < numberInputs; i++) { var input = {}; input['prevout'] = transaction.bytes(offset, 36); offset += 36; varint = root._getVarint(transaction, offset); offset += varint[1]; input['script'] = transaction.bytes(offset, varint[0]); offset += varint[0]; input['sequence'] = transaction.bytes(offset, 4); offset += 4; inputs.push(input); } varint = root._getVarint(transaction, offset); var numberOutputs = varint[0]; offset += varint[1]; var outputStartOffset = offset; for (var i = 0; i < numberOutputs; i++) { var output = {}; output['amount'] = transaction.bytes(offset, 8); offset += 8; varint = root._getVarint(transaction, offset); offset += varint[1]; output['script'] = transaction.bytes(offset, varint[0]); offset += varint[0]; outputs.push(output); } var locktime = transaction.bytes(offset, 4); result['version'] = version; result['inputs'] = inputs; result['outputs'] = outputs; result['locktime'] = locktime; result['outputScript'] = transaction.bytes(outputStartOffset, offset - outputStartOffset); return result; } root._getVarint = function(data, offset) { if (data.byteAt(offset) < 0xfd) { return [data.byteAt(offset), 1]; } if (data.byteAt(offset) == 0xfd) { return [((data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 3]; } if (data.byteAt(offset) == 0xfe) { return [((data.byteAt(offset + 4) << 24) + (data.byteAt(offset + 3) << 16) + (data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 5]; } } root._reverseBytestring = function(x) { var res = ""; for (var i = x.length - 1; i >= 0; i--) { res += Convert.toHexByte(x.byteAt(i)); } return new ByteString(res, GP.HEX); } return root; }); var Convert = {}; /** * Convert a binary string to his hexadecimal representation * @param {String} src binary string * @static * @returns {String} hexadecimal representation */ Convert.stringToHex = function(src) { var r = ""; var hexes = new Array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"); for (var i = 0; i < src.length; i++) { r += hexes[src.charCodeAt(i) >> 4] + hexes[src.charCodeAt(i) & 0xf]; } return r; } /** * Convert an hexadecimal string to its binary representation * @param {String} src hexadecimal string * @static * @return {Array} byte array * @throws {InvalidString} if the string isn't properly formatted */ Convert.hexToBin = function(src) { var result = ""; var digits = "0123456789ABCDEF"; if ((src.length % 2) != 0) { throw "Invalid string"; } src = src.toUpperCase(); for (var i = 0; i < src.length; i += 2) { var x1 = digits.indexOf(src.charAt(i)); if (x1 < 0) { return ""; } var x2 = digits.indexOf(src.charAt(i + 1)); if (x2 < 0) { return ""; } result += String.fromCharCode((x1 << 4) + x2); } return result; } /** * Convert a double digit hexadecimal number to an integer * @static * @param {String} data buffer containing the digit to parse * @param {Number} offset offset to the digit (default is 0) * @returns {Number} converted digit */ Convert.readHexDigit = function(data, offset) { var digits = '0123456789ABCDEF'; if (typeof offset == "undefined") { offset = 0; } return (digits.indexOf(data.substring(offset, offset + 1).toUpperCase()) << 4) + (digits.indexOf(data.substring(offset + 1, offset + 2).toUpperCase())); } /** * Convert a number to a two digits hexadecimal string (deprecated) * @static * @param {Number} number number to convert * @returns {String} converted number */ Convert.toHexDigit = function(number) { var digits = '0123456789abcdef'; return digits.charAt(number >> 4) + digits.charAt(number & 0x0F); } /** * Convert a number to a two digits hexadecimal string (similar to toHexDigit) * @static * @param {Number} number number to convert * @returns {String} converted number */ Convert.toHexByte = function(number) { return Convert.toHexDigit(number); } /** * Convert a BCD number to a two digits hexadecimal string * @static * @param {Number} number number to convert * @returns {String} converted number */ Convert.toHexByteBCD = function(numberBCD) { var number = ((numberBCD / 10) * 16) + (numberBCD % 10); return Convert.toHexDigit(number); } /** * Convert a number to an hexadecimal short number * @static * @param {Number} number number to convert * @returns {String} converted number */ Convert.toHexShort = function(number) { return Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff); } /** * Convert a number to an hexadecimal int number * @static * @param {Number} number number to convert * @returns {String} converted number */ Convert.toHexInt = function(number) { return Convert.toHexDigit((number >> 24) & 0xff) + Convert.toHexDigit((number >> 16) & 0xff) + Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff); } var GP = {}; GP.ASCII = 1; GP.HEX = 5; /** * @class GPScript ByteString implementation * @param {String} value initial value * @param {HEX|ASCII} encoding encoding to use * @property {Number} length length of the ByteString * @constructs */ var ByteString = function(value, encoding) { this.encoding = encoding; this.hasBuffer = (typeof Buffer != 'undefined'); if (this.hasBuffer && (value instanceof Buffer)) { this.value = value; this.encoding = GP.HEX; } else { switch (encoding) { case GP.HEX: if (!this.hasBuffer) { this.value = Convert.hexToBin(value); } else { this.value = new Buffer(value, 'hex'); } break; case GP.ASCII: if (!this.hasBuffer) { this.value = value; } else { this.value = new Buffer(value, 'ascii'); } break; default: throw "Invalid arguments"; } } this.length = this.value.length; } /** * Retrieve the byte value at the given index * @param {Number} index index * @returns {Number} byte value */ ByteString.prototype.byteAt = function(index) { if (arguments.length < 1) { throw "Argument missing"; } if (typeof index != "number") { throw "Invalid index"; } if ((index < 0) || (index >= this.value.length)) { throw "Invalid index offset"; } if (!this.hasBuffer) { return Convert.readHexDigit(Convert.stringToHex(this.value.substring(index, index + 1))); } else { return this.value[index]; } } /** * Retrieve a subset of the ByteString * @param {Number} offset offset to start at * @param {Number} [count] size of the target ByteString (default : use the remaining length) * @returns {ByteString} subset of the original ByteString */ ByteString.prototype.bytes = function(offset, count) { var result; if (arguments.length < 1) { throw "Argument missing"; } if (typeof offset != "number") { throw "Invalid offset"; } //if ((offset < 0) || (offset >= this.value.length)) { if (offset < 0) { throw "Invalid offset"; } if (typeof count == "number") { if (count < 0) { throw "Invalid count"; } if (!this.hasBuffer) { result = new ByteString(this.value.substring(offset, offset + count), GP.ASCII); } else { result = new Buffer(count); this.value.copy(result, 0, offset, offset + count); } } else if (typeof count == "undefined") { if (!this.hasBuffer) { result = new ByteString(this.value.substring(offset), GP.ASCII); } else { result = new Buffer(this.value.length - offset); this.value.copy(result, 0, offset, this.value.length); } } else { throw "Invalid count"; } if (!this.hasBuffer) { result.encoding = this.encoding; return result; } else { return new ByteString(result, GP.HEX); } } /** * Appends two ByteString * @param {ByteString} target ByteString to append * @returns {ByteString} result of the concatenation */ ByteString.prototype.concat = function(target) { if (arguments.length < 1) { throw "Not enough arguments"; } if (!(target instanceof ByteString)) { throw "Invalid argument"; } if (!this.hasBuffer) { var result = this.value + target.value; var x = new ByteString(result, GP.ASCII); x.encoding = this.encoding; return x; } else { var result = Buffer.concat([this.value, target.value]); return new ByteString(result, GP.HEX); } } /** * Check if two ByteString are equal * @param {ByteString} target ByteString to check against * @returns {Boolean} true if the two ByteString are equal */ ByteString.prototype.equals = function(target) { if (arguments.length < 1) { throw "Not enough arguments"; } if (!(target instanceof ByteString)) { throw "Invalid argument"; } if (!this.hasBuffer) { return (this.value == target.value); } else { return Buffer.equals(this.value, target.value); } } /** * Convert the ByteString to a String using the given encoding * @param {HEX|ASCII|UTF8|BASE64|CN} encoding encoding to use * @return {String} converted content */ ByteString.prototype.toString = function(encoding) { var targetEncoding = this.encoding; if (arguments.length >= 1) { if (typeof encoding != "number") { throw "Invalid encoding"; } switch (encoding) { case GP.HEX: case GP.ASCII: targetEncoding = encoding; break; default: throw "Unsupported arguments"; } targetEncoding = encoding; } switch (targetEncoding) { case GP.HEX: if (!this.hasBuffer) { return Convert.stringToHex(this.value); } else { return this.value.toString('hex'); } case GP.ASCII: if (!this.hasBuffer) { return this.value; } else { return this.value.toString(); } default: throw "Unsupported"; } } ByteString.prototype.toStringIE = function(encoding) { return this.toString(encoding); } ByteString.prototype.toBuffer = function() { return this.value; }