UNPKG

@ledgerhq/hw-transport

Version:
344 lines 15.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAltStatusMessage = exports.StatusCodes = exports.TransportStatusError = exports.TransportError = void 0; const events_1 = __importDefault(require("events")); const errors_1 = require("@ledgerhq/errors"); Object.defineProperty(exports, "TransportError", { enumerable: true, get: function () { return errors_1.TransportError; } }); Object.defineProperty(exports, "StatusCodes", { enumerable: true, get: function () { return errors_1.StatusCodes; } }); Object.defineProperty(exports, "getAltStatusMessage", { enumerable: true, get: function () { return errors_1.getAltStatusMessage; } }); Object.defineProperty(exports, "TransportStatusError", { enumerable: true, get: function () { return errors_1.TransportStatusError; } }); const logs_1 = require("@ledgerhq/logs"); const DEFAULT_LOG_TYPE = "transport"; /** * The Transport class defines a generic interface for communicating with a Ledger hardware wallet. * There are different kind of transports based on the technology (channels like U2F, HID, Bluetooth, Webusb) and environment (Node, Web,...). * It is an abstract class that needs to be implemented. */ class Transport { exchangeTimeout = 30000; unresponsiveTimeout = 15000; deviceModel = null; tracer; constructor({ context, logType } = {}) { this.tracer = new logs_1.LocalTracer(logType ?? DEFAULT_LOG_TYPE, context); } /** * Check if the transport is supported on the current platform/browser. * @returns {Promise<boolean>} A promise that resolves with a boolean indicating support. */ static isSupported; /** * List all available descriptors for the transport. * For a better granularity, checkout `listen()`. * * @returns {Promise<Array<any>>} A promise that resolves with an array of descriptors. * @example * TransportFoo.list().then(descriptors => ...) */ static list; /** * Listen for device events for the transport. The method takes an observer of DescriptorEvent and returns a Subscription. * A DescriptorEvent is an object containing a "descriptor" and a "type" field. The "type" field can be "add" or "remove", and the "descriptor" field can be passed to the "open" method. * The "listen" method will first emit all currently connected devices and then will emit events as they occur, such as when a USB device is plugged in or a Bluetooth device becomes discoverable. * @param {Observer<DescriptorEvent<any>>} observer - An object with "next", "error", and "complete" functions, following the observer pattern. * @returns {Subscription} A Subscription object on which you can call ".unsubscribe()" to stop listening to descriptors. * @example const sub = TransportFoo.listen({ next: e => { if (e.type==="add") { sub.unsubscribe(); const transport = await TransportFoo.open(e.descriptor); ... } }, error: error => {}, complete: () => {} }) */ static listen; /** * Attempt to create a Transport instance with a specific descriptor. * @param {any} descriptor - The descriptor to open the transport with. * @param {number} timeout - An optional timeout for the transport connection. * @param {TraceContext} context Optional tracing/log context * @returns {Promise<Transport>} A promise that resolves with a Transport instance. * @example TransportFoo.open(descriptor).then(transport => ...) */ static open; /** * Send data to the device using a low level API. * It's recommended to use the "send" method for a higher level API. * @param {Buffer} apdu - The data to send. * @param {Object} options - Contains optional options for the exchange function * - abortTimeoutMs: stop the exchange after a given timeout. Another timeout exists * to detect unresponsive device (see `unresponsiveTimeout`). This timeout aborts the exchange. * @returns {Promise<Buffer>} A promise that resolves with the response data from the device. */ exchange(_apdu, { abortTimeoutMs: _abortTimeoutMs } = {}) { throw new Error("exchange not implemented"); } /** * Send apdus in batch to the device using a low level API. * The default implementation is to call exchange for each apdu. * @param {Array<Buffer>} apdus - array of apdus to send. * @param {Observer<Buffer>} observer - an observer that will receive the response of each apdu. * @returns {Subscription} A Subscription object on which you can call ".unsubscribe()" to stop sending apdus. */ exchangeBulk(apdus, observer) { let unsubscribed = false; const unsubscribe = () => { unsubscribed = true; }; const main = async () => { if (unsubscribed) return; for (const apdu of apdus) { const r = await this.exchange(apdu); if (unsubscribed) return; const status = r.readUInt16BE(r.length - 2); if (status !== errors_1.StatusCodes.OK) { throw new errors_1.TransportStatusError(status); } observer.next(r); } }; main().then(() => !unsubscribed && observer.complete(), e => !unsubscribed && observer.error(e)); return { unsubscribe }; } /** * Set the "scramble key" for the next data exchanges with the device. * Each app can have a different scramble key and it is set internally during instantiation. * @param {string} key - The scramble key to set. * deprecated This method is no longer needed for modern transports and should be migrated away from. * no @ before deprecated as it breaks documentationjs on version 14.0.2 * https://github.com/documentationjs/documentation/issues/1596 */ setScrambleKey(_key) { } /** * Close the connection with the device. * * Note: for certain transports (hw-transport-node-hid-singleton for ex), once the promise resolved, * the transport instance is actually still cached, and the device is disconnected only after a defined timeout. * But for the consumer of the Transport, this does not matter and it can consider the transport to be closed. * * @returns {Promise<void>} A promise that resolves when the transport is closed. */ close() { return Promise.resolve(); } _events = new events_1.default(); /** * Listen for an event on the transport instance. * Transport implementations may have specific events. Common events include: * "disconnect" : triggered when the transport is disconnected. * @param {string} eventName - The name of the event to listen for. * @param {(...args: Array<any>) => any} cb - The callback function to be invoked when the event occurs. */ on(eventName, cb) { this._events.on(eventName, cb); } /** * Stop listening to an event on an instance of transport. */ off(eventName, cb) { this._events.removeListener(eventName, cb); } emit(event, ...args) { this._events.emit(event, ...args); } /** * Enable or not logs of the binary exchange */ setDebugMode() { console.warn("setDebugMode is deprecated. use @ledgerhq/logs instead. No logs are emitted in this anymore."); } /** * Set a timeout (in milliseconds) for the exchange call. Only some transport might implement it. (e.g. U2F) */ setExchangeTimeout(exchangeTimeout) { this.exchangeTimeout = exchangeTimeout; } /** * Define the delay before emitting "unresponsive" on an exchange that does not respond */ setExchangeUnresponsiveTimeout(unresponsiveTimeout) { this.unresponsiveTimeout = unresponsiveTimeout; } /** * Send data to the device using the higher level API. * * @param {number} cla - The instruction class for the command. * @param {number} ins - The instruction code for the command. * @param {number} p1 - The first parameter for the instruction. * @param {number} p2 - The second parameter for the instruction. * @param {Buffer} data - The data to be sent. Defaults to an empty buffer. * @param {Array<number>} statusList - A list of acceptable status codes for the response. Defaults to [StatusCodes.OK]. * @param {Object} options - Contains optional options for the exchange function * - abortTimeoutMs: stop the send after a given timeout. Another timeout exists * to detect unresponsive device (see `unresponsiveTimeout`). This timeout aborts the exchange. * @returns {Promise<Buffer>} A promise that resolves with the response data from the device. */ send = async (cla, ins, p1, p2, data = Buffer.alloc(0), statusList = [errors_1.StatusCodes.OK], { abortTimeoutMs } = {}) => { const tracer = this.tracer.withUpdatedContext({ function: "send" }); if (data.length >= 256) { tracer.trace("data.length exceeded 256 bytes limit", { dataLength: data.length }); throw new errors_1.TransportError("data.length exceed 256 bytes limit. Got: " + data.length, "DataLengthTooBig"); } const response = await this.exchange( // The size of the data is added in 1 byte just before `data` Buffer.concat([Buffer.from([cla, ins, p1, p2]), Buffer.from([data.length]), data]), { abortTimeoutMs }); const sw = response.readUInt16BE(response.length - 2); if (!statusList.some(s => s === sw)) { throw new errors_1.TransportStatusError(sw); } return response; }; /** * create() allows to open the first descriptor available or * throw if there is none or if timeout is reached. * This is a light helper, alternative to using listen() and open() (that you may need for any more advanced usecase) * @example TransportFoo.create().then(transport => ...) */ static create(openTimeout = 3000, listenTimeout) { return new Promise((resolve, reject) => { let found = false; const sub = this.listen({ next: e => { found = true; if (sub) sub.unsubscribe(); if (listenTimeoutId) clearTimeout(listenTimeoutId); this.open(e.descriptor, openTimeout).then(resolve, reject); }, error: e => { if (listenTimeoutId) clearTimeout(listenTimeoutId); reject(e); }, complete: () => { if (listenTimeoutId) clearTimeout(listenTimeoutId); if (!found) { reject(new errors_1.TransportError(this.ErrorMessage_NoDeviceFound, "NoDeviceFound")); } }, }); const listenTimeoutId = listenTimeout ? setTimeout(() => { sub.unsubscribe(); reject(new errors_1.TransportError(this.ErrorMessage_ListenTimeout, "ListenTimeout")); }, listenTimeout) : null; }); } // Blocks other exchange to happen concurrently exchangeBusyPromise; /** * Wrapper to make an exchange "atomic" (blocking any other exchange) * * It also handles "unresponsiveness" by emitting "unresponsive" and "responsive" events. * * @param f The exchange job, using the transport to run * @returns a Promise resolving with the output of the given job */ async exchangeAtomicImpl(f) { const tracer = this.tracer.withUpdatedContext({ function: "exchangeAtomicImpl", unresponsiveTimeout: this.unresponsiveTimeout, }); if (this.exchangeBusyPromise) { tracer.trace("Atomic exchange is already busy"); throw new errors_1.TransportRaceCondition("An action was already pending on the Ledger device. Please deny or reconnect."); } // Sets the atomic guard let resolveBusy; const busyPromise = new Promise(r => { resolveBusy = r; }); this.exchangeBusyPromise = busyPromise; // The device unresponsiveness handler let unresponsiveReached = false; const timeout = setTimeout(() => { tracer.trace(`Timeout reached, emitting Transport event "unresponsive"`, { unresponsiveTimeout: this.unresponsiveTimeout, }); unresponsiveReached = true; this.emit("unresponsive"); }, this.unresponsiveTimeout); try { const res = await f(); if (unresponsiveReached) { tracer.trace("Device was unresponsive, emitting responsive"); this.emit("responsive"); } return res; } finally { tracer.trace("Finalize, clearing busy guard"); clearTimeout(timeout); if (resolveBusy) resolveBusy(); this.exchangeBusyPromise = null; } } decorateAppAPIMethods(self, methods, scrambleKey) { for (const methodName of methods) { self[methodName] = this.decorateAppAPIMethod(methodName, self[methodName], self, scrambleKey); } } _appAPIlock = null; decorateAppAPIMethod(methodName, f, ctx, scrambleKey) { return async (...args) => { const { _appAPIlock } = this; if (_appAPIlock) { return Promise.reject(new errors_1.TransportError("Ledger Device is busy (lock " + _appAPIlock + ")", "TransportLocked")); } try { this._appAPIlock = methodName; this.setScrambleKey(scrambleKey); return await f.apply(ctx, args); } finally { this._appAPIlock = null; } }; } /** * Sets the context used by the logging/tracing mechanism * * Useful when re-using (cached) the same Transport instance, * but with a new tracing context. * * @param context A TraceContext, that can undefined to reset the context */ setTraceContext(context) { this.tracer = this.tracer.withContext(context); } /** * Updates the context used by the logging/tracing mechanism * * The update only overrides the key-value that are already defined in the current context. * * @param contextToAdd A TraceContext that will be added to the current context */ updateTraceContext(contextToAdd) { this.tracer.updateContext(contextToAdd); } /** * Gets the tracing context of the transport instance */ getTraceContext() { return this.tracer.getContext(); } static ErrorMessage_ListenTimeout = "No Ledger device found (timeout)"; static ErrorMessage_NoDeviceFound = "No Ledger device found"; } exports.default = Transport; //# sourceMappingURL=Transport.js.map