@zondax/ledger-filecoin
Version:
Node API for the Filecoin App (Ledger Nano S+, X, Stax and Flex)
263 lines (254 loc) • 10.2 kB
JavaScript
;
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;