ledger-qrl-js
Version:
Node API for Ledger Nano S - QRL app
366 lines (337 loc) • 14.5 kB
JavaScript
/********************************************************************************
* Ledger Node JS API
* (c) 2016-2017 Ledger
*
* 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.
********************************************************************************/
var HID = require('node-hid');
var Q = require('q');
var LedgerNode = function(device, ledgerTransport, timeout, debug) {
if (typeof timeout == "undefined") {
timeout = 0;
}
this.device = device;
this.ledgerTransport = ledgerTransport;
this.timeout = timeout;
this.exchangeStack = [];
this.debug = debug;
}
LedgerNode.list_async = function() {
var devices = HID.devices();
var deviceList = [];
for (var i in devices) {
if ((devices[i].vendorId == 0x2581) && (devices[i].productId == 0x3b7c)) {
deviceList.push(devices[i].path);
}
if ((devices[i].vendorId == 0x2c97) && (devices[i].usagePage == 65440)) {
deviceList.push(devices[i].path);
} else if (devices[i].vendorId == 0x2c97) {
// Resolve first HID path of composite ledger device across all platforms.
if (process.platform === 'darwin') {
// Don't add devices if their path ends in 1
let lastChar = devices[i].path[devices[i].path.length - 1];
if (lastChar != '1') {
deviceList.push(devices[i].path);
}
} else if (process.platform === 'linux') {
// Don't add devices if their path ends in 2
let lastChar = devices[i].path[devices[i].path.length - 1];
if (lastChar != '2') {
deviceList.push(devices[i].path);
}
} else if (process.platform === 'win32') {
// Only add HID paths that contain mi_00
if (devices[i].path.indexOf('mi_00') > -1) {
deviceList.push(devices[i].path);
}
}
}
}
return Q.fcall(function() {
return deviceList;
});
}
LedgerNode.prototype.exchange = function(apduHex, statusList) {
var ledgerWrap = function(channel, command, packetSize) {
var sequenceIdx = 0;
var offset = 0;
var tmp = Buffer.alloc(7);
tmp.writeUInt16BE(channel, 0);
tmp[2] = 0x05; // TAG_APDU
tmp.writeUInt16BE(sequenceIdx, 3);
sequenceIdx++;
tmp.writeUInt16BE(command.length, 5);
var blockSize = (command.length > packetSize - 7 ? packetSize - 7 : command.length);
var result = Buffer.concat([tmp, command.slice(offset, offset + blockSize)], blockSize + 7);
offset += blockSize;
while (offset != command.length) {
tmp = Buffer.alloc(5);
tmp.writeUInt16BE(channel, 0);
tmp[2] = 0x05; // TAG_APDU
tmp.writeUInt16BE(sequenceIdx, 3);
sequenceIdx++;
blockSize = (command.length - offset > packetSize - 5 ? packetSize - 5 : command.length - offset);
result = Buffer.concat([result, tmp, command.slice(offset, offset + blockSize)], result.length + blockSize + 5);
offset += blockSize;
}
return result;
}
var ledgerUnwrap = function(channel, data, packetSize) {
var offset = 0;
var responseLength;
var sequenceIdx = 0;
var response;
if ((typeof data == "undefined") || (data.length < 7 + 5)) {
return;
}
if (data[offset++] != (channel >> 8)) {
throw "Invalid channel;"
}
if (data[offset++] != (channel & 0xff)) {
throw "Invalid channel";
}
if (data[offset++] != 0x05) {
throw "Invalid tag";
}
if (data[offset++] != 0x00) {
throw "Invalid sequence";
}
if (data[offset++] != 0x00) {
throw "Invalid sequence";
}
responseLength = ((data[offset++] & 0xff) << 8);
responseLength |= (data[offset++] & 0xff);
if (data.length < 7 + responseLength) {
return;
}
var blockSize = (responseLength > packetSize - 7 ? packetSize - 7 : responseLength);
response = data.slice(offset, offset + blockSize);
offset += blockSize;
while (response.length != responseLength) {
sequenceIdx++;
if (offset == data.length) {
return;
}
if (data[offset++] != (channel >> 8)) {
throw "Invalid channel;"
}
if (data[offset++] != (channel & 0xff)) {
throw "Invalid channel";
}
if (data[offset++] != 0x05) {
throw "Invalid tag";
}
if (data[offset++] != (sequenceIdx >> 8)) {
throw "Invalid sequence";
}
if (data[offset++] != (sequenceIdx & 0xff)) {
throw "Invalid sequence";
}
blockSize = (responseLength - response.length > packetSize - 5 ? packetSize - 5 : responseLength - response.length);
if (blockSize > data.length - offset) {
return;
}
response = Buffer.concat([response, data.slice(offset, offset + blockSize)], response.length + blockSize);
offset += blockSize;
}
return response;
}
var currentObject = this;
var apdu = Buffer.from(apduHex, 'hex');
var deferred = Q.defer();
var exchangeTimeout;
deferred.promise.apdu = apdu;
if (!this.ledgerTransport) {
deferred.promise.transport = apdu;
}
else {
deferred.promise.transport = ledgerWrap(0x0101, apdu, 64);
}
if (this.timeout != 0) {
exchangeTimeout = setTimeout(function() { // Node.js supports timeouts
deferred.reject("timeout");
}, this.timeout);
}
// enter the exchange wait list
currentObject.exchangeStack.push(deferred);
if (currentObject.exchangeStack.length == 1) {
var processNextExchange = function() {
// don't pop it now, to avoid multiple at once
var deferred = currentObject.exchangeStack[0];
var send_async = function(cardObject, content) {
if (cardObject.debug) {
console.log('=>' + content.toString('hex'));
}
var data = [ 0x00 ];
for (var i=0; i<content.length; i++) {
data.push(content[i]);
}
cardObject.device.write(data);
return Q.fcall(function() {
return content.length;
});
}
var recv_async = function(cardObject, size) {
return Q.ninvoke(cardObject.device, "read").then(function(res) {
var buffer = Buffer.from(res);
if (cardObject.debug) {
console.log('<=' + buffer.toString('hex'));
}
return buffer;
});
}
var performExchange = function() {
var deferredHidSend = Q.defer();
var offsetSent = 0;
var firstReceived = true;
var toReceive = 0;
var received = Buffer.alloc(0);
var sendPart = function() {
if (offsetSent == deferred.promise.transport.length) {
return receivePart();
}
var blockSize = (deferred.promise.transport.length - offsetSent > 64 ? 64 : deferred.promise.transport.length - offsetSent);
var block = deferred.promise.transport.slice(offsetSent, offsetSent + blockSize);
var paddingSize = 64 - block.length;
if (paddingSize != 0) {
var padding = Buffer.alloc(paddingSize).fill(0);
block = Buffer.concat([block, padding], block.length + paddingSize);
}
return send_async(currentObject, block).then(
function(result) {
offsetSent += blockSize;
return sendPart();
}
).fail(function(error) {
deferredHidSend.reject(error);
});
}
var receivePart = function() {
if (!currentObject.ledgerTransport) {
return recv_async(currentObject, 64).then(function(result) {
received = Buffer.concat([received, result], received.length + result.length);
if (firstReceived) {
firstReceived = false;
if ((received.length == 2) || (received[0] != 0x61)) {
deferredHidSend.resolve(received);
}
else {
toReceive = received[1];
if (toReceive == 0) {
toReceive == 256;
}
toReceive += 2;
}
}
if (toReceive < 64) {
deferredHidSend.resolve(received);
}
else {
toReceive -= 64;
return receivePart();
}
}).fail(function(error) {
deferredHidSend.reject(error);
});
}
else {
return recv_async(currentObject, 64).then(function(result) {
received = Buffer.concat([received, result], received.length + result.length);
var response = ledgerUnwrap(0x0101, received, 64);
if (typeof response != "undefined") {
deferredHidSend.resolve(response);
}
else {
return receivePart();
}
}).fail(function(error) {
deferredHidSend.reject(error);
});
}
}
sendPart();
return deferredHidSend.promise;
}
performExchange().then(function(result) {
var resultBin = result;
if (!currentObject.ledgerTransport) {
if (resultBin.length == 2 || resultBin[0] != 0x61) {
status = (resultBin[0] << 8) | (resultBin[1]);
deferred.promise.response = resultBin.toString('hex');
}
else {
var size = resultBin.byteAt(1);
// fake T0
if (size == 0) { size = 256; }
deferred.promise.response = resultBin.toString('hex', 2);
status = (resultBin[2 + size] << 8) | (resultBin[2 + size + 1]);
}
}
else {
deferred.promise.response = resultBin.toString('hex');
status = (resultBin[resultBin.length - 2] << 8) | (resultBin[resultBin.length - 1]);
}
// Check the status
var statusFound = false;
for (var index in statusList) {
if (statusList[index] == status) {
statusFound = true;
break;
}
}
if (!statusFound) {
deferred.reject("Invalid status " + status.toString(16));
}
// build the response
if (currentObject.timeout != 0) {
clearTimeout(exchangeTimeout);
}
deferred.resolve(deferred.promise.response);
})
.fail(function(err) {
if (currentObject.timeout != 0) {
clearTimeout(exchangeTimeout);
}
deferred.reject(err);
})
.finally(function () {
// consume current promise
currentObject.exchangeStack.shift();
// schedule next exchange
if (currentObject.exchangeStack.length > 0) {
processNextExchange();
}
});
}; //processNextExchange
// schedule next exchange
processNextExchange();
}
// the exchangeStack will process the promise when possible
return deferred.promise;
}
LedgerNode.prototype.setScrambleKey = function(scrambleKey) {
}
LedgerNode.prototype.close_async = function() {
this.device.close();
return Q.fcall(function() {
});
}
LedgerNode.create_async = function(timeout, debug) {
return LedgerNode.list_async().then(function(result) {
if (result.length == 0) {
throw "No device found";
}
return new LedgerNode(new HID.HID(result[0]), true, timeout, debug);
});
}
module.exports = LedgerNode;