UNPKG

@zondax/ledger-js

Version:

TS / Node API for apps running on Ledger devices

250 lines 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const bip32_1 = require("./bip32"); const common_1 = require("./common"); const consts_1 = require("./consts"); const responseError_1 = require("./responseError"); /** * Base class for interacting with a Ledger device. */ class BaseApp { /** * Constructs a new BaseApp instance. * @param transport - The transport mechanism to communicate with the device. * @param params - The constructor parameters. */ constructor(transport, params) { if (transport == null) { throw new Error('Transport has not been defined'); } this.transport = transport; this.CLA = params.cla; this.INS = params.ins; this.P1_VALUES = params.p1Values; this.CHUNK_SIZE = params.chunkSize; this.REQUIRED_PATH_LENGTHS = params.acceptedPathLengths; this.CUSTOM_APP_ERROR_DESCRIPTION = params.customAppErrorDescription; } /** * Serializes a derivation path into a buffer. * @param path - The derivation path in string format. * @returns A buffer representing the serialized path. */ serializePath(path) { return (0, bip32_1.serializePath)(path, this.REQUIRED_PATH_LENGTHS); } /** * Prepares chunks of data to be sent to the device. * @param path - The derivation path. * @param message - The message to be sent. * @returns An array of buffers that are ready to be sent. */ prepareChunks(path, message) { const serializedPathBuffer = this.serializePath(path); const chunks = this.messageToChunks(message); chunks.unshift(serializedPathBuffer); return chunks; } /** * Splits a buffer into chunks of `this.CHUNK_SIZE` size. * @param message - The message to be chunked. * @returns An array of buffers, each representing a chunk of the original message. */ messageToChunks(message) { const chunks = []; const messageBuffer = Buffer.from(message); for (let i = 0; i < messageBuffer.length; i += this.CHUNK_SIZE) { const end = Math.min(i + this.CHUNK_SIZE, messageBuffer.length); chunks.push(messageBuffer.subarray(i, end)); } return chunks; } /** * Sends a chunk of data to the device and handles the response. * Determines the payload type based on the chunk index and sends the chunk to the device. * Processes the response from the device. * * @param ins - The instruction byte. * @param p2 - P2 parameter byte. * @param chunkIdx - The current chunk index. * @param chunkNum - The total number of chunks. * @param chunk - The chunk data as a buffer. * @returns A promise that resolves to the processed response from the device. * @throws {ResponseError} If the response from the device indicates an error. */ async sendGenericChunk(ins, p2, chunkIdx, chunkNum, chunk) { let payloadType = consts_1.PAYLOAD_TYPE.ADD; if (chunkIdx === 1) { payloadType = consts_1.PAYLOAD_TYPE.INIT; } if (chunkIdx === chunkNum) { payloadType = consts_1.PAYLOAD_TYPE.LAST; } const statusList = [consts_1.LedgerError.NoErrors, consts_1.LedgerError.DataIsInvalid, consts_1.LedgerError.BadKeyHandle]; let responseBuffer; try { responseBuffer = await this.transport.send(this.CLA, ins, payloadType, p2, chunk, statusList); } catch (e) { // In case transport.send send throws an Error, we still want our custom ResponseError let statusCode = e.statusCode || e.returnCode; const buffer = Buffer.alloc(2); buffer.writeUInt16BE(statusCode, 0); return (0, common_1.processResponse)(buffer, this.CUSTOM_APP_ERROR_DESCRIPTION); } return (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION); } /** * Sends a chunk of data to the device and handles the response. * This method determines the payload type based on the chunk index and sends the chunk to the device. * It then processes the response from the device. * * @param ins - The instruction byte. * @param chunkIdx - The current chunk index. * @param chunkNum - The total number of chunks. * @param chunk - The chunk data as a buffer. * @returns A promise that resolves to the processed response from the device. * @throws {ResponseError} If the response from the device indicates an error. */ async signSendChunk(ins, chunkIdx, chunkNum, chunk) { return this.sendGenericChunk(ins, 0, chunkIdx, chunkNum, chunk); } /** * Retrieves the version information from the device. * @returns A promise that resolves to the version information. * @throws {ResponseError} If the response from the device indicates an error. */ async getVersion() { try { const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_VERSION, 0, 0); const response = (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION); // valid options are // test mode: 1 byte // major, minor, patch: 3 byte total // device locked: 1 byte // targetId: 4 bytes // total: 5 or 9 bytes // ----- // test mode: 1 byte // major, minor, patch: 6 byte total // device locked: 1 byte // targetId: 4 bytes // total: 8 or 12 bytes // ----- // test mode: 1 byte // major, minor, patch: 12 byte total // device locked: 1 byte // targetId: 4 bytes // total: 14 or 18 bytes let testMode; let major, minor, patch; if (response.length() === 5 || response.length() === 9) { testMode = response.readBytes(1).readUInt8() !== 0; major = response.readBytes(1).readUInt8(); minor = response.readBytes(1).readUInt8(); patch = response.readBytes(1).readUInt8(); } else if (response.length() === 8 || response.length() === 12) { testMode = response.readBytes(1).readUInt8() !== 0; major = response.readBytes(2).readUInt16BE(); minor = response.readBytes(2).readUInt16BE(); patch = response.readBytes(2).readUInt16BE(); } else if (response.length() === 14 || response.length() === 18) { testMode = response.readBytes(1).readUInt8() !== 0; major = response.readBytes(4).readUInt32BE(); minor = response.readBytes(4).readUInt32BE(); patch = response.readBytes(4).readUInt32BE(); } else { throw new responseError_1.ResponseError(consts_1.LedgerError.TechnicalProblem, 'Invalid response length'); } const deviceLocked = response.readBytes(1).readUInt8() === 1; let targetId = ''; if (response.length() >= 4) { targetId = response.readBytes(4).readUInt32BE().toString(16).padStart(8, '0'); } return { testMode, major, minor, patch, deviceLocked, targetId, }; } catch (error) { throw (0, common_1.processErrorResponse)(error); } } /** * Retrieves application information from the device. * @returns A promise that resolves to the application information. * @throws {ResponseError} If the response from the device indicates an error. */ async appInfo() { try { const responseBuffer = await this.transport.send(consts_1.LEDGER_DASHBOARD_CLA, 0x01, 0, 0); const response = (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION); const formatId = response.readBytes(1).readUInt8(); if (formatId !== 1) { throw new responseError_1.ResponseError(consts_1.LedgerError.TechnicalProblem, 'Format ID not recognized'); } const appNameLen = response.readBytes(1).readUInt8(); const appName = response.readBytes(appNameLen).toString('ascii'); const appVersionLen = response.readBytes(1).readUInt8(); const appVersion = response.readBytes(appVersionLen).toString('ascii'); const flagLen = response.readBytes(1).readUInt8(); const flagsValue = response.readBytes(flagLen).readUInt8(); return { appName, appVersion, flagLen, flagsValue, flagRecovery: (flagsValue & 1) !== 0, flagSignedMcuCode: (flagsValue & 2) !== 0, flagOnboarded: (flagsValue & 4) !== 0, flagPINValidated: (flagsValue & 128) !== 0, }; } catch (error) { throw (0, common_1.processErrorResponse)(error); } } /** * Retrieves device information from the device. * @returns A promise that resolves to the device information. * @throws {ResponseError} If the response from the device indicates an error. */ async deviceInfo() { try { const responseBuffer = await this.transport.send(0xe0, 0x01, 0, 0, Buffer.from([]), [consts_1.LedgerError.NoErrors, 0x6e00]); const response = (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION); const targetId = response.readBytes(4).toString('hex'); const secureElementVersionLen = response.readBytes(1).readUInt8(); const seVersion = response.readBytes(secureElementVersionLen).toString(); const flagsLen = response.readBytes(1).readUInt8(); const flag = response.readBytes(flagsLen).toString('hex'); const mcuVersionLen = response.readBytes(1).readUInt8(); let tmp = response.readBytes(mcuVersionLen); // Patch issue in mcu version // Find the first zero byte and trim the buffer up to that point const firstZeroIndex = tmp.indexOf(0); if (firstZeroIndex !== -1) { tmp = tmp.subarray(0, firstZeroIndex); } const mcuVersion = tmp.toString(); return { targetId, seVersion, flag, mcuVersion, }; } catch (error) { throw (0, common_1.processErrorResponse)(error); } } } exports.default = BaseApp; //# sourceMappingURL=app.js.map