@radixdlt/hardware-ledger
Version:
Ledger Nano hardware wallet connection
255 lines (252 loc) ⢠12.9 kB
JavaScript
;
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