UNPKG

@radixdlt/hardware-ledger

Version:
255 lines (252 loc) • 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HardwareWalletLedger = void 0; const rxjs_1 = require("rxjs"); const crypto_1 = require("@radixdlt/crypto"); const operators_1 = require("rxjs/operators"); const util_1 = require("@radixdlt/util"); const hardware_wallet_1 = require("@radixdlt/hardware-wallet"); const apdu_1 = require("./apdu"); const ledgerNano_1 = require("./ledgerNano"); const util_2 = require("@radixdlt/util"); const tx_parser_1 = require("@radixdlt/tx-parser"); const neverthrow_1 = require("neverthrow"); const hardwareError = (message) => new Error(JSON.stringify({ type: 'HARDWARE', message, })); const truncate = (str, n) => str.length > n ? str.slice(0, n) : str; const withLedgerNano = (ledgerNano) => { const getPublicKey = (input) => { var _a, _b, _c; return ledgerNano .sendAPDUToDevice(apdu_1.RadixAPDU.getPublicKey({ path: (_a = input.path) !== null && _a !== void 0 ? _a : hardware_wallet_1.path000H, display: (_b = input.display) !== null && _b !== void 0 ? _b : false, verifyAddressOnly: (_c = input.verifyAddressOnly) !== null && _c !== void 0 ? _c : false, })) .pipe((0, operators_1.mergeMap)((buf) => { if (!Buffer.isBuffer(buf)) { buf = Buffer.from(buf); // Convert Uint8Array to Buffer for Electron renderer compatibility šŸ’© } // Response `buf`: pub_key_len (1) || pub_key (var) || chain_code_len (1) || chain_code (var) const readNextBuffer = (0, util_1.readBuffer)(buf); const publicKeyLengthResult = readNextBuffer(1); if (publicKeyLengthResult.isErr()) { const errMsg = `Failed to parse length of public key from response buffer: ${(0, util_1.msgFromError)(publicKeyLengthResult.error)}`; util_2.log.error(errMsg); return (0, rxjs_1.throwError)(() => hardwareError(errMsg)); } const publicKeyLength = publicKeyLengthResult.value.readUIntBE(0, 1); const publicKeyBytesResult = readNextBuffer(publicKeyLength); if (publicKeyBytesResult.isErr()) { const errMsg = `Failed to parse public key bytes from response buffer: ${(0, util_1.msgFromError)(publicKeyBytesResult.error)}`; util_2.log.error(errMsg); return (0, rxjs_1.throwError)(() => hardwareError(errMsg)); } const publicKeyBytes = publicKeyBytesResult.value; // We ignore remaining bytes, being: `chain_code_len (1) || chain_code (var)` return (0, util_1.toObservableFromResult)(crypto_1.PublicKey.fromBuffer(publicKeyBytes)); })); }; const getVersion = () => ledgerNano .sendAPDUToDevice(apdu_1.RadixAPDU.getVersion()) .pipe((0, operators_1.mergeMap)(buf => (0, util_1.toObservableFromResult)(hardware_wallet_1.SemVer.fromBuffer(buf)))); const parseSignatureFromLedger = (buf) => { // Response `buf`: pub_key_len (1) || pub_key (var) || chain_code_len (1) || chain_code (var) const bufferReader = util_2.BufferReader.create(buf); const signatureDERlengthResult = bufferReader.readNextBuffer(1); if (signatureDERlengthResult.isErr()) { const errMsg = `Failed to parse length of signature from response buffer: ${(0, util_1.msgFromError)(signatureDERlengthResult.error)}`; util_2.log.error(errMsg); return (0, neverthrow_1.err)(hardwareError(errMsg)); } const signatureDERlength = signatureDERlengthResult.value.readUIntBE(0, 1); const signatureDERBytesResult = bufferReader.readNextBuffer(signatureDERlength); if (signatureDERBytesResult.isErr()) { const errMsg = `Failed to parse Signature DER bytes from response buffer: ${(0, util_1.msgFromError)(signatureDERBytesResult.error)}`; util_2.log.error(errMsg); return (0, neverthrow_1.err)(hardwareError(errMsg)); } const signatureDERBytes = signatureDERBytesResult.value; // We ignore remaining bytes, being: `Signature.V (1)` return crypto_1.Signature.fromDER(signatureDERBytes).map(signature => ({ signature, remainingBytes: bufferReader.remainingBytes(), })); }; const doSignHash = (input) => { var _a; return ledgerNano .sendAPDUToDevice(apdu_1.RadixAPDU.doSignHash({ path: (_a = input.path) !== null && _a !== void 0 ? _a : hardware_wallet_1.path000H, hashToSign: input.hashToSign, })) .pipe((0, operators_1.mergeMap)((buf) => (0, util_1.toObservableFromResult)(parseSignatureFromLedger(buf).map(r => r.signature)))); }; const doKeyExchange = (input) => { var _a; return ledgerNano .sendAPDUToDevice(apdu_1.RadixAPDU.doKeyExchange((_a = input.path) !== null && _a !== void 0 ? _a : hardware_wallet_1.path000H, input.publicKeyOfOtherParty, input.display)) .pipe((0, operators_1.mergeMap)((buf) => { // Response `buf`: sharedkeyPointLen (1) || sharedKeyPoint (var) const readNextBuffer = (0, util_1.readBuffer)(buf); const sharedKeyPointLengthResult = readNextBuffer(1); if (sharedKeyPointLengthResult.isErr()) { const errMsg = `Failed to parse length of shared key point from response buffer: ${(0, util_1.msgFromError)(sharedKeyPointLengthResult.error)}`; util_2.log.error(errMsg); return (0, rxjs_1.throwError)(() => hardwareError(errMsg)); } const sharedKeyPointLength = sharedKeyPointLengthResult.value.readUIntBE(0, 1); const sharedKeyPointBytesResult = readNextBuffer(sharedKeyPointLength); if (sharedKeyPointBytesResult.isErr()) { const errMsg = `Failed to parse shared key point bytes from response buffer: ${(0, util_1.msgFromError)(sharedKeyPointBytesResult.error)}`; util_2.log.error(errMsg); return (0, rxjs_1.throwError)(() => hardwareError(errMsg)); } const sharedKeyPointBytes = sharedKeyPointBytesResult.value; return (0, util_1.toObservableFromResult)(crypto_1.ECPointOnCurve.fromBuffer(sharedKeyPointBytes)); })); }; const doSignTransaction = (input) => { var _a; const displayInstructionContentsOnLedgerDevice = true; const displayTXSummaryOnLedgerDevice = true; const subs = new rxjs_1.Subscription(); const transactionRes = tx_parser_1.Transaction.fromBuffer(Buffer.from(input.tx.blob, 'hex')); if (transactionRes.isErr()) { const errMsg = `Failed to parse tx, underlying error: ${(0, util_1.msgFromError)(transactionRes.error)}`; util_2.log.error(errMsg); return (0, rxjs_1.throwError)(() => hardwareError(errMsg)); } const transaction = transactionRes.value; const instructions = transaction.instructions; const numberOfInstructions = instructions.length; const sendInstructionSubject = new rxjs_1.Subject(); const resultBufferFromLedgerSubject = new rxjs_1.Subject(); const outputSubject = new rxjs_1.Subject(); const maxBytesPerExchange = 255; const nextInstructionToSend = () => { const instructionToSend = instructions.shift(); // "pop first" util_2.log.debug(` šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ Sending instruction #${numberOfInstructions - instructions.length}/#${numberOfInstructions}. (length: #${instructionToSend.toBuffer().length} bytes). Raw string representation: " ${instructionToSend.toString()} " Human readable string representation: " ${instructionToSend.toHumanReadableString !== undefined ? instructionToSend.toHumanReadableString() : 'no human readable representation available.'} " Bytes: " ${instructionToSend.toBuffer().toString('hex')} " šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ šŸ“¦ `); return instructionToSend; }; const sendInstruction = () => { sendInstructionSubject.next(nextInstructionToSend()); }; const moreInstructionsToSend = () => instructions.length > 0; subs.add(ledgerNano .sendAPDUToDevice(apdu_1.RadixAPDU.signTX.initialSetup({ path: (_a = input.path) !== null && _a !== void 0 ? _a : hardware_wallet_1.path000H, txByteCount: input.tx.blob.length / 2, numberOfInstructions, nonNativeTokenRriHRP: input.nonXrdHRP ? truncate(input.nonXrdHRP, 11) : undefined, })) .subscribe({ next: _irrelevantBuf => { sendInstruction(); }, error: error => { sendInstructionSubject.error(error); }, })); subs.add(sendInstructionSubject .pipe((0, operators_1.mergeMap)(nextInstruction => { const instructionBytes = nextInstruction.toBuffer(); if (instructionBytes.length > maxBytesPerExchange) { const errMsg = `Failed to send instruction, it is longer than max allowed payload size of ${maxBytesPerExchange}, specifically #${instructionBytes.length} bytes.`; return (0, rxjs_1.throwError)(() => hardwareError(errMsg)); } return (0, rxjs_1.of)(instructionBytes); }), (0, operators_1.mergeMap)((instructionBytes) => { return ledgerNano.sendAPDUToDevice(apdu_1.RadixAPDU.signTX.singleInstruction({ instructionBytes, isLastInstruction: !moreInstructionsToSend(), displayInstructionContentsOnLedgerDevice, displayTXSummaryOnLedgerDevice, })); }), (0, operators_1.tap)({ next: (responseFromLedger) => { if (!moreInstructionsToSend()) { resultBufferFromLedgerSubject.next(responseFromLedger); } else { sendInstruction(); } }, })) .subscribe({ error: (error) => { const errMsg = `Failed to sign tx with Ledger, underlying error while streaming tx bytes: '${(0, util_1.msgFromError)(error)}'`; util_2.log.error(errMsg); outputSubject.error(hardwareError(errMsg)); }, })); subs.add(resultBufferFromLedgerSubject.subscribe({ next: (bytes) => { const parsedResult = parseSignatureFromLedger(bytes); if (!parsedResult.isOk()) { const errMsg = `Failed to parse signature from response from Ledger, underlying error: '${(0, util_1.msgFromError)(parsedResult.error)}'`; util_2.log.error(errMsg); outputSubject.error(hardwareError(errMsg)); return; } const signature = parsedResult.value.signature; const remainingBytes = parsedResult.value.remainingBytes; const signatureV = remainingBytes.readUInt8(0); console.log(`Signature V: ${signatureV}`); const hash = remainingBytes.slice(1); if (hash.length !== 32) { const errMsg = `Expected hash to have 32 bytes length`; util_2.log.error(errMsg); outputSubject.error({ type: 'HARDWARE', error: new Error(errMsg), }); return; } console.log(`Ledger app produced hash: ${hash.toString('hex')}`); outputSubject.next({ signature, signatureV, hashCalculatedByLedger: hash, }); }, })); return outputSubject.asObservable().pipe((0, operators_1.take)(1)); }; const hwWithoutSK = { getPublicKey, getVersion, doSignHash, doKeyExchange, doSignTransaction, }; return Object.assign(Object.assign({}, hwWithoutSK), { makeSigningKey: (path, verificationPrompt) => (0, hardware_wallet_1.signingKeyWithHardWareWallet)(hwWithoutSK, path, verificationPrompt) }); }; const create = (transport) => { const ledgerNano$ = (0, rxjs_1.from)(ledgerNano_1.LedgerNano.connect(transport)); return ledgerNano$.pipe((0, operators_1.map)((ledger) => withLedgerNano(ledger))); }; exports.HardwareWalletLedger = { create, from: withLedgerNano, }; //# sourceMappingURL=hardwareWalletFromLedger.js.map