@citadeldao/hw-app-alamgu
Version:
Common ledgerJS routines for Alamgu apps
370 lines • 17.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
exports.splitPath = exports.buildBip32KeyPayload = exports.Common = void 0;
var fast_sha256_1 = __importDefault(require("fast-sha256"));
/**
* Common API for ledger apps
*
* @example
* import Kadena from "hw-app-kda";
* const kda = new Kadena(transport)
*/
var Common = /** @class */ (function () {
function Common(transport, scrambleKey, appName, verbosity) {
if (appName === void 0) { appName = null; }
if (verbosity === void 0) { verbosity = null; }
this.transport = transport;
this.appName = appName;
this.verbose = verbosity === true;
transport.decorateAppAPIMethods(this, ["menu", "getPublicKey", "signTransaction", "getVersion"], scrambleKey);
}
/**
* Retrieves the public key associated with a particular BIP32 path from the ledger app.
*
* @param path - the path to retrieve.
*/
Common.prototype.getPublicKey = function (path) {
return __awaiter(this, void 0, void 0, function () {
var cla, ins, p1, p2, payload, response, keySize, publicKey, address, addressSize, res;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
cla = 0x00;
ins = 0x02;
p1 = 0;
p2 = 0;
payload = buildBip32KeyPayload(path);
return [4 /*yield*/, this.sendChunks(cla, ins, p1, p2, payload)];
case 1:
response = _a.sent();
keySize = response[0];
publicKey = response.slice(1, keySize + 1);
address = null;
if (response.length > keySize + 2) {
addressSize = response[keySize + 1];
address = response.slice(keySize + 2, keySize + 2 + addressSize);
}
res = {
publicKey: publicKey,
address: address
};
return [2 /*return*/, res];
}
});
});
};
/**
* Sign a transaction with the key at a BIP32 path.
*
* @param txn - The transaction; this can be any of a node Buffer, Uint8Array, or a hexadecimal string, encoding the form of the transaction appropriate for hashing and signing.
* @param path - the path to use when signing the transaction.
*/
Common.prototype.signTransaction = function (path, txn) {
return __awaiter(this, void 0, void 0, function () {
var paths, cla, ins, p1, p2, rawTxn, hashSize, bip32KeyPayload, payload_txn, signature;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
paths = splitPath(path);
cla = 0x00;
ins = 0x03;
p1 = 0;
p2 = 0;
// Transaction payload is the byte length as uint32le followed by the bytes
// Type guard not actually required but TypeScript can't tell that.
if (this.verbose)
this.log(txn);
rawTxn = typeof txn == "string" ? Buffer.from(txn, "hex") : Buffer.from(txn);
hashSize = Buffer.alloc(4);
hashSize.writeUInt32LE(rawTxn.length, 0);
bip32KeyPayload = buildBip32KeyPayload(path);
payload_txn = Buffer.concat([hashSize, rawTxn]);
this.log("Payload Txn", payload_txn);
return [4 /*yield*/, this.sendChunks(cla, ins, p1, p2, [payload_txn, bip32KeyPayload])];
case 1:
signature = _a.sent();
return [2 /*return*/, {
signature: signature
}];
}
});
});
};
/**
* Retrieve the app version on the attached ledger device.
* @alpha TODO this doesn't exist yet
*/
Common.prototype.getVersion = function () {
return __awaiter(this, void 0, void 0, function () {
var _a, major, minor, patch, appName;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, this.sendChunks(0x00, 0x00, 0x00, 0x00, Buffer.alloc(1))];
case 1:
_a = __read.apply(void 0, [_b.sent()]), major = _a[0], minor = _a[1], patch = _a[2], appName = _a.slice(3);
return [2 /*return*/, {
major: major,
minor: minor,
patch: patch
}];
}
});
});
};
/**
* Send a raw payload as chunks to a particular APDU instruction.
*
* @remarks
*
* This is intended to be used to implement a more useful API in this class and subclasses of it, not for end use.
*/
Common.prototype.sendChunks = function (cla, ins, p1, p2, payload) {
return __awaiter(this, void 0, void 0, function () {
var rv, chunkSize, i;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
rv = Buffer.alloc(0);
chunkSize = 230;
if (payload instanceof Array) {
payload = Buffer.concat(payload);
}
i = 0;
_a.label = 1;
case 1:
if (!(i < payload.length)) return [3 /*break*/, 4];
return [4 /*yield*/, this.transport.send(cla, ins, p1, p2, payload.slice(i, i + chunkSize))];
case 2:
rv = _a.sent();
_a.label = 3;
case 3:
i += chunkSize;
return [3 /*break*/, 1];
case 4:
// Remove the status code here instead of in signTransaction, because sendWithBlocks _has_ to handle it.
return [2 /*return*/, rv.slice(0, -2)];
}
});
});
};
/**
* Convert a raw payload into what is essentially a singly-linked list of chunks, which
allows the ledger to re-seek the data in a secure fashion.
*/
Common.prototype.sendWithBlocks = function (cla, ins, p1, p2, payload,
// Constant (protocol dependent) data that the ledger may want to refer to
// besides the payload.
extraData) {
if (extraData === void 0) { extraData = new Map(); }
return __awaiter(this, void 0, void 0, function () {
var rv, chunkSize, parameterList, data, _loop_1, this_1, j;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
chunkSize = 180;
if (!(payload instanceof Array)) {
payload = [payload];
}
parameterList = [];
data = new Map(extraData);
_loop_1 = function (j) {
var chunkList = [];
for (var i = 0; i < payload[j].length; i += chunkSize) {
var cur = payload[j].slice(i, i + chunkSize);
chunkList.push(cur);
}
// Store the hash that points to the "rest of the list of chunks"
var lastHash = Buffer.alloc(32);
this_1.log(lastHash);
// Since we are doing a foldr, we process the last chunk first
// We have to do it this way, because a block knows the hash of
// the next block.
data = chunkList.reduceRight(function (blocks, chunk) {
var linkedChunk = Buffer.concat([lastHash, chunk]);
_this.log("Chunk: ", chunk);
_this.log("linkedChunk: ", linkedChunk);
lastHash = Buffer.from((0, fast_sha256_1["default"])(linkedChunk));
blocks.set(lastHash.toString('hex'), linkedChunk);
return blocks;
}, data);
parameterList.push(lastHash);
lastHash = Buffer.alloc(32);
};
this_1 = this;
for (j = 0; j < payload.length; j++) {
_loop_1(j);
}
this.log(data);
return [4 /*yield*/, this.handleBlocksProtocol(cla, ins, p1, p2, Buffer.concat([Buffer.from([HostToLedger.START])].concat(parameterList)), data)];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
};
Common.prototype.handleBlocksProtocol = function (cla, ins, p1, p2, initialPayload, data) {
return __awaiter(this, void 0, void 0, function () {
var payload, result, rv, rv_instruction, rv_payload, chunk;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
payload = initialPayload;
result = Buffer.alloc(0);
_a.label = 1;
case 1:
this.log("Sending payload to ledger: ", payload.toString('hex'));
return [4 /*yield*/, this.transport.send(cla, ins, p1, p2, payload)];
case 2:
rv = _a.sent();
this.log("Received response: ", rv);
rv_instruction = rv[0];
rv_payload = rv.slice(1, rv.length - 2);
if (!(rv_instruction in LedgerToHost)) {
throw new TypeError("Unknown instruction returned from ledger");
}
switch (rv_instruction) {
case LedgerToHost.RESULT_ACCUMULATING:
case LedgerToHost.RESULT_FINAL:
result = Buffer.concat([result, rv_payload]);
// Won't actually send this if we drop out of the loop for RESULT_FINAL
payload = Buffer.from([HostToLedger.RESULT_ACCUMULATING_RESPONSE]);
break;
case LedgerToHost.GET_CHUNK:
chunk = data.get(rv_payload.toString('hex'));
this.log("Getting block ", rv_payload);
this.log("Found block ", chunk);
if (chunk) {
payload = Buffer.concat([Buffer.from([HostToLedger.GET_CHUNK_RESPONSE_SUCCESS]), chunk]);
}
else {
payload = Buffer.from([HostToLedger.GET_CHUNK_RESPONSE_FAILURE]);
}
break;
case LedgerToHost.PUT_CHUNK:
data.set(Buffer.from((0, fast_sha256_1["default"])(rv_payload)).toString('hex'), rv_payload);
payload = Buffer.from([HostToLedger.PUT_CHUNK_RESPONSE]);
break;
}
_a.label = 3;
case 3:
if (rv_instruction != 1) return [3 /*break*/, 1];
_a.label = 4;
case 4: return [2 /*return*/, result];
}
});
});
};
Common.prototype.log = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (this.verbose)
console.log(args);
};
return Common;
}());
exports.Common = Common;
var LedgerToHost;
(function (LedgerToHost) {
LedgerToHost[LedgerToHost["RESULT_ACCUMULATING"] = 0] = "RESULT_ACCUMULATING";
LedgerToHost[LedgerToHost["RESULT_FINAL"] = 1] = "RESULT_FINAL";
LedgerToHost[LedgerToHost["GET_CHUNK"] = 2] = "GET_CHUNK";
LedgerToHost[LedgerToHost["PUT_CHUNK"] = 3] = "PUT_CHUNK";
})(LedgerToHost || (LedgerToHost = {}));
;
var HostToLedger;
(function (HostToLedger) {
HostToLedger[HostToLedger["START"] = 0] = "START";
HostToLedger[HostToLedger["GET_CHUNK_RESPONSE_SUCCESS"] = 1] = "GET_CHUNK_RESPONSE_SUCCESS";
HostToLedger[HostToLedger["GET_CHUNK_RESPONSE_FAILURE"] = 2] = "GET_CHUNK_RESPONSE_FAILURE";
HostToLedger[HostToLedger["PUT_CHUNK_RESPONSE"] = 3] = "PUT_CHUNK_RESPONSE";
HostToLedger[HostToLedger["RESULT_ACCUMULATING_RESPONSE"] = 4] = "RESULT_ACCUMULATING_RESPONSE";
})(HostToLedger || (HostToLedger = {}));
;
function buildBip32KeyPayload(path) {
var paths = splitPath(path);
// Bip32Key payload is:
// 1 byte with number of elements in u32 array path
// Followed by the u32 array itself
var payload = Buffer.alloc(1 + paths.length * 4);
payload[0] = paths.length;
paths.forEach(function (element, index) {
payload.writeUInt32LE(element, 1 + 4 * index);
});
return payload;
}
exports.buildBip32KeyPayload = buildBip32KeyPayload;
// TODO use bip32-path library
function splitPath(path) {
var result = [];
var components = path.split("/");
components.forEach(function (element) {
var number = parseInt(element, 10);
if (isNaN(number)) {
return; // FIXME shouldn't it throws instead?
}
if (element.length > 1 && element[element.length - 1] === "'") {
number += 0x80000000;
}
result.push(number);
});
return result;
}
exports.splitPath = splitPath;
//# sourceMappingURL=Common.js.map