UNPKG

@ledgerhq/hw-app-eth

Version:
454 lines • 22.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.signEIP712HashedMessage = exports.signEIP712Message = void 0; /* eslint-disable @typescript-eslint/no-duplicate-enum-values */ const semver_1 = __importDefault(require("semver")); const index_1 = require("@ledgerhq/evm-tools/message/EIP712/index"); const erc20_1 = require("../../services/ledger/erc20"); const utils_1 = require("../../utils"); const loadConfig_1 = require("../../services/ledger/loadConfig"); const utils_2 = require("./utils"); /** * @ignore for the README * * Factory to create the recursive function that will pass on each * field level and APDUs to describe its struct implementation * * @param {Eth["sendStructImplem"]} sendStructImplem * @param {EIP712MessageTypes} types * @returns {void} */ const makeRecursiveFieldStructImplem = ({ transport, loadConfig, chainId, erc20SignaturesBlob, types, filters, shouldUseV1Filters, shouldUseDiscardedFields, coinRefsTokensMap, }) => { const typesMap = {}; for (const type in types) { typesMap[type] = types[type]?.reduce((acc, curr) => ({ ...acc, [curr.name]: curr.type }), {}); } // This recursion will call itself to handle each level of each field // in order to send APDUs for each of them const recursiveFieldStructImplem = async (destructedType, data, path = "") => { const [typeDescription, arrSizes] = destructedType; const [currSize, ...restSizes] = arrSizes; const isCustomType = !utils_2.EIP712_TYPE_PROPERTIES[typeDescription?.name?.toUpperCase() || ""]; if (Array.isArray(data) && typeof currSize !== "undefined") { await sendStructImplem(transport, { structType: "array", value: data.length, }); const entryPath = `${path}.[]`; if (!data.length) { // If the array is empty and a filter exists, we need to let the app know that the filter can be discarded const entryFilters = filters?.fields.filter(f => f.path.startsWith(entryPath)); if (entryFilters && shouldUseDiscardedFields) { for (const entryFilter of entryFilters) { await sendFilteringInfo(transport, "discardField", loadConfig, { path: entryFilter.path, }); await sendFilteringInfo(transport, "showField", loadConfig, { displayName: entryFilter.label, sig: entryFilter.signature, format: entryFilter.format, coinRef: entryFilter.coin_ref, chainId, erc20SignaturesBlob, shouldUseV1Filters, coinRefsTokensMap, isDiscarded: true, }); } } } // If the array is not empty, we need to send the struct implementation for each entry for (const entry of data) { await recursiveFieldStructImplem([typeDescription, restSizes], entry, entryPath); } } else if (isCustomType) { for (const fieldName of Object.keys(typesMap[typeDescription?.name || ""])) { const fieldValue = data[fieldName]; const fieldType = typesMap[typeDescription?.name || ""]?.[fieldName]; if (fieldType) { await recursiveFieldStructImplem((0, utils_2.destructTypeFromString)(fieldType), fieldValue, `${path}.${fieldName}`); } } } else { const filter = filters?.fields.find(f => path === f.path); if (filter) { await sendFilteringInfo(transport, "showField", loadConfig, { displayName: filter.label, sig: filter.signature, format: filter.format, coinRef: filter.coin_ref, chainId, erc20SignaturesBlob, shouldUseV1Filters, coinRefsTokensMap, isDiscarded: false, }); } await sendStructImplem(transport, { structType: "field", value: { data, type: typeDescription?.name || "", sizeInBits: typeDescription?.size, }, }); } }; return recursiveFieldStructImplem; }; /** * @ignore for the README * * This method is used to send the message definition with all its types. * This method should be used before the sendStructImplem one * * @param {String} structType * @param {String|Buffer} value * @returns {Promise<void>} */ const sendStructDef = (transport, structDef) => { let APDU_FIELDS; (function (APDU_FIELDS) { APDU_FIELDS[APDU_FIELDS["CLA"] = 224] = "CLA"; APDU_FIELDS[APDU_FIELDS["INS"] = 26] = "INS"; APDU_FIELDS[APDU_FIELDS["P1_complete"] = 0] = "P1_complete"; APDU_FIELDS[APDU_FIELDS["P1_partial"] = 1] = "P1_partial"; APDU_FIELDS[APDU_FIELDS["P2_name"] = 0] = "P2_name"; APDU_FIELDS[APDU_FIELDS["P2_field"] = 255] = "P2_field"; })(APDU_FIELDS || (APDU_FIELDS = {})); const { structType, value } = structDef; const data = structType === "name" && typeof value === "string" ? Buffer.from(value, "utf-8") : value; return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_complete, structType === "name" ? APDU_FIELDS.P2_name : APDU_FIELDS.P2_field, data); }; /** * @ignore for the README * * This method provides a trusted new display name to use for the upcoming field. * This method should be used after the sendStructDef one. * * If the method describes an empty name (length of 0), the upcoming field will be taken * into account but won’t be shown on the device. * * The signature is computed on : * json key length || json key || display name length || display name * * signed by the following secp256k1 public key: * 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353 * * @param {String} structType "root" | "array" | "field" * @param {string | number | StructFieldData} value * @returns {Promise<Buffer | void>} */ const sendStructImplem = async (transport, structImplem) => { let APDU_FIELDS; (function (APDU_FIELDS) { APDU_FIELDS[APDU_FIELDS["CLA"] = 224] = "CLA"; APDU_FIELDS[APDU_FIELDS["INS"] = 28] = "INS"; APDU_FIELDS[APDU_FIELDS["P1_complete"] = 0] = "P1_complete"; APDU_FIELDS[APDU_FIELDS["P1_partial"] = 1] = "P1_partial"; APDU_FIELDS[APDU_FIELDS["P2_root"] = 0] = "P2_root"; APDU_FIELDS[APDU_FIELDS["P2_array"] = 15] = "P2_array"; APDU_FIELDS[APDU_FIELDS["P2_field"] = 255] = "P2_field"; })(APDU_FIELDS || (APDU_FIELDS = {})); const { structType, value } = structImplem; if (structType === "root") { return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_complete, APDU_FIELDS.P2_root, Buffer.from(value, "utf-8")); } if (structType === "array") { return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_complete, APDU_FIELDS.P2_array, Buffer.from((0, utils_1.intAsHexBytes)(value, 1), "hex")); } if (structType === "field") { const { data: rawData, type, sizeInBits } = value; const encodedData = utils_2.EIP712_TYPE_ENCODERS[type.toUpperCase()]?.(rawData, sizeInBits); if (encodedData) { // const dataLengthPer16Bits = (encodedData.length & 0xff00) >> 8; const dataLengthPer16Bits = Math.floor(encodedData.length / 256); // const dataLengthModulo16Bits = encodedData.length & 0xff; const dataLengthModulo16Bits = encodedData.length % 256; const data = Buffer.concat([ Buffer.from((0, utils_1.intAsHexBytes)(dataLengthPer16Bits, 1), "hex"), Buffer.from((0, utils_1.intAsHexBytes)(dataLengthModulo16Bits, 1), "hex"), encodedData, ]); const bufferSlices = new Array(Math.ceil(data.length / 256)) .fill(null) .map((_, i) => data.subarray(i * 255, (i + 1) * 255)); for (const bufferSlice of bufferSlices) { await transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, bufferSlice !== bufferSlices[bufferSlices.length - 1] ? APDU_FIELDS.P1_partial : APDU_FIELDS.P1_complete, APDU_FIELDS.P2_field, bufferSlice); } } } return Promise.resolve(); }; async function sendFilteringInfo(transport, type, loadConfig, data) { let APDU_FIELDS; (function (APDU_FIELDS) { APDU_FIELDS[APDU_FIELDS["CLA"] = 224] = "CLA"; APDU_FIELDS[APDU_FIELDS["INS"] = 30] = "INS"; APDU_FIELDS[APDU_FIELDS["P1_standard"] = 0] = "P1_standard"; APDU_FIELDS[APDU_FIELDS["P1_discarded"] = 1] = "P1_discarded"; APDU_FIELDS[APDU_FIELDS["P2_activate"] = 0] = "P2_activate"; APDU_FIELDS[APDU_FIELDS["P2_discarded"] = 1] = "P2_discarded"; APDU_FIELDS[APDU_FIELDS["P2_show_field"] = 255] = "P2_show_field"; APDU_FIELDS[APDU_FIELDS["P2_message_info"] = 15] = "P2_message_info"; APDU_FIELDS[APDU_FIELDS["P2_datetime"] = 252] = "P2_datetime"; APDU_FIELDS[APDU_FIELDS["P2_amount_join_token"] = 253] = "P2_amount_join_token"; APDU_FIELDS[APDU_FIELDS["P2_amount_join_value"] = 254] = "P2_amount_join_value"; APDU_FIELDS[APDU_FIELDS["P2_raw"] = 255] = "P2_raw"; })(APDU_FIELDS || (APDU_FIELDS = {})); switch (type) { case "activate": return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_discarded, APDU_FIELDS.P2_activate); case "contractName": { const { displayName, filtersCount, sig } = data; const { displayNameBuffer, sigBuffer } = (0, utils_2.getFilterDisplayNameAndSigBuffers)(displayName, sig); const filtersCountBuffer = Buffer.from((0, utils_1.intAsHexBytes)(filtersCount, 1), "hex"); const payload = Buffer.concat([displayNameBuffer, filtersCountBuffer, sigBuffer]); return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_standard, APDU_FIELDS.P2_message_info, payload); } case "showField": { const { displayName, sig, format, coinRef, chainId, coinRefsTokensMap, shouldUseV1Filters, erc20SignaturesBlob, isDiscarded, } = data; const { displayNameBuffer, sigBuffer } = (0, utils_2.getFilterDisplayNameAndSigBuffers)(displayName, sig); if (shouldUseV1Filters) { const payload = Buffer.concat([displayNameBuffer, sigBuffer]); return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_standard, APDU_FIELDS.P2_show_field, payload); } const isTokenAddress = format === "token"; if (isTokenAddress && coinRef !== undefined) { const { token, deviceTokenIndex } = coinRefsTokensMap[coinRef]; if (deviceTokenIndex === undefined) { const payload = await (0, erc20_1.byContractAddressAndChainId)(token, chainId, erc20SignaturesBlob); if (payload) { let PROVIDE_TOKEN_INFOS_APDU_FIELDS; (function (PROVIDE_TOKEN_INFOS_APDU_FIELDS) { PROVIDE_TOKEN_INFOS_APDU_FIELDS[PROVIDE_TOKEN_INFOS_APDU_FIELDS["CLA"] = 224] = "CLA"; PROVIDE_TOKEN_INFOS_APDU_FIELDS[PROVIDE_TOKEN_INFOS_APDU_FIELDS["INS"] = 10] = "INS"; PROVIDE_TOKEN_INFOS_APDU_FIELDS[PROVIDE_TOKEN_INFOS_APDU_FIELDS["P1"] = 0] = "P1"; PROVIDE_TOKEN_INFOS_APDU_FIELDS[PROVIDE_TOKEN_INFOS_APDU_FIELDS["P2"] = 0] = "P2"; })(PROVIDE_TOKEN_INFOS_APDU_FIELDS || (PROVIDE_TOKEN_INFOS_APDU_FIELDS = {})); const response = await transport.send(PROVIDE_TOKEN_INFOS_APDU_FIELDS.CLA, PROVIDE_TOKEN_INFOS_APDU_FIELDS.INS, PROVIDE_TOKEN_INFOS_APDU_FIELDS.P1, PROVIDE_TOKEN_INFOS_APDU_FIELDS.P2, payload.data); coinRefsTokensMap[coinRef].deviceTokenIndex = response[0]; } } } // For some messages like a Permit has no token address in its message, only the amount is provided. // In those cases, we'll need to provide the verifying contract contained in the EIP712 domain // The verifying contract is refrerenced by the coinRef 255 (0xff) in CAL and in the device // independently of the token index returned by the app after a providerERC20TokenInfo const shouldUseVerifyingContract = format === "amount" && coinRef === 255; if (shouldUseVerifyingContract) { const { token } = coinRefsTokensMap[255]; const payload = await (0, erc20_1.byContractAddressAndChainId)(token, chainId, erc20SignaturesBlob); if (payload) { await transport.send(0xe0, 0x0a, 0x00, 0x00, payload.data); coinRefsTokensMap[255].deviceTokenIndex = 255; } } if (!format) { throw new Error("Missing format"); } const P2FormatMap = { raw: APDU_FIELDS.P2_raw, datetime: APDU_FIELDS.P2_datetime, token: APDU_FIELDS.P2_amount_join_token, amount: APDU_FIELDS.P2_amount_join_value, }; const payload = (0, utils_2.getPayloadForFilterV2)(format, coinRef, coinRefsTokensMap, displayNameBuffer, sigBuffer); return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, isDiscarded ? APDU_FIELDS.P1_discarded : APDU_FIELDS.P1_standard, P2FormatMap[format], payload); } case "discardField": { const { path } = data; const pathBuffer = Buffer.from(path); const pathLengthBuffer = Buffer.from((0, utils_1.intAsHexBytes)(pathBuffer.length, 1), "hex"); const payload = Buffer.concat([pathLengthBuffer, pathBuffer]); return transport.send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1_standard, APDU_FIELDS.P2_discarded, payload); } } } /** * @ignore for the README * * Sign an EIP-721 formatted message following the specification here: * https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.asc#sign-eth-eip-712 * @example eth.signEIP721Message("44'/60'/0'/0/0", { domain: { chainId: 69, name: "Da Domain", verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", version: "1" }, types: { "EIP712Domain": [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" } ], "Test": [ { name: "contents", type: "string" } ] }, primaryType: "Test", message: {contents: "Hello, Bob!"}, }) * * @param {String} path derivationPath * @param {Object} typedMessage message to sign * @param {Boolean} fullImplem use the legacy implementation * @returns {Promise} */ const signEIP712Message = async (transport, path, typedMessage, fullImplem = false, loadConfig) => { let APDU_FIELDS; (function (APDU_FIELDS) { APDU_FIELDS[APDU_FIELDS["CLA"] = 224] = "CLA"; APDU_FIELDS[APDU_FIELDS["INS"] = 12] = "INS"; APDU_FIELDS[APDU_FIELDS["P1"] = 0] = "P1"; APDU_FIELDS[APDU_FIELDS["P2_v0"] = 0] = "P2_v0"; APDU_FIELDS[APDU_FIELDS["P2_full"] = 1] = "P2_full"; })(APDU_FIELDS || (APDU_FIELDS = {})); const { primaryType, types: unsortedTypes, domain, message } = typedMessage; const { calServiceURL } = (0, loadConfig_1.getLoadConfig)(loadConfig); // Types are sorted by alphabetical order in order to get the same schema hash no matter the JSON format const types = (0, index_1.sortObjectAlphabetically)(unsortedTypes); const { version } = await (0, utils_2.getAppAndVersion)(transport); const shouldUseV1Filters = !semver_1.default.gte(version, "1.11.1-0", { includePrerelease: true }); const shouldUseDiscardedFields = semver_1.default.gte(version, "1.12.0-0", { includePrerelease: true }); const filters = await (0, index_1.getFiltersForMessage)(typedMessage, shouldUseV1Filters, calServiceURL); const coinRefsTokensMap = (0, utils_2.getCoinRefTokensMap)(filters, shouldUseV1Filters, typedMessage); const typeEntries = Object.entries(types); // Looping on all types entries and fields to send structs' definitions for (const [typeName, entries] of typeEntries) { await sendStructDef(transport, { structType: "name", value: typeName, }); for (const { name, type } of entries) { const typeEntryBuffer = (0, utils_2.makeTypeEntryStructBuffer)({ name, type }); await sendStructDef(transport, { structType: "field", value: typeEntryBuffer, }); } } if (filters) { await sendFilteringInfo(transport, "activate", loadConfig); } const erc20SignaturesBlob = !shouldUseV1Filters ? await (0, erc20_1.findERC20SignaturesInfo)(loadConfig, domain.chainId || 0) : undefined; // Create the recursion that should pass on each entry // of the domain fields and primaryType fields const recursiveFieldStructImplem = makeRecursiveFieldStructImplem({ transport, loadConfig, chainId: domain.chainId || 0, erc20SignaturesBlob, types, filters, shouldUseV1Filters, shouldUseDiscardedFields, coinRefsTokensMap, }); // Looping on all domain type's entries and fields to send // structs' implementations const domainName = "EIP712Domain"; await sendStructImplem(transport, { structType: "root", value: domainName, }); const domainTypeFields = types[domainName]; for (const { name, type } of domainTypeFields) { const domainFieldValue = domain[name]; await recursiveFieldStructImplem((0, utils_2.destructTypeFromString)(type), domainFieldValue); } if (filters) { const { contractName, fields } = filters; const contractNameInfos = { displayName: contractName.label, filtersCount: fields.length, sig: contractName.signature, }; await sendFilteringInfo(transport, "contractName", loadConfig, contractNameInfos); } // Looping on all primaryType type's entries and fields to send // struct' implementations await sendStructImplem(transport, { structType: "root", value: primaryType, }); const primaryTypeFields = types[primaryType]; for (const { name, type } of primaryTypeFields) { const primaryTypeValue = message[name]; await recursiveFieldStructImplem((0, utils_2.destructTypeFromString)(type), primaryTypeValue, name); } // Sending the final signature. const paths = (0, utils_1.splitPath)(path); const signatureBuffer = Buffer.alloc(1 + paths.length * 4); signatureBuffer[0] = paths.length; paths.forEach((element, index) => { signatureBuffer.writeUInt32BE(element, 1 + 4 * index); }); return transport .send(APDU_FIELDS.CLA, APDU_FIELDS.INS, APDU_FIELDS.P1, fullImplem ? APDU_FIELDS.P2_v0 : APDU_FIELDS.P2_full, signatureBuffer) .then(response => { const v = response[0]; const r = response.subarray(1, 1 + 32).toString("hex"); const s = response.subarray(1 + 32, 1 + 32 + 32).toString("hex"); return { v, r, s, }; }); }; exports.signEIP712Message = signEIP712Message; /** * @ignore for the README * Sign a prepared message following web3.eth.signTypedData specification. The host computes the domain separator and hashStruct(message) * @example eth.signEIP712HashedMessage("44'/60'/0'/0/0", Buffer.from("0101010101010101010101010101010101010101010101010101010101010101").toString("hex"), Buffer.from("0202020202020202020202020202020202020202020202020202020202020202").toString("hex")).then(result => { var v = result['v'] - 27; v = v.toString(16); if (v.length < 2) { v = "0" + v; } console.log("Signature 0x" + result['r'] + result['s'] + v); }) */ const signEIP712HashedMessage = (transport, path, domainSeparatorHex, hashStructMessageHex) => { const domainSeparator = (0, utils_1.hexBuffer)(domainSeparatorHex); const hashStruct = (0, utils_1.hexBuffer)(hashStructMessageHex); const paths = (0, utils_1.splitPath)(path); const buffer = Buffer.alloc(1 + paths.length * 4 + 32 + 32, 0); let offset = 0; buffer[0] = paths.length; paths.forEach((element, index) => { buffer.writeUInt32BE(element, 1 + 4 * index); }); offset = 1 + 4 * paths.length; domainSeparator.copy(buffer, offset); offset += 32; hashStruct.copy(buffer, offset); return transport.send(0xe0, 0x0c, 0x00, 0x00, buffer).then(response => { const v = response[0]; const r = response.subarray(1, 1 + 32).toString("hex"); const s = response.subarray(1 + 32, 1 + 32 + 32).toString("hex"); return { v, r, s, }; }); }; exports.signEIP712HashedMessage = signEIP712HashedMessage; //# sourceMappingURL=index.js.map