UNPKG

@zondax/ledger-stacks

Version:

Node API for Stacks App (Ledger Nano S+, X, Stax, Flex and Apex)

370 lines 17.1 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.LedgerError = void 0; const varuint_bitcoin_1 = require("varuint-bitcoin"); const common_1 = require("./common"); Object.defineProperty(exports, "LedgerError", { enumerable: true, get: function () { return common_1.LedgerError; } }); const helper_1 = require("./helper"); __exportStar(require("./types"), exports); function processGetAddrResponse(response) { let partialResponse = response; const errorCodeData = partialResponse.slice(-2); const returnCode = (errorCodeData[0] ?? 0) * 256 + (errorCodeData[1] ?? 0); const publicKey = Buffer.from(partialResponse.slice(0, common_1.PKLEN)); partialResponse = partialResponse.slice(common_1.PKLEN); const address = Buffer.from(partialResponse.slice(0, -2)).toString(); return { publicKey, address, returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), }; } // Builds the chunked GET_ADDR_MULTISIG payload. Larger key sets (up to n = 15) // exceed a single APDU, so it is sent over chunked transport: // INIT chunk : path(20) -- the device's own path // ADD/LAST : confirm(1) | version(1) | hash_mode(1) | m(1) | n(1) | device_index(1) // | cosigner keys ((n-1) * 33) -- split into CHUNK_SIZE pieces // The device derives its own key and splices it in at `deviceKeyIndex`, so the // caller only supplies the OTHER cosigner keys (ordered, device slot omitted). function serializeMultisigChunks(path, version, options, confirm) { const { numRequired, deviceKeyIndex, cosignerPublicKeys } = options; const hashMode = options.hashMode ?? common_1.MULTISIG_HASH_MODE.P2SH; const cosigners = cosignerPublicKeys.map(k => (Buffer.isBuffer(k) ? k : Buffer.from(k, 'hex'))); const numPubkeys = cosigners.length + 1; if (numPubkeys < 1 || numPubkeys > common_1.MULTISIG_MAX_PUBKEYS) { throw new Error(`Unsupported number of keys: ${numPubkeys} (1..${common_1.MULTISIG_MAX_PUBKEYS})`); } if (numRequired < 1 || numRequired > numPubkeys) { throw new Error(`Invalid threshold: ${numRequired} of ${numPubkeys}`); } if (deviceKeyIndex < 0 || deviceKeyIndex >= numPubkeys) { throw new Error(`deviceKeyIndex ${deviceKeyIndex} out of range (0..${numPubkeys - 1})`); } cosigners.forEach((k, i) => { if (k.length !== common_1.PKLEN) { throw new Error(`Cosigner key ${i} must be a ${common_1.PKLEN}-byte compressed public key`); } }); const header = Buffer.from([confirm ? 1 : 0, version, hashMode, numRequired, numPubkeys, deviceKeyIndex]); const body = Buffer.concat([header, ...cosigners]); // INIT chunk carries the path; the header + keys are chunked into the rest. const chunks = [(0, helper_1.serializePath)(path)]; for (let i = 0; i < body.length; i += common_1.CHUNK_SIZE) { chunks.push(body.subarray(i, i + common_1.CHUNK_SIZE)); } return chunks; } class StacksApp { constructor(transport) { this.transport = transport; if (!transport) { throw new Error('Transport has not been defined'); } } 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.slice(i, end)); } return chunks; } signGetChunks(path, message) { return StacksApp.prepareChunks((0, helper_1.serializePath)(path), message); } getVersion() { return (0, common_1.getVersion)(this.transport).catch(err => (0, common_1.processErrorResponse)(err)); } getAppInfo() { return this.transport.send(0xb0, 0x01, 0, 0).then(response => { const errorCodeData = response.slice(-2); const returnCode = (errorCodeData[0] ?? 0) * 256 + (errorCodeData[1] ?? 0); 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] ?? 0; appName = response.slice(2, 2 + appNameLen).toString('ascii'); let idx = 2 + appNameLen; const appVersionLen = response[idx] ?? 0; idx += 1; appVersion = response.slice(idx, idx + appVersionLen).toString('ascii'); idx += appVersionLen; const appFlagsLen = response[idx] ?? 0; idx += 1; flagLen = appFlagsLen; flagsValue = response[idx] ?? 0; } 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); } getAddressAndPubKey(path, version) { const serializedPath = (0, helper_1.serializePath)(path); return this.transport .send(common_1.CLA, common_1.INS.GET_ADDR_SECP256K1, common_1.P1_VALUES.ONLY_RETRIEVE, version, serializedPath, [0x9000]) .then(processGetAddrResponse, common_1.processErrorResponse); } getIdentityPubKey(path) { const serializedPath = (0, helper_1.serializePath)(path); return this.transport .send(common_1.CLA, common_1.INS.GET_AUTH_PUBKEY, common_1.P1_VALUES.ONLY_RETRIEVE, 0, serializedPath, [0x9000]) .then(processGetAddrResponse, common_1.processErrorResponse); } getMasterFingerprint() { return this.transport.send(common_1.CLA, common_1.INS.GET_MASTER_FINGERPRINT, 0, 0, Buffer.alloc(0), [common_1.LedgerError.NoErrors]).then((response) => { const errorCodeData = response.slice(-2); const returnCode = (errorCodeData[0] ?? 0) * 256 + (errorCodeData[1] ?? 0); if (returnCode !== common_1.LedgerError.NoErrors) { return { returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), fingerprint: Buffer.alloc(0), }; } const fingerprint = response.slice(0, 4); // Master fingerprint is 4 bytes return { returnCode, errorMessage: (0, common_1.errorCodeToString)(returnCode), fingerprint, }; }, common_1.processErrorResponse); } showAddressAndPubKey(path, version) { const serializedPath = (0, helper_1.serializePath)(path); return this.transport .send(common_1.CLA, common_1.INS.GET_ADDR_SECP256K1, common_1.P1_VALUES.SHOW_ADDRESS_IN_DEVICE, version, serializedPath, [common_1.LedgerError.NoErrors]) .then(processGetAddrResponse, common_1.processErrorResponse); } /** * Derive a multisig (P2SH) address. The device derives its own key from * `path` and combines it with the supplied cosigner keys to compute the * address; the response's `publicKey` is this device's own key. * * `version` is the c32 multisig version byte (20 mainnet `SM…`, 21 testnet `SN…`). */ getMultisigAddressAndPubKey(path, version, options) { const chunks = serializeMultisigChunks(path, version, options, false); return this.sendMultisigChunks(chunks); } /** Same as {@link getMultisigAddressAndPubKey} but shows the address on-device for verification. */ showMultisigAddressAndPubKey(path, version, options) { const chunks = serializeMultisigChunks(path, version, options, true); return this.sendMultisigChunks(chunks); } // Sends the multisig chunks in order (INIT, ADD…, LAST) and parses the final // response ([device pubkey || c32 address]). P2 is 0; the chunk type is in P1. sendMultisigChunks(chunks) { const send = async () => { // INIT chunk (the path); intermediate responses are just SW=9000. let response = await this.transport.send(common_1.CLA, common_1.INS.GET_ADDR_MULTISIG, common_1.PAYLOAD_TYPE.INIT, 0, chunks[0], [common_1.LedgerError.NoErrors]); for (let i = 1; i < chunks.length; i += 1) { const payloadType = i === chunks.length - 1 ? common_1.PAYLOAD_TYPE.LAST : common_1.PAYLOAD_TYPE.ADD; response = await this.transport.send(common_1.CLA, common_1.INS.GET_ADDR_MULTISIG, payloadType, 0, chunks[i], [common_1.LedgerError.NoErrors]); } return response; }; return send().then(processGetAddrResponse, common_1.processErrorResponse); } 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; } return this.transport .send(common_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.slice(-2); const returnCode = (errorCodeData[0] ?? 0) * 256 + (errorCodeData[1] ?? 0); let errorMessage = (0, common_1.errorCodeToString)(returnCode); let postSignHash = Buffer.alloc(0); let signatureCompact = Buffer.alloc(0); let signatureVRS = Buffer.alloc(0); let signatureDER = Buffer.alloc(0); 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) { postSignHash = response.slice(0, 32); signatureCompact = response.slice(32, 97); signatureVRS = Buffer.alloc(65); signatureVRS[0] = signatureCompact[signatureCompact.length - 1] ?? 0; Buffer.from(signatureCompact).copy(signatureVRS, 1, 0, 64); signatureDER = response.slice(97, response.length - 2); return { postSignHash, signatureCompact, signatureVRS, signatureDER, returnCode: returnCode, errorMessage: errorMessage, }; } return { returnCode: returnCode, errorMessage: errorMessage, }; }, common_1.processErrorResponse); } async sign(path, message) { try { const chunks = this.signGetChunks(path, message); let result = { returnCode: 0, errorMessage: '', postSignHash: null, signatureCompact: null, signatureDER: null, }; const response = await this.signSendChunk(1, chunks.length, chunks[0], common_1.INS.SIGN_SECP256K1); result.returnCode = response.returnCode; result.errorMessage = response.errorMessage; for (let i = 1; i < chunks.length; i += 1) { result = await this.signSendChunk(1 + i, chunks.length, chunks[i], common_1.INS.SIGN_SECP256K1); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } return result; } catch (e) { return (0, common_1.processErrorResponse)(e); } } async sign_msg(path, message) { try { const len = Buffer.from((0, varuint_bitcoin_1.encode)(message.length).buffer); const stacks_message = '\x17Stacks Signed Message:\n'; const blob = Buffer.concat([Buffer.from(stacks_message), len, Buffer.from(message)]); const ins = common_1.INS.SIGN_SECP256K1; const chunks = this.signGetChunks(path, blob); let result = { returnCode: 0, errorMessage: '', postSignHash: null, signatureCompact: null, signatureDER: null, }; const response = await this.signSendChunk(1, chunks.length, chunks[0], ins); result.returnCode = response.returnCode; result.errorMessage = response.errorMessage; for (let i = 1; i < chunks.length; i += 1) { result = await this.signSendChunk(1 + i, chunks.length, chunks[i], ins); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } return result; } catch (e) { return (0, common_1.processErrorResponse)(e); } } async sign_jwt(path, message) { try { const blob = Buffer.from(message); const ins = common_1.INS.SIGN_JWT_SECP256K1; const chunks = this.signGetChunks(path, blob); let result = { returnCode: 0, errorMessage: '', postSignHash: null, signatureCompact: null, signatureDER: null, }; const response = await this.signSendChunk(1, chunks.length, chunks[0], ins); result.returnCode = response.returnCode; result.errorMessage = response.errorMessage; for (let i = 1; i < chunks.length; i += 1) { result = await this.signSendChunk(1 + i, chunks.length, chunks[i], ins); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } return result; } catch (e) { return (0, common_1.processErrorResponse)(e); } } async sign_structured_msg(path, domain, message) { try { const header = 'SIP018'; const blob = Buffer.concat([Buffer.from(header), Buffer.from(domain, 'hex'), Buffer.from(message, 'hex')]); const ins = common_1.INS.SIGN_SECP256K1; const chunks = this.signGetChunks(path, blob); let result = { returnCode: 0, errorMessage: '', postSignHash: null, signatureCompact: null, signatureDER: null, }; const response = await this.signSendChunk(1, chunks.length, chunks[0], ins); result.returnCode = response.returnCode; result.errorMessage = response.errorMessage; for (let i = 1; i < chunks.length; i += 1) { result = await this.signSendChunk(1 + i, chunks.length, chunks[i], ins); if (result.returnCode !== common_1.LedgerError.NoErrors) { break; } } return result; } catch (e) { return (0, common_1.processErrorResponse)(e); } } } exports.default = StacksApp; //# sourceMappingURL=index.js.map