UNPKG

@zondax/ledger-filecoin

Version:

Node API for the Filecoin App (Ledger Nano S+, X, Stax and Flex)

263 lines (254 loc) 10.2 kB
'use strict'; var BaseApp = require('@zondax/ledger-js'); var varint = require('varint'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var BaseApp__default = /*#__PURE__*/_interopDefault(BaseApp); var varint__namespace = /*#__PURE__*/_interopNamespace(varint); exports.P1_VALUES = void 0; (function (P1_VALUES) { P1_VALUES[P1_VALUES["ONLY_RETRIEVE"] = 0] = "ONLY_RETRIEVE"; P1_VALUES[P1_VALUES["SHOW_ADDRESS_IN_DEVICE"] = 1] = "SHOW_ADDRESS_IN_DEVICE"; })(exports.P1_VALUES || (exports.P1_VALUES = {})); const PUBKEYLEN = 65; /** * Minimal ETH APDU implementation to avoid heavy hw-app-eth dependency * This implementation only includes the methods needed for Filecoin EVM support */ const CLA = 0xe0; const INS_GET_ETH_ADDRESS = 0x02; const INS_SIGN_ETH_TRANSACTION = 0x04; const INS_SIGN_PERSONAL_MESSAGE = 0x08; const CHUNK_SIZE = 255; /** * Encodes a BIP32 path into a buffer */ function encodePath(path) { const pathArray = typeof path === 'string' ? path.split('/').filter((p) => p !== 'm') : path; const pathNumbers = pathArray.map((p) => { const stripped = p.replace(/'/g, ''); const num = parseInt(stripped, 10); return p.includes("'") ? num + 0x80000000 : num; }); const buffer = Buffer.alloc(1 + pathNumbers.length * 4); buffer.writeUInt8(pathNumbers.length, 0); pathNumbers.forEach((element, index) => { buffer.writeUInt32BE(element, 1 + 4 * index); }); return buffer; } /** * Get Ethereum address from the Ledger device */ async function getETHAddress(transport, path, display = false, chaincode = false) { const pathBuffer = encodePath(path); const p1 = display ? 0x01 : 0x00; const p2 = chaincode ? 0x01 : 0x00; const response = await transport.send(CLA, INS_GET_ETH_ADDRESS, p1, p2, pathBuffer); // Parse response: publicKey (65 bytes), address (40 bytes ASCII), optional chainCode (32 bytes) const publicKeyLength = response[0]; const publicKey = response.slice(1, 1 + publicKeyLength).toString('hex'); const addressLength = response[1 + publicKeyLength]; const address = response.slice(1 + publicKeyLength + 1, 1 + publicKeyLength + 1 + addressLength).toString('ascii'); const result = { publicKey, address: address.startsWith('0x') ? address : '0x' + address, }; if (chaincode) { const chainCode = response.slice(1 + publicKeyLength + 1 + addressLength, 1 + publicKeyLength + 1 + addressLength + 32).toString('hex'); result.chainCode = chainCode; } return result; } /** * Sign an Ethereum transaction */ async function signETHTransaction(transport, path, rawTxHex, resolution) { const pathBuffer = encodePath(path); // Remove 0x prefix if present const txHex = rawTxHex.startsWith('0x') ? rawTxHex.slice(2) : rawTxHex; const txBuffer = Buffer.from(txHex, 'hex'); const chunks = []; pathBuffer.length + txBuffer.length; // First chunk includes path let offset = 0; const firstChunkSize = Math.min(CHUNK_SIZE - pathBuffer.length, txBuffer.length); chunks.push(Buffer.concat([pathBuffer, txBuffer.slice(0, firstChunkSize)])); offset += firstChunkSize; // Remaining chunks while (offset < txBuffer.length) { const chunkSize = Math.min(CHUNK_SIZE, txBuffer.length - offset); chunks.push(txBuffer.slice(offset, offset + chunkSize)); offset += chunkSize; } let response = Buffer.alloc(0); // Send chunks for (let i = 0; i < chunks.length; i++) { const p1 = i === 0 ? 0x00 : 0x80; // First chunk or subsequent const p2 = 0x00; response = await transport.send(CLA, INS_SIGN_ETH_TRANSACTION, p1, p2, chunks[i]); } // Parse signature response // Response format: v (1 byte) + r (32 bytes) + s (32 bytes) if (response.length < 65) { throw new Error('Invalid signature response length'); } const v = response[0].toString(16).padStart(2, '0'); const r = response.slice(1, 33).toString('hex'); const s = response.slice(33, 65).toString('hex'); return { v, r, s }; } /** * Sign a personal message (EIP-191) */ async function signPersonalMessageEVM(transport, path, messageHex) { const pathBuffer = encodePath(path); // Remove 0x prefix if present const msgHex = messageHex.startsWith('0x') ? messageHex.slice(2) : messageHex; const messageBuffer = Buffer.from(msgHex, 'hex'); // Prepare message length as 4-byte big-endian const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeUInt32BE(messageBuffer.length, 0); const chunks = []; // First chunk includes path and length let offset = 0; const firstChunkData = Buffer.concat([pathBuffer, lengthBuffer]); const firstChunkSize = Math.min(CHUNK_SIZE - firstChunkData.length, messageBuffer.length); chunks.push(Buffer.concat([firstChunkData, messageBuffer.slice(0, firstChunkSize)])); offset += firstChunkSize; // Remaining chunks while (offset < messageBuffer.length) { const chunkSize = Math.min(CHUNK_SIZE, messageBuffer.length - offset); chunks.push(messageBuffer.slice(offset, offset + chunkSize)); offset += chunkSize; } let response = Buffer.alloc(0); // Send chunks for (let i = 0; i < chunks.length; i++) { const p1 = i === 0 ? 0x00 : 0x80; // First chunk or subsequent const p2 = 0x00; response = await transport.send(CLA, INS_SIGN_PERSONAL_MESSAGE, p1, p2, chunks[i]); } // Parse signature response if (response.length < 65) { throw new Error('Invalid signature response length'); } const v = response[0].toString(16).padStart(2, '0'); const r = response.slice(1, 33).toString('hex'); const s = response.slice(33, 65).toString('hex'); return { v, r, s }; } class FilecoinApp extends BaseApp__default.default { constructor(transport) { super(transport, FilecoinApp._params); if (!this.transport) { throw new Error('Transport has not been defined'); } } parseAddressResponse(response) { const compressed_pk = response.readBytes(PUBKEYLEN); const addrByteLength = response.readBytes(1)[0]; const addrByte = response.readBytes(addrByteLength); const addrStringLength = response.readBytes(1)[0]; const addrString = response.readBytes(addrStringLength).toString(); return { compressed_pk, addrByte, addrString, }; } async getAddressAndPubKey(path) { const bip44PathBuffer = this.serializePath(path); try { const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_ADDR_SECP256K1, exports.P1_VALUES.ONLY_RETRIEVE, 0, bip44PathBuffer); const response = BaseApp.processResponse(responseBuffer); return this.parseAddressResponse(response); } catch (e) { throw BaseApp.processErrorResponse(e); } } async showAddressAndPubKey(path) { const bip44PathBuffer = this.serializePath(path); try { const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_ADDR_SECP256K1, exports.P1_VALUES.SHOW_ADDRESS_IN_DEVICE, 0, bip44PathBuffer); const response = BaseApp.processResponse(responseBuffer); return this.parseAddressResponse(response); } catch (e) { throw BaseApp.processErrorResponse(e); } } async _sign(instruction, path, data) { const chunks = this.prepareChunks(path, data); try { // First chunk let signatureResponse = await this.sendGenericChunk(instruction, 0, 1, chunks.length, chunks[0]); for (let i = 1; i < chunks.length; i += 1) { signatureResponse = await this.sendGenericChunk(instruction, 0, 1 + i, chunks.length, chunks[i]); } return { signature_compact: signatureResponse.readBytes(65), signature_der: signatureResponse.getAvailableBuffer(), }; } catch (e) { throw BaseApp.processErrorResponse(e); } } sign(path, blob) { return this._sign(this.INS.SIGN_SECP256K1, path, blob); } signRawBytes(path, message) { const len = Buffer.from(varint__namespace.encode(message.length)); const data = Buffer.concat([len, message]); return this._sign(this.INS.SIGN_RAW_BYTES, path, data); } signPersonalMessageFVM(path, messageHex) { const len = Buffer.alloc(4); len.writeUInt32BE(messageHex.length, 0); const data = Buffer.concat([len, messageHex]); return this._sign(this.INS.SIGN_PERSONAL_MESSAGE, path, data); } async signETHTransaction(path, rawTxHex, resolution = null) { return await signETHTransaction(this.transport, path, rawTxHex); } async getETHAddress(path, boolDisplay = false, boolChaincode = false) { return await getETHAddress(this.transport, path, boolDisplay, boolChaincode); } async signPersonalMessageEVM(path, messageHex) { return await signPersonalMessageEVM(this.transport, path, messageHex); } } FilecoinApp._INS = { GET_VERSION: 0x00, GET_ADDR_SECP256K1: 0x01, SIGN_SECP256K1: 0x02, SIGN_RAW_BYTES: 0x07, SIGN_PERSONAL_MESSAGE: 0x08, }; FilecoinApp._params = { cla: 0x06, ins: { ...FilecoinApp._INS }, p1Values: { ONLY_RETRIEVE: 0x00, SHOW_ADDRESS_IN_DEVICE: 0x01 }, chunkSize: 250, requiredPathLengths: [5], }; exports.FilecoinApp = FilecoinApp; exports.PUBKEYLEN = PUBKEYLEN;