UNPKG

@zondax/ledger-lisk

Version:

Node API for the Lisk App (Ledger Nano S/X/S+)

297 lines (296 loc) 12.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LedgerAccount = exports.LiskApp = exports.LedgerError = void 0; const common_1 = require("./common"); Object.defineProperty(exports, "LedgerError", { enumerable: true, get: function () { return common_1.LedgerError; } }); const config_1 = require("./config"); __exportStar(require("./types"), exports); function processGetAddrResponse(response) { const errorCodeData = response.subarray(-2); const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; const pubKey = response.subarray(0, config_1.PKLEN).toString("hex"); const address = response.subarray(config_1.PKLEN, response.length - 2).toString("ascii"); return { pubKey, address, return_code: returnCode, error_message: (0, common_1.errorCodeToString)(returnCode), }; } class LiskApp { constructor(transport) { if (!transport) { throw new Error("Transport has not been defined"); } this.transport = transport; } static prepareChunks(serializedPathBuffer, message) { const chunks = []; // First chunk (only path) chunks.push(serializedPathBuffer); const messageBuffer = Buffer.from(message); const buffer = Buffer.concat([messageBuffer]); for (let i = 0; i < buffer.length; i += common_1.CHUNK_SIZE) { let end = i + common_1.CHUNK_SIZE; if (i > buffer.length) { end = buffer.length; } chunks.push(buffer.subarray(i, end)); } return chunks; } async signGetChunks(path, message) { return LiskApp.prepareChunks((0, common_1.serializePath)(path), message); } async getVersion() { return await (0, common_1.getVersion)(this.transport).catch((err) => (0, common_1.processErrorResponse)(err)); } async getAppInfo() { return await this.transport.send(0xb0, 0x01, 0, 0).then((response) => { const errorCodeData = response.subarray(-2); const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; const result = {}; let appName = "err"; let appVersion = "err"; let flagLen = 0; let flagsValue = 0; if (response[0] !== 1) { // Ledger responds with format ID 1. There is no spec for any format != 1 result.errorMessage = "response format ID not recognized"; result.returnCode = common_1.LedgerError.DeviceIsBusy; } else { const appNameLen = response[1]; appName = response.subarray(2, 2 + appNameLen).toString("ascii"); let idx = 2 + appNameLen; const appVersionLen = response[idx]; idx += 1; appVersion = response.subarray(idx, idx + appVersionLen).toString("ascii"); idx += appVersionLen; const appFlagsLen = response[idx]; idx += 1; flagLen = appFlagsLen; flagsValue = response[idx]; } return { appName, appVersion, flagLen, flagsValue, flagRecovery: (flagsValue & 1) !== 0, // eslint-disable-next-line no-bitwise flagSignedMcuCode: (flagsValue & 2) !== 0, // eslint-disable-next-line no-bitwise flagOnboarded: (flagsValue & 4) !== 0, // eslint-disable-next-line no-bitwise flagPINValidated: (flagsValue & 128) !== 0, return_code: returnCode, error_message: (0, common_1.errorCodeToString)(returnCode), }; }, common_1.processErrorResponse); } async deviceInfo() { return await this.transport .send(0xe0, 0x01, 0, 0, Buffer.from([]), [common_1.LedgerError.NoErrors, common_1.LedgerError.AppDoesNotSeemToBeOpen]) .then((response) => { const errorCodeData = response.subarray(-2); const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; const targetId = response.subarray(0, 4).toString("hex"); let pos = 4; const secureElementVersionLen = response[pos]; pos += 1; const seVersion = response.subarray(pos, pos + secureElementVersionLen).toString(); pos += secureElementVersionLen; const flagsLen = response[pos]; pos += 1; const flag = response.subarray(pos, pos + flagsLen).toString("hex"); pos += flagsLen; const mcuVersionLen = response[pos]; pos += 1; // Patch issue in mcu version let tmp = response.subarray(pos, pos + mcuVersionLen); if (tmp[mcuVersionLen - 1] === 0) { tmp = response.subarray(pos, pos + mcuVersionLen - 1); } const mcuVersion = tmp.toString(); return { targetId, seVersion, flag, mcuVersion, return_code: returnCode, error_message: (0, common_1.errorCodeToString)(returnCode), }; }, common_1.processErrorResponse); } async getAddressAndPubKey(path) { const serializedPath = (0, common_1.serializePath)(path); return await this.transport .send(config_1.CLA, 1 /* INS.GET_ADDR_PUBKEY */, common_1.P1_VALUES.ONLY_RETRIEVE, 0, serializedPath, [common_1.LedgerError.NoErrors]) .then(processGetAddrResponse, common_1.processErrorResponse); } async getMultipleAddresses(indexes) { if (indexes.length === 0) throw new Error("Indexes array must not be empty"); const addresses = { return_code: common_1.LedgerError.NoErrors, error_message: (0, common_1.errorCodeToString)(common_1.LedgerError.NoErrors), addr: {}, }; for (const index of indexes) { const addr = await this.getAddressAndPubKey(`m/44'/134'/${index}'`); if (addr.return_code !== common_1.LedgerError.NoErrors) return { return_code: addr.return_code, error_message: addr.error_message, addr: [] }; addresses.addr[index] = { pubKey: addr.pubKey, address: addr.address }; } return addresses; } async showAddressAndPubKey(path) { const serializedPath = (0, common_1.serializePath)(path); return await this.transport .send(config_1.CLA, 1 /* INS.GET_ADDR_PUBKEY */, common_1.P1_VALUES.SHOW_ADDRESS_IN_DEVICE, 0, serializedPath, [common_1.LedgerError.NoErrors]) .then(processGetAddrResponse, common_1.processErrorResponse); } async signSendChunk(chunkIdx, chunkNum, chunk, ins) { let payloadType = common_1.PAYLOAD_TYPE.ADD; if (chunkIdx === 1) { payloadType = common_1.PAYLOAD_TYPE.INIT; } if (chunkIdx === chunkNum) { payloadType = common_1.PAYLOAD_TYPE.LAST; } // Check supported sign instructions if (ins !== 2 /* INS.SIGN_TXN */) { // Error here } return await this.transport .send(config_1.CLA, ins, payloadType, 0, chunk, [ common_1.LedgerError.NoErrors, common_1.LedgerError.DataIsInvalid, common_1.LedgerError.BadKeyHandle, common_1.LedgerError.SignVerifyError, ]) .then((response) => { const errorCodeData = response.subarray(-2); const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; let errorMessage = (0, common_1.errorCodeToString)(returnCode); if (returnCode === common_1.LedgerError.BadKeyHandle || returnCode === common_1.LedgerError.DataIsInvalid || returnCode === common_1.LedgerError.SignVerifyError) { errorMessage = `${errorMessage} : ${response.subarray(0, response.length - 2).toString("ascii")}`; } if (returnCode === common_1.LedgerError.NoErrors && response.length > 2) { const signature = response.subarray(0, response.length - 2); return { signature, return_code: returnCode, error_message: errorMessage, }; } return { return_code: returnCode, error_message: errorMessage, }; }, common_1.processErrorResponse); } async signImpl(path, message, ins) { return await this.signGetChunks(path, message).then(async (chunks) => { return await this.signSendChunk(1, chunks.length, chunks[0], ins).then(async (result) => { for (let i = 1; i < chunks.length; i += 1) { // eslint-disable-next-line no-await-in-loop,no-param-reassign result = await this.signSendChunk(1 + i, chunks.length, chunks[i], ins); if (result.return_code !== common_1.ERROR_CODE.NoError) { break; } } return { return_code: result.return_code, error_message: result.error_message, signature: result.signature, }; }, common_1.processErrorResponse); }); } async sign(path, message) { return await this.signImpl(path, message, 2 /* INS.SIGN_TXN */); } async signMessage(path, message) { return await this.signImpl(path, message, 3 /* INS.SIGN_MSG */); } async claimMessage(path, message) { return await this.signImpl(path, message, 4 /* INS.CLAIM_MSG */); } } exports.LiskApp = LiskApp; /** * Class to specify An account used to query the ledger. */ var SupportedCoin; (function (SupportedCoin) { /** * @see https://lisk.io */ SupportedCoin[SupportedCoin["LISK"] = 134] = "LISK"; })(SupportedCoin || (SupportedCoin = {})); class LedgerAccount { constructor() { this._account = 0; this._coinIndex = SupportedCoin.LISK; // LISK } /** * Specify the account number * @param {number} newAccount * @returns {this} */ account(newAccount) { this.assertValidPath(newAccount); this._account = newAccount; return this; } /** * Specify the coin index. At the moment will force alwais Lisk. * @see https://github.com/satoshilabs/slips/blob/master/slip-0044.md * @param {number} newIndex * @returns {this} */ coinIndex(newIndex) { this.assertValidPath(newIndex); // this._coinIndex = newIndex; this._coinIndex = SupportedCoin.LISK; return this; } /** * Derive the path using hardened entries. * @returns {string} defines the path in buffer form. */ derivePath() { const pathArray = `m/44'/${this._coinIndex}'/${this._account}'`; return pathArray; } /** * Asserts that the given param is a valid path (integer > 0) */ assertValidPath(n) { if (!Number.isInteger(n)) { throw new Error("Param must be an integer"); } if (n < 0) { throw new Error("Param must be greater than zero"); } } } exports.LedgerAccount = LedgerAccount;