UNPKG

ledger-qrl-js

Version:

Node API for Ledger Nano S - QRL app

549 lines (479 loc) 18.8 kB
/******************************************************************************** * Ledger Node JS API * (c) 2016-2017 Ledger * (c) 2018 ZondaX GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ********************************************************************************/ 'use strict'; let Q = require('q'); let QRL = require('./ledger-qrl-consts'); let LedgerQrl = function (comm) { this.comm = comm; this.comm.setScrambleKey('QRL'); }; function serialize(CLA, INS, p1 = 0, p2 = 0, data = null) { var size = 5; if (data != null) { if (data.length > 255) { throw new Error('maximum data size = 255'); } size += data.length } var buffer = Buffer.alloc(size); buffer[0] = CLA; buffer[1] = INS; buffer[2] = p1; buffer[3] = p2; buffer[4] = 0; if (data != null) { buffer[4] = data.length; buffer.set(data, 5); } return buffer; } function concatenateTypedArrays (resultConstructor, ...arrays) { let totalLength = 0 for (let arr of arrays) { totalLength += arr.length } let result = new resultConstructor(totalLength) let offset = 0 for (let arr of arrays) { result.set(arr, offset) offset += arr.length } return result } function errorMessage(error_code) { switch (error_code) { case 14: return "Timeout"; case 0x9000: return "No errors"; case 0x9001: return "Device is busy"; case 0x6400: return "Execution Error"; case 0x6700: return "Wrong Length"; case 0x6982: return "Empty Buffer"; case 0x6983: return "Output buffer too small"; case 0x6984: return "Data is invalid"; case 0x6985: return "Conditions not satisfied"; case 0x6986: return "Command not allowed"; case 0x6A80: return "Bad key handle"; case 0x6B00: return "Invalid P1/P2"; case 0x6D00: return "Instruction not supported"; case 0x6E00: return "CLA not supported"; case 0x6F00: return "Unknown error"; case 0x6F01: return "Sign/verify error"; default: return "Unknown error code"; } } LedgerQrl.prototype.get_version = function () { let buffer = serialize( QRL.CLA, QRL.INS_VERSION, 0, 0); return this.comm.exchange(buffer.toString("hex"), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["test_mode"] = apduResponse[0] !== 0; result["major"] = apduResponse[1]; result["minor"] = apduResponse[2]; result["patch"] = apduResponse[3]; result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.get_state = function () { let buffer = serialize( QRL.CLA, QRL.INS_GETSTATE, 0, 0); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["state"] = apduResponse[0]; // 0 - Not ready, 1 - generating keys, 2 = ready result["xmss_index"] = apduResponse[2] + apduResponse[1] * 256; result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.publickey = function () { let buffer = serialize( QRL.CLA, QRL.INS_PUBLIC_KEY); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["public_key"] = apduResponse.slice(0, apduResponse.length - 2); result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.signSend = function (message) { let buffer = serialize( QRL.CLA, QRL.INS_SIGN, 0, 0, message); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["signature_chunk"] = apduResponse.slice(0, apduResponse.length - 2); result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.signNext = function () { let buffer = serialize( QRL.CLA, QRL.INS_SIGN_NEXT); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["signature_chunk"] = apduResponse.slice(0, apduResponse.length - 2); result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); console.log('apduResponse') console.log(apduResponse) console.log(result) return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.setIdx = function (idx) { if (idx < 0 || idx > 255) { let result = {}; result["return_code"] = 0x6984; result["error_message"] = errorMessage(result["return_code"]); return result; } let buffer = serialize( QRL.CLA, QRL.INS_SETIDX, 0, 0, [idx]); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.viewAddress = function () { let buffer = serialize( QRL.CLA, QRL.INS_VIEW_ADDRESS, 0, 0, []); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { let result = {}; // Unfortunately, ledger returns an string!! :( result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; }); }; LedgerQrl.prototype.test_comm = function (count) { let buffer = serialize( QRL.CLA, QRL.INS_TEST_COMM, count); return this.comm.exchange(buffer.toString('hex'), [0x9000]).then( function (apduResponse) { var result = {}; apduResponse = Buffer.from(apduResponse, 'hex'); let error_code_data = apduResponse.slice(-2); result["test_answer"] = apduResponse.slice(0, apduResponse.length - 2); result["return_code"] = error_code_data[0] * 256 + error_code_data[1]; result["error_message"] = errorMessage(result["return_code"]); return result; }, function (response) { // Error handling let result = {}; if(typeof response.errorCode !== 'undefined') { if(response.errorCode === 5) { // Timeout result["return_code"] = 14; result["error_message"] = errorMessage(result["return_code"]); return result; } else { // Error result["return_code"] = response.errorCode; result["error_message"] = errorMessage(result["return_code"]); return result; } } else { // Error result["return_code"] = parseInt(response.slice(-4), 16); result["error_message"] = errorMessage(result["return_code"]); return result; } }); }; LedgerQrl.prototype.retrieveSignature = async function (transaction) { let myqrl = this; return myqrl.signSend(transaction).then(async function (result) { let response = {}; response["return_code"] = result.return_code; response["error_message"] = result.error_message; response["signature"] = null; if (result.return_code === QRL.APDU_ERROR_CODE_OK) { let signature = new Uint8Array() for (let i = 0; i < 11; i++) { result = await myqrl.signNext(); if (result.return_code !== QRL.APDU_ERROR_CODE_OK) { response["return_code"] = result.return_code; response["error_message"] = result.error_message; break; } signature = concatenateTypedArrays( Uint8Array, signature, result.signature_chunk ); response = result; } response["return_code"] = result.return_code; response["error_message"] = result.error_message; response["signature"] = signature; } return response; }) }; LedgerQrl.prototype.createTx = function (source_address, fee, dest_addresses, dest_amounts) { // https://github.com/ZondaX/ledger-qrl-app/src/lib/qrl_types.h // Verify that sizes are valid if (source_address.length !== QRL.P_TX_ADDRESS_SIZE) { throw Error("Source address length invalid") } if (fee.length !== 8) { throw Error("fee should be 8 bytes") } if (dest_addresses.length !== dest_amounts.length) { throw Error("dest addresses and amount should have the same number of items") } if (dest_addresses.length > 3) { throw Error("maximum supported number of destinations is 3") } for (let i = 0; i < dest_addresses.length; i++) { if (dest_addresses[i].length !== QRL.P_TX_ADDRESS_SIZE) { throw Error("Destination address length invalid") } if (dest_amounts[i].length !== 8) { throw Error("each dest_amount should be 8 bytes") } } // Define buffer size var num_dest = dest_addresses.length; let tx = Buffer.alloc(2 + 47 * (1 + num_dest)); tx[QRL.P_TX_TYPE] = QRL.QRLTX_TX; tx[QRL.P_TX_NUM_DEST] = num_dest; source_address.copy(tx, QRL.P_TX_SRC_ADDR); fee.copy(tx, QRL.P_TX_SRC_FEE); let offset = QRL.P_TX_DEST; for (let i = 0; i < dest_addresses.length; i++) { dest_addresses[i].copy(tx, offset); offset += QRL.P_TX_ADDRESS_SIZE; dest_amounts[i].copy(tx, offset); offset += 8; } return tx; }; LedgerQrl.prototype.createMessageTx = function (source_address, fee, message) { // https://github.com/ZondaX/ledger-qrl-app/src/lib/qrl_types.h // Verify that sizes are valid if (source_address.length !== QRL.P_TX_ADDRESS_SIZE) { throw Error("Source address length invalid") } if (fee.length !== 8) { throw Error("fee should be 8 bytes") } if (message.length > QRL.P_TX_MAX_MESSAGE_SIZE) { throw Error("Message length exceed maximum size") } // Define buffer size let messageLength = message.length; let tx = Buffer.alloc(2 + 47 + (messageLength)); tx[QRL.P_TX_TYPE] = QRL.QRLTX_MESSAGE; tx[QRL.P_TX_NUM_DEST] = messageLength; source_address.copy(tx, QRL.P_TX_SRC_ADDR); fee.copy(tx, QRL.P_TX_SRC_FEE); message.copy(tx, 49) return tx; }; module.exports = LedgerQrl;