@zondax/ledger-stacks
Version:
Node API for Stacks App (Ledger Nano S+, X, Stax, Flex and Apex)
370 lines • 17.1 kB
JavaScript
;
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