@ledgerhq/hw-app-btc
Version:
Ledger Hardware Wallet Bitcoin Application API
140 lines • 7.39 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppClient = void 0;
const bip32_1 = require("../bip32");
const merkelizedPsbt_1 = require("./merkelizedPsbt");
const clientCommands_1 = require("./clientCommands");
const varint_1 = require("../varint");
const merkle_1 = require("./merkle");
const CLA_BTC = 0xe1;
const CLA_FRAMEWORK = 0xf8;
const CURRENT_PROTOCOL_VERSION = 1; // supported from version 2.1.0 of the app
var BitcoinIns;
(function (BitcoinIns) {
BitcoinIns[BitcoinIns["GET_PUBKEY"] = 0] = "GET_PUBKEY";
// GET_ADDRESS = 0x01, // Removed from app
BitcoinIns[BitcoinIns["REGISTER_WALLET"] = 2] = "REGISTER_WALLET";
BitcoinIns[BitcoinIns["GET_WALLET_ADDRESS"] = 3] = "GET_WALLET_ADDRESS";
BitcoinIns[BitcoinIns["SIGN_PSBT"] = 4] = "SIGN_PSBT";
BitcoinIns[BitcoinIns["GET_MASTER_FINGERPRINT"] = 5] = "GET_MASTER_FINGERPRINT";
BitcoinIns[BitcoinIns["SIGN_MESSAGE"] = 16] = "SIGN_MESSAGE";
})(BitcoinIns || (BitcoinIns = {}));
var FrameworkIns;
(function (FrameworkIns) {
FrameworkIns[FrameworkIns["CONTINUE_INTERRUPTED"] = 1] = "CONTINUE_INTERRUPTED";
})(FrameworkIns || (FrameworkIns = {}));
/**
* This class encapsulates the APDU protocol documented at
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md
*/
class AppClient {
transport;
constructor(transport) {
this.transport = transport;
}
async makeRequest(ins, data, cci) {
let response = await this.transport.send(CLA_BTC, ins, 0, CURRENT_PROTOCOL_VERSION, data, [0x9000, 0xe000]);
while (response.readUInt16BE(response.length - 2) === 0xe000) {
if (!cci) {
throw new Error("Unexpected SW_INTERRUPTED_EXECUTION");
}
const hwRequest = response.slice(0, -2);
const commandResponse = cci.execute(hwRequest);
response = await this.transport.send(CLA_FRAMEWORK, FrameworkIns.CONTINUE_INTERRUPTED, 0, CURRENT_PROTOCOL_VERSION, commandResponse, [0x9000, 0xe000]);
}
return response.slice(0, -2); // drop the status word (can only be 0x9000 at this point)
}
async getExtendedPubkey(display, pathElements) {
if (pathElements.length > 6) {
throw new Error("Path too long. At most 6 levels allowed.");
}
const response = await this.makeRequest(BitcoinIns.GET_PUBKEY, Buffer.concat([Buffer.from(display ? [1] : [0]), (0, bip32_1.pathElementsToBuffer)(pathElements)]));
return response.toString("ascii");
}
async getWalletAddress(walletPolicy, walletHMAC, change, addressIndex, display) {
if (change !== 0 && change !== 1)
throw new Error("Change can only be 0 or 1");
if (addressIndex < 0 || !Number.isInteger(addressIndex))
throw new Error("Invalid address index");
if (walletHMAC != null && walletHMAC.length != 32) {
throw new Error("Invalid HMAC length");
}
const clientInterpreter = new clientCommands_1.ClientCommandInterpreter(() => { });
clientInterpreter.addKnownList(walletPolicy.keys.map(k => Buffer.from(k, "ascii")));
clientInterpreter.addKnownPreimage(walletPolicy.serialize());
clientInterpreter.addKnownPreimage(Buffer.from(walletPolicy.descriptorTemplate, "ascii"));
const addressIndexBuffer = Buffer.alloc(4);
addressIndexBuffer.writeUInt32BE(addressIndex, 0);
const response = await this.makeRequest(BitcoinIns.GET_WALLET_ADDRESS, Buffer.concat([
Buffer.from(display ? [1] : [0]),
walletPolicy.getWalletId(),
walletHMAC || Buffer.alloc(32, 0),
Buffer.from([change]),
addressIndexBuffer,
]), clientInterpreter);
return response.toString("ascii");
}
async signPsbt(psbt, walletPolicy, walletHMAC, progressCallback) {
const merkelizedPsbt = new merkelizedPsbt_1.MerkelizedPsbt(psbt);
if (walletHMAC != null && walletHMAC.length != 32) {
throw new Error("Invalid HMAC length");
}
const clientInterpreter = new clientCommands_1.ClientCommandInterpreter(progressCallback);
// prepare ClientCommandInterpreter
clientInterpreter.addKnownList(walletPolicy.keys.map(k => Buffer.from(k, "ascii")));
clientInterpreter.addKnownPreimage(walletPolicy.serialize());
clientInterpreter.addKnownPreimage(Buffer.from(walletPolicy.descriptorTemplate, "ascii"));
clientInterpreter.addKnownMapping(merkelizedPsbt.globalMerkleMap);
for (const map of merkelizedPsbt.inputMerkleMaps) {
clientInterpreter.addKnownMapping(map);
}
for (const map of merkelizedPsbt.outputMerkleMaps) {
clientInterpreter.addKnownMapping(map);
}
clientInterpreter.addKnownList(merkelizedPsbt.inputMapCommitments);
const inputMapsRoot = new merkle_1.Merkle(merkelizedPsbt.inputMapCommitments.map(m => (0, merkle_1.hashLeaf)(m))).getRoot();
clientInterpreter.addKnownList(merkelizedPsbt.outputMapCommitments);
const outputMapsRoot = new merkle_1.Merkle(merkelizedPsbt.outputMapCommitments.map(m => (0, merkle_1.hashLeaf)(m))).getRoot();
await this.makeRequest(BitcoinIns.SIGN_PSBT, Buffer.concat([
merkelizedPsbt.getGlobalKeysValuesRoot(),
(0, varint_1.createVarint)(merkelizedPsbt.getGlobalInputCount()),
inputMapsRoot,
(0, varint_1.createVarint)(merkelizedPsbt.getGlobalOutputCount()),
outputMapsRoot,
walletPolicy.getWalletId(),
walletHMAC || Buffer.alloc(32, 0),
]), clientInterpreter);
const yielded = clientInterpreter.getYielded();
const ret = new Map();
for (const inputAndSig of yielded) {
// V2 yield format:
// <inputIndex : varint> <pubkeyLen : 1 byte> <pubkey : pubkeyLen bytes> <signature : variable length>
const [inputIndex, inputIndexLen] = (0, varint_1.getVarint)(inputAndSig, 0);
const pubkeyAugmLen = inputAndSig[inputIndexLen];
const signature = inputAndSig.subarray(inputIndexLen + 1 + pubkeyAugmLen);
ret.set(inputIndex, signature);
}
return ret;
}
async getMasterFingerprint() {
return this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.from([]));
}
async signMessage(message, pathElements) {
if (pathElements.length > 6) {
throw new Error("Path too long. At most 6 levels allowed.");
}
const clientInterpreter = new clientCommands_1.ClientCommandInterpreter(() => { });
// prepare ClientCommandInterpreter
const nChunks = Math.ceil(message.length / 64);
const chunks = [];
for (let i = 0; i < nChunks; i++) {
chunks.push(message.subarray(64 * i, 64 * i + 64));
}
clientInterpreter.addKnownList(chunks);
const chunksRoot = new merkle_1.Merkle(chunks.map(m => (0, merkle_1.hashLeaf)(m))).getRoot();
const response = await this.makeRequest(BitcoinIns.SIGN_MESSAGE, Buffer.concat([(0, bip32_1.pathElementsToBuffer)(pathElements), (0, varint_1.createVarint)(message.length), chunksRoot]), clientInterpreter);
return response.toString("base64");
}
}
exports.AppClient = AppClient;
//# sourceMappingURL=appClient.js.map