Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HID support #8

Open
wants to merge 2 commits into
base: s-x-compat
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions examples/getAppVersion-hid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

const Logger = require('blgr');
const {HID, LedgerHSD} = require('../lib/hsd-ledger');
const {Device} = HID;

(async () => {
// Create logger.
const logger = new Logger({
console: true,
level: 'info'
});

// Get first device available and
// set optional properties.
const device = await Device.requestDevice();
device.set({
timeout: 15000, // optional (default is 5mins)
logger: logger // optional
});

// Create ledger client object.
// Note: network defaults to 'main'
const ledger = new LedgerHSD({ device, logger }); // logger optional

// Open logger and device.
await logger.open();
await device.open();

// Retrieve app version.
const version = await ledger.getAppVersion();
logger.info('Version: %s', version);

// Close logger and device.
await device.close();
await logger.close();
})().catch((e) => {
console.error(e);
process.exit(1);
});
16 changes: 14 additions & 2 deletions lib/device/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class Device {

/**
* Set device options.
* @param {!Object} options
* @see {@link Device}
* @param {Object} options
* @throws {AssertionError}
* @see {@link Device}
*/

set(options) {
Expand Down Expand Up @@ -97,6 +97,18 @@ class Device {
return new this(options);
}

/**
* Assertion
* @param {Boolean} value
* @param {String?} reason
* @throws {DeviceError}
*/

enforce(value, reason) {
if (!value)
throw new DeviceError(reason, Device);
}

/**
* Open connetion with device
* @returns {Promise}
Expand Down
306 changes: 306 additions & 0 deletions lib/device/hid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
/*!
* hid.js - Ledger HID communication
* Copyright (c) 2021, Nodari Chkuaselidze (MIT License).
* https://github.com/handshake-org/hsd-ledger
*/

'use strict';

const assert = require('bsert');
const Logger = require('blgr');
const {Lock} = require('bmutex');
const NHID = require('node-hid');
const APDU = require('../apdu');
const {APDUWriter, APDUReader} = APDU;
const DeviceError = require('./error');
const {Device} = require('./device');

/**
* Ledger HID PacketSize
*/

const PACKET_SIZE = 64;

class HID extends Device {
constructor(options) {
super();

this.lock = new Lock(false);
this.type = 'hid';

this.device = null;
this.devicePath = null;

this.opened = false;

if (options)
this.set(options);
}

/**
* Set device options.
* @param {Object} options
*/

set(options) {
super.set(options);

if (options.device != null) {
if (typeof options.device === 'string')
this.devicePath = options.device;

if (typeof options.device === 'object')
this.devicePath = options.device.path;

assert(this.devicePath, 'Could not set device.');
}

if (options.path != null) {
assert(typeof options.path === 'string');
this.devicePath = options.path;
}

if (options.vendorId != null) {
assert(typeof options.vendorId === 'number');
this.vendorId = options.vendorId;
}

if (options.productId != null) {
assert(typeof options.productId === 'number');
this.productId = options.productId;
}

if (options.product != null) {
assert(typeof options.product === 'string');
this.productName = options.product;
}

if (options.manufacturer != null) {
assert(typeof options.manufacturer === 'string');
this.manufacturerName = options.manufacturer;
}

if (options.serialNumber != null) {
assert(typeof options.serialNumber === 'string');
this.serialNumber = options.serialNumber;
}
}

/**
* Assertion
* @param {Boolean} value
* @param {String?} reason
* @throws {DeviceError}
*/

enforce(value, reason) {
if (!value)
throw new DeviceError(reason, HID);
}

async open() {
this.enforce(this.opened === false, 'Device is already open.');
this.enforce(this.devicePath, 'Device is not configured.');

this.device = new NHID.HID(this.devicePath);
this.opened = true;

this.logger.info('Device is open.');
}

async close() {
this.enforce(this.opened === true, 'Device is already closed.');
this.enforce(this.device !== null, 'Can not find device.');

this.device.close();
this.device = null;
this.opened = false;

this.logger.info('Device is closed.');
}

/**
* Pads the buffer to PACKET_SIZE
* @private
* @param {Buffer} message
* @returns {Buffer} - padded
*/

_padMessage(message) {
const paddedMessage = Buffer.alloc(PACKET_SIZE);

message.copy(paddedMessage);
return paddedMessage;
}

/**
* Write device data
* @private
* @param {Buffer} data
* @returns {Promise}
*/

_write(data) {
return new Promise((resolve, reject) => {
setImmediate(() => {
const level = this.logger.logger.level;

if (level >= Logger.levels.DEBUG)
this.logger.debug('==>', data.toString('hex'));

const array = [0x00];

for (const val of data.values())
array.push(val);

// this.device.write - is synchronous.
resolve(this.device.write(array));
});
});
}

/**
* Read device data
* @private
* @returns {Promise}
*/

async _read() {
return new Promise((resolve, reject) => {
this.device.read((err, data) => {
if (err || !data) {
reject(err);
return;
}

data = Buffer.from(data);

const level = this.logger.logger.level;
if (level >= Logger.levels.DEBUG)
this.logger.spam('<==', data.toString('hex'));

resolve(data);
});
});
}

/**
* Exchange APDU commands with device
* Lock
* @param {Buffer} apdu
* @returns {Promise<Buffer>} - Response data
* @throws {LedgerError}
*/

async exchange(apdu) {
const unlock = await this.lock.lock();

try {
return await this._exchange(apdu);
} finally {
unlock();
}
}

/**
* Exchange APDU command with device
* without lock
* @param {Buffer} apdu
* @returns {Promise<Buffer>} - Response data
* @throws {LedgerError}
*/

async _exchange(apdu) {
this.enforce(this.opened === true, 'Connection is not open');

const writer = new APDUWriter({
channelID: APDU.CHANNEL_ID,
tag: APDU.TAG_APDU,
data: apdu,
packetSize: PACKET_SIZE
});

const reader = new APDUReader({
channelID: APDU.CHANNEL_ID,
tag: APDU.TAG_APDU,
packetSize: PACKET_SIZE
});

const messages = writer.toMessages();

for (const message of messages)
await this._write(this._padMessage(message));

while (!reader.finished) {
const data = await this._readTimeout();

reader.pushMessage(data);
}

return reader.getData();
}

/**
* Set options from node-hid options.
* @param {Object} options
* @returns {HID}
*/

fromHIDInfo(options) {
this.set(options);
return this;
}

/**
* Get HID device from node-hid options.
* @param {Object} options
* @returns {HID}
*/

static fromHIDInfo(options) {
const hid = new HID();
return hid.fromHIDInfo(options);
}

/**
* Get Ledger HID devices
* @returns {Promise<Object[]>}
*/

static async getDevices() {
const allDevices = NHID.devices();
const devices = [];

for (const device of allDevices) {
if (this.isLedgerDevice(device))
devices.push(device);
}

return devices;
}

/**
* Select first device
* @param {Promise<HID>}
*/

static async requestDevice() {
const devices = await this.getDevices();
assert(devices.length > 0, 'Could not find a device.');

return this.fromHIDInfo(devices[0]);
}

static isLedgerDevice(deviceOptions) {
if (process.platform === 'win32' || process.platform === 'darwin') {
if (deviceOptions.usagePage !== 0xffa0)
return false;
} else if (deviceOptions.interface !== 0) {
return false;
}

return deviceOptions.vendorId === 0x2c97;
}
}

exports.Device = HID;
2 changes: 1 addition & 1 deletion lib/device/usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ else
const assert = require('bsert');
const Logger = require('blgr');
const {Lock} = require('bmutex');
const DeviceError = require('../device/error');
const DeviceError = require('./error');
const APDU = require('../apdu');
const {APDUWriter, APDUReader} = APDU;
const {Device} = require('./device');
Expand Down
Loading