UNPKG

@avalabs/hw-app-avalanche

Version:

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

637 lines 28.9 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); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LedgerError = void 0; const common_1 = require("./common"); Object.defineProperty(exports, "LedgerError", { enumerable: true, get: function () { return common_1.LedgerError; } }); const helper_1 = require("./helper"); const hw_app_eth_1 = __importDefault(require("@ledgerhq/hw-app-eth")); __exportStar(require("./types"), exports); function processGetAddrResponse(response) { let partialResponse = response; const errorCodeData = partialResponse.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; //get public key len (variable) const PKLEN = partialResponse[0]; if (PKLEN === undefined) { throw new Error("Invalid response: missing public key length"); } const publicKey = Buffer.from(partialResponse.slice(1, 1 + PKLEN)); //"advance" buffer partialResponse = partialResponse.slice(1 + PKLEN); let hash; let address; if (PKLEN != common_1.ED25519_PK_SIZE) { hash = Buffer.from(partialResponse.slice(0, 20)); //"advance" buffer partialResponse = partialResponse.slice(20); address = Buffer.from(partialResponse.subarray(0, -2)).toString(); } else { // ED25519: Convert raw bytes to hex string address = partialResponse.subarray(0, -2).toString("hex"); } return { publicKey, hash, address, returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), }; } function processGetXPubResponse(response) { let partialResponse = response; const errorCodeData = partialResponse.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; //get public key len (variable) const PKLEN = partialResponse[0]; if (PKLEN === undefined) { throw new Error("Invalid response: missing public key length"); } const publicKey = Buffer.from(partialResponse.slice(1, 1 + PKLEN)); //"advance" buffer partialResponse = partialResponse.slice(1 + PKLEN); const chain_code = Buffer.from(partialResponse.slice(0, -2)); return { publicKey, chain_code, returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), }; } class AvalancheApp { constructor(transport, ethScrambleKey = "w0w", ethLoadConfig = {}) { this.transport = transport; if (!transport) { throw new Error("Transport has not been defined"); } this.eth = new hw_app_eth_1.default(transport, ethScrambleKey, ethLoadConfig); } static prepareChunks(message, serializedPathBuffer) { const chunks = []; // First chunk (only path) if (serializedPathBuffer !== undefined) { // First chunk (only path) chunks.push(serializedPathBuffer); } const buffer = Buffer.from(message); 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.slice(i, end)); } return chunks; } async signGetChunks(message, path) { if (path === undefined) { return await AvalancheApp.prepareChunks(message, Buffer.alloc(0)); } return await AvalancheApp.prepareChunks(message, (0, helper_1.serializePath)(path)); } concatMessageAndChangePath(message, path) { // data const msg = message; // no change_path if (path === undefined) { const buffer = Buffer.alloc(1); buffer.writeUInt8(0); return Buffer.concat([new Uint8Array(buffer), new Uint8Array(msg)]); } let buffer = Buffer.alloc(1); buffer.writeUInt8(path.length); path.forEach((element) => { buffer = Buffer.concat([ new Uint8Array(buffer), new Uint8Array((0, helper_1.serializePathSuffix)(element)), ]); }); return Buffer.concat([new Uint8Array(buffer), new Uint8Array(msg)]); } async signSendChunk(chunkIdx, chunkNum, chunk, param, ins = common_1.INS.SIGN) { let payloadType = common_1.PAYLOAD_TYPE.ADD; let p2 = 0; if (chunkIdx === 1) { payloadType = common_1.PAYLOAD_TYPE.INIT; if (param === undefined) { throw Error("number type not given"); } p2 = param; } if (chunkIdx === chunkNum) { payloadType = common_1.PAYLOAD_TYPE.LAST; } return await this.transport .send(common_1.CLA, ins, payloadType, p2, chunk, [ common_1.LedgerError.NoErrors, common_1.LedgerError.DataIsInvalid, common_1.LedgerError.BadKeyHandle, common_1.LedgerError.SignVerifyError, ]) .then((response) => { const errorCodeData = response.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } 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.slice(0, response.length - 2).toString("ascii")}`; } if (returnCode === common_1.LedgerError.NoErrors && response.length > 2) { return { hash: null, signature: null, returnCode: returnCode, errorMessage: errorMessage, }; } return { returnCode: returnCode, errorMessage: errorMessage, }; }, common_1.processErrorResponse); } async signHash(path_prefix, signing_paths, hash, curve_type = common_1.P2_VALUES.SECP256K1) { if (hash.length !== common_1.HASH_LEN) { throw new Error("Invalid hash length"); } //send hash and path const first_response = await this.transport .send(common_1.CLA, common_1.INS.SIGN_HASH, common_1.FIRST_MESSAGE, 0x00, Buffer.concat([ new Uint8Array((0, helper_1.serializePath)(path_prefix)), new Uint8Array(hash), ]), [common_1.LedgerError.NoErrors]) .then((response) => { const errorCodeData = response.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } 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) { errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`; } return { returnCode: returnCode, errorMessage: errorMessage, }; }, common_1.processErrorResponse); if (first_response.returnCode !== common_1.LedgerError.NoErrors) { return first_response; } return this._signAndCollect(signing_paths, curve_type); } async _signAndCollect(signing_paths, curve_type) { // base response object to output on each iteration const result = { returnCode: common_1.LedgerError.NoErrors, errorMessage: "", hash: null, signatures: null, }; // where each pair path_suffix, signature are stored const signatures = new Map(); for (let idx = 0; idx < signing_paths.length; idx++) { const suffix = signing_paths[idx]; if (!suffix) { throw new Error(`Invalid signing path at index ${idx}`); } const path_buf = (0, helper_1.serializePathSuffix)(suffix); const p1 = idx >= signing_paths.length - 1 ? common_1.LAST_MESSAGE : common_1.NEXT_MESSAGE; // send path to sign hash that should be in device's ram memory await this.transport .send(common_1.CLA, common_1.INS.SIGN_HASH, p1, curve_type, path_buf, [ common_1.LedgerError.NoErrors, common_1.LedgerError.DataIsInvalid, common_1.LedgerError.BadKeyHandle, common_1.LedgerError.SignVerifyError, ]) .then((response) => { const errorCodeData = response.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; const errorMessage = (0, common_1.errorCodeToString)(returnCode); if (returnCode === common_1.LedgerError.BadKeyHandle || returnCode === common_1.LedgerError.DataIsInvalid || returnCode === common_1.LedgerError.SignVerifyError) { result.errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`; } if (returnCode === common_1.LedgerError.NoErrors && response.length > 2) { signatures.set(suffix, response.slice(0, -2)); } result.returnCode = returnCode; result.errorMessage = errorMessage; return; }, common_1.processErrorResponse); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } result.signatures = signatures; return result; } async sign(path_prefix, signing_paths, message, change_paths, curve_type = common_1.P2_VALUES.SECP256K1) { // Do not show outputs that go to the signers let paths = signing_paths; if (change_paths !== undefined) { // remove duplication just is case paths = [...new Set([...paths, ...change_paths])]; } // Prepend change_paths to the message as the device do set which outputs should be // shown at parsing const msg = this.concatMessageAndChangePath(message, paths); // Send transaction for review const response = await this.signGetChunks(msg, path_prefix).then((chunks) => { if (!chunks || chunks.length === 0 || !chunks[0]) { throw new Error("Invalid chunks array"); } return this.signSendChunk(1, chunks.length, chunks[0], common_1.FIRST_MESSAGE, common_1.INS.SIGN).then(async (response) => { // initialize response let result = { returnCode: response.returnCode, errorMessage: response.errorMessage, signatures: null, }; // send chunks for (let i = 1; i < chunks.length; i += 1) { const chunk = chunks[i]; if (!chunk) { throw new Error(`Invalid chunk at index ${i}`); } result = await this.signSendChunk(1 + i, chunks.length, chunk, common_1.NEXT_MESSAGE, common_1.INS.SIGN); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } return result; }, common_1.processErrorResponse); }, common_1.processErrorResponse); if (response.returnCode !== common_1.LedgerError.NoErrors) { return response; } // Transaction was approved so start iterating over signing_paths to sign // and collect each signature return this._signAndCollect(signing_paths, curve_type); } // Sign an arbitrary message. // This function takes in an avax path prefix like: m/44'/9000'/0'/0' // signing_paths: ["0/1", "5/8"] // message: The message to be signed async signMsg(path_prefix, signing_paths, message, curve_type = common_1.P2_VALUES.SECP256K1) { const coinType = (0, helper_1.pathCoinType)(path_prefix); if (coinType !== "9000'") { throw new Error("Only avax path is supported"); } const header = Buffer.from("\x1AAvalanche Signed Message:\n", "utf8"); const content = Buffer.from(message, "utf8"); const msgSize = Buffer.alloc(4); msgSize.writeUInt32BE(content.length, 0); const avax_msg = Buffer.from(`${header}${msgSize}${content}`, "utf8"); // Send msg for review const response = await this.signGetChunks(avax_msg, path_prefix).then((chunks) => { if (!chunks || chunks.length === 0 || !chunks[0]) { throw new Error("Invalid chunks array"); } return this.signSendChunk(1, chunks.length, chunks[0], common_1.FIRST_MESSAGE, common_1.INS.SIGN_MSG).then(async (response) => { // initialize response let result = { returnCode: response.returnCode, errorMessage: response.errorMessage, signatures: null, }; // send chunks for (let i = 1; i < chunks.length; i += 1) { const chunk = chunks[i]; if (!chunk) { throw new Error(`Invalid chunk at index ${i}`); } result = await this.signSendChunk(1 + i, chunks.length, chunk, common_1.NEXT_MESSAGE, common_1.INS.SIGN_MSG); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } return result; }, common_1.processErrorResponse); }, common_1.processErrorResponse); if (response.returnCode !== common_1.LedgerError.NoErrors) { return response; } // Message was approved so start iterating over signing_paths to sign // and collect each signature return this._signAndCollect(signing_paths, curve_type); } 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.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } 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]; if (appNameLen === undefined) { throw new Error("Invalid response: missing app name length"); } appName = response.slice(2, 2 + appNameLen).toString("ascii"); let idx = 2 + appNameLen; const appVersionLen = response[idx]; if (appVersionLen === undefined) { throw new Error("Invalid response: missing app version length"); } idx += 1; appVersion = response.slice(idx, idx + appVersionLen).toString("ascii"); idx += appVersionLen; const appFlagsLen = response[idx]; if (appFlagsLen === undefined) { throw new Error("Invalid response: missing app flags length"); } idx += 1; flagLen = appFlagsLen; const flagsVal = response[idx]; if (flagsVal === undefined) { throw new Error("Invalid response: missing flags value"); } flagsValue = flagsVal; } return { returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), // appName, appVersion, flagLen, flagsValue, flagRecovery: (flagsValue & 1) !== 0, flagSignedMcuCode: (flagsValue & 2) !== 0, flagOnboarded: (flagsValue & 4) !== 0, flagPINValidated: (flagsValue & 128) !== 0, }; }, common_1.processErrorResponse); } async _pubkey(path, show, hrp, chainid, curve_type = common_1.P2_VALUES.SECP256K1) { const p1 = show ? common_1.P1_VALUES.SHOW_ADDRESS_IN_DEVICE : common_1.P1_VALUES.ONLY_RETRIEVE; const serializedPath = (0, helper_1.serializePath)(path); // Validate curve type if (curve_type !== common_1.P2_VALUES.SECP256K1 && curve_type !== common_1.P2_VALUES.ED25519) { throw new Error("Invalid curve type. Must be 0 for secp256k1 or 1 for ed25519"); } const payload = curve_type === common_1.P2_VALUES.SECP256K1 ? Buffer.concat([ new Uint8Array((0, helper_1.serializeHrp)(hrp)), new Uint8Array((0, helper_1.serializeChainID)(chainid)), new Uint8Array(serializedPath), ]) : serializedPath; return await this.transport .send(common_1.CLA, common_1.INS.GET_ADDR, p1, curve_type, payload, [common_1.LedgerError.NoErrors]) .then(processGetAddrResponse, common_1.processErrorResponse); } async getAddressAndPubKey(path, show, hrp, chainid, curve_type = common_1.P2_VALUES.SECP256K1) { return await this._pubkey(path, show, hrp, chainid, curve_type); } async _xpub(path, show, hrp, chainid) { const p1 = show ? common_1.P1_VALUES.SHOW_ADDRESS_IN_DEVICE : common_1.P1_VALUES.ONLY_RETRIEVE; const serializedPath = (0, helper_1.serializePath)(path); const serializedHrp = (0, helper_1.serializeHrp)(hrp); const serializedChainID = (0, helper_1.serializeChainID)(chainid); return await this.transport .send(common_1.CLA, common_1.INS.GET_EXTENDED_PUBLIC_KEY, p1, 0, Buffer.concat([ new Uint8Array(serializedHrp), new Uint8Array(serializedChainID), new Uint8Array(serializedPath), ]), [common_1.LedgerError.NoErrors]) .then(processGetXPubResponse, common_1.processErrorResponse); } async getExtendedPubKey(path, show, hrp, chainid) { return await this._xpub(path, show, hrp, chainid); } async _walletId(show) { const p1 = show ? common_1.P1_VALUES.SHOW_ADDRESS_IN_DEVICE : common_1.P1_VALUES.ONLY_RETRIEVE; return await this.transport .send(common_1.CLA, common_1.INS.WALLET_ID, p1, 0) .then((response) => { const errorCodeData = response.slice(-2); if (errorCodeData.length < 2) { throw new Error("Invalid response: missing error code data"); } const returnCode = (errorCodeData[0] * 256 + errorCodeData[1]); return { returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), id: response.slice(0, 6), }; }, common_1.processErrorResponse); } async getWalletId() { return await this._walletId(false); } async showWalletId() { return await this._walletId(true); } signEVMTransaction(path, rawTxHex, resolution) { return this.eth.signTransaction(path, rawTxHex, resolution); } getETHAddress(path, boolDisplay, boolChaincode) { return this.eth.getAddress(path, boolDisplay, boolChaincode); } getAppConfiguration() { return this.eth.getAppConfiguration(); } async provideERC20TokenInformation(ticker, contractName, address, decimals, chainId) { // Calculate lengths const tickerLength = Buffer.byteLength(ticker); const contractNameLength = Buffer.byteLength(contractName); // Create a buffer with the exact size needed const buffer = Buffer.alloc(1 + tickerLength + 1 + contractNameLength + 20 + 4 + 4); let offset = 0; // Ticker length and ticker buffer.writeUInt8(tickerLength, offset); offset += 1; buffer.write(ticker, offset); offset += tickerLength; // Contract name length and contract name buffer.writeUInt8(contractNameLength, offset); offset += 1; buffer.write(contractName, offset); offset += contractNameLength; // Address (20 bytes, hex string needs to be parsed) var addr_offset = 0; if (address.startsWith("0x")) { addr_offset = 2; } // Slice to remove '0x' const addressBuffer = Buffer.from(address.slice(addr_offset), "hex"); addressBuffer.copy(new Uint8Array(buffer), offset); offset += 20; // Decimals (4 bytes, big endian) buffer.writeUInt32BE(decimals, offset); offset += 4; // Chain ID (4 bytes, big endian) buffer.writeUInt32BE(chainId, offset); offset += 4; return await this.eth.provideERC20TokenInformation(buffer.toString("hex")); } async provideNFTInformation(collectionName, contractAddress, chainId) { const NAME_LENGTH_SIZE = 1; const HEADER_SIZE = common_1.TYPE_SIZE + common_1.VERSION_SIZE + NAME_LENGTH_SIZE; const KEY_ID_SIZE = 1; const PROD_NFT_METADATA_KEY = 1; const collectionNameLength = Buffer.byteLength(collectionName, "utf8"); if (collectionNameLength > common_1.COLLECTION_NAME_MAX_LEN) { throw new Error(`Collection name exceeds maximum allowed length of ${common_1.COLLECTION_NAME_MAX_LEN}`); } // We generate a fake signature, because verification is disabled // in the app. const fakeDerSignature = this._generateFakeDerSignature(); const buffer = Buffer.alloc(HEADER_SIZE + collectionNameLength + common_1.ADDRESS_LENGTH + common_1.CHAIN_ID_SIZE + KEY_ID_SIZE + common_1.ALGORITHM_ID_SIZE + common_1.SIGNATURE_LENGTH_SIZE + fakeDerSignature.length); let offset = 0; buffer.writeUInt8(common_1.TYPE_1, offset); offset += common_1.TYPE_SIZE; buffer.writeUInt8(common_1.VERSION_1, offset); offset += common_1.VERSION_SIZE; buffer.writeUInt8(collectionNameLength, offset); offset += NAME_LENGTH_SIZE; buffer.write(collectionName, offset, "utf8"); offset += collectionNameLength; Buffer.from(contractAddress.slice(2), "hex").copy(new Uint8Array(buffer), offset); // Remove '0x' from address offset += common_1.ADDRESS_LENGTH; buffer.writeBigUInt64BE(chainId, offset); offset += common_1.CHAIN_ID_SIZE; buffer.writeUInt8(PROD_NFT_METADATA_KEY, offset); // Assume production key for simplicity offset += KEY_ID_SIZE; buffer.writeUInt8(common_1.ALGORITHM_ID_1, offset); // Assume a specific algorithm for signature or hash offset += common_1.ALGORITHM_ID_SIZE; buffer.writeUInt8(fakeDerSignature.length, offset); offset += common_1.SIGNATURE_LENGTH_SIZE; fakeDerSignature.copy(new Uint8Array(buffer), offset); return await this.eth.provideNFTInformation(buffer.toString("hex")); } _generateFakeDerSignature() { const fakeSignatureLength = 70; const fakeDerSignature = Buffer.alloc(fakeSignatureLength); // Fill the buffer with random bytes for (let i = 0; i < fakeSignatureLength; i++) { fakeDerSignature[i] = Math.floor(Math.random() * 256); } return fakeDerSignature; } // We assume pluginName is ERC721 for Nft tokens async setPlugin(contractAddress, methodSelector, chainId) { const KEY_ID = 2; const PLUGIN_NAME_LENGTH_SIZE = 1; const KEY_ID_SIZE = 1; const PLUGIN_NAME = "ERC721"; const pluginNameBuffer = Buffer.from(PLUGIN_NAME, "utf8"); const pluginNameLength = pluginNameBuffer.length; const contractAddressBuffer = Buffer.from(contractAddress.slice(2), "hex"); const methodSelectorBuffer = Buffer.from(methodSelector.slice(2), "hex"); // We generate a fake signature, because verification is disabled // in the app. const signatureBuffer = this._generateFakeDerSignature(); const signatureLength = signatureBuffer.length; const buffer = Buffer.alloc(common_1.TYPE_SIZE + common_1.VERSION_SIZE + PLUGIN_NAME_LENGTH_SIZE + pluginNameLength + contractAddressBuffer.length + methodSelectorBuffer.length + common_1.CHAIN_ID_SIZE + KEY_ID_SIZE + common_1.ALGORITHM_ID_SIZE + common_1.SIGNATURE_LENGTH_SIZE + signatureLength); let offset = 0; buffer.writeUInt8(common_1.TYPE_1, offset); offset += common_1.TYPE_SIZE; buffer.writeUInt8(common_1.VERSION_1, offset); offset += common_1.VERSION_SIZE; buffer.writeUInt8(pluginNameLength, offset); offset += PLUGIN_NAME_LENGTH_SIZE; pluginNameBuffer.copy(new Uint8Array(buffer), offset); offset += pluginNameLength; contractAddressBuffer.copy(new Uint8Array(buffer), offset); offset += contractAddressBuffer.length; methodSelectorBuffer.copy(new Uint8Array(buffer), offset); offset += methodSelectorBuffer.length; buffer.writeBigUInt64BE(BigInt(chainId), offset); offset += common_1.CHAIN_ID_SIZE; // use default key_id buffer.writeUInt8(KEY_ID, offset); offset += KEY_ID_SIZE; // use default algorithm buffer.writeUInt8(common_1.ALGORITHM_ID_1, offset); offset += common_1.ALGORITHM_ID_SIZE; buffer.writeUInt8(signatureLength, offset); offset += common_1.SIGNATURE_LENGTH_SIZE; signatureBuffer.copy(new Uint8Array(buffer), offset); return await this.eth.setPlugin(buffer.toString("hex")); } async clearSignTransaction(path, rawTxHex, resolutionConfig, throwOnError = false) { return await this.eth.clearSignTransaction(path, rawTxHex, resolutionConfig, throwOnError); } async signEIP712Message(path, jsonMessage, fullImplem = false) { return await this.eth.signEIP712Message(path, jsonMessage, fullImplem); } async signEIP712HashedMessage(path, domainSeparatorHex, hashStructMessageHex) { return await this.eth.signEIP712HashedMessage(path, domainSeparatorHex, hashStructMessageHex); } } exports.default = AvalancheApp; //# sourceMappingURL=index.js.map