@zondax/ledger-lisk
Version:
Node API for the Lisk App (Ledger Nano S/X/S+)
297 lines (296 loc) • 12.3 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.LedgerAccount = exports.LiskApp = exports.LedgerError = void 0;
const common_1 = require("./common");
Object.defineProperty(exports, "LedgerError", { enumerable: true, get: function () { return common_1.LedgerError; } });
const config_1 = require("./config");
__exportStar(require("./types"), exports);
function processGetAddrResponse(response) {
const errorCodeData = response.subarray(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const pubKey = response.subarray(0, config_1.PKLEN).toString("hex");
const address = response.subarray(config_1.PKLEN, response.length - 2).toString("ascii");
return {
pubKey,
address,
return_code: returnCode,
error_message: (0, common_1.errorCodeToString)(returnCode),
};
}
class LiskApp {
constructor(transport) {
if (!transport) {
throw new Error("Transport has not been defined");
}
this.transport = transport;
}
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.subarray(i, end));
}
return chunks;
}
async signGetChunks(path, message) {
return LiskApp.prepareChunks((0, common_1.serializePath)(path), message);
}
async getVersion() {
return await (0, common_1.getVersion)(this.transport).catch((err) => (0, common_1.processErrorResponse)(err));
}
async getAppInfo() {
return await this.transport.send(0xb0, 0x01, 0, 0).then((response) => {
const errorCodeData = response.subarray(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
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];
appName = response.subarray(2, 2 + appNameLen).toString("ascii");
let idx = 2 + appNameLen;
const appVersionLen = response[idx];
idx += 1;
appVersion = response.subarray(idx, idx + appVersionLen).toString("ascii");
idx += appVersionLen;
const appFlagsLen = response[idx];
idx += 1;
flagLen = appFlagsLen;
flagsValue = response[idx];
}
return {
appName,
appVersion,
flagLen,
flagsValue,
flagRecovery: (flagsValue & 1) !== 0,
// eslint-disable-next-line no-bitwise
flagSignedMcuCode: (flagsValue & 2) !== 0,
// eslint-disable-next-line no-bitwise
flagOnboarded: (flagsValue & 4) !== 0,
// eslint-disable-next-line no-bitwise
flagPINValidated: (flagsValue & 128) !== 0,
return_code: returnCode,
error_message: (0, common_1.errorCodeToString)(returnCode),
};
}, common_1.processErrorResponse);
}
async deviceInfo() {
return await this.transport
.send(0xe0, 0x01, 0, 0, Buffer.from([]), [common_1.LedgerError.NoErrors, common_1.LedgerError.AppDoesNotSeemToBeOpen])
.then((response) => {
const errorCodeData = response.subarray(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const targetId = response.subarray(0, 4).toString("hex");
let pos = 4;
const secureElementVersionLen = response[pos];
pos += 1;
const seVersion = response.subarray(pos, pos + secureElementVersionLen).toString();
pos += secureElementVersionLen;
const flagsLen = response[pos];
pos += 1;
const flag = response.subarray(pos, pos + flagsLen).toString("hex");
pos += flagsLen;
const mcuVersionLen = response[pos];
pos += 1;
// Patch issue in mcu version
let tmp = response.subarray(pos, pos + mcuVersionLen);
if (tmp[mcuVersionLen - 1] === 0) {
tmp = response.subarray(pos, pos + mcuVersionLen - 1);
}
const mcuVersion = tmp.toString();
return {
targetId,
seVersion,
flag,
mcuVersion,
return_code: returnCode,
error_message: (0, common_1.errorCodeToString)(returnCode),
};
}, common_1.processErrorResponse);
}
async getAddressAndPubKey(path) {
const serializedPath = (0, common_1.serializePath)(path);
return await this.transport
.send(config_1.CLA, 1 /* INS.GET_ADDR_PUBKEY */, common_1.P1_VALUES.ONLY_RETRIEVE, 0, serializedPath, [common_1.LedgerError.NoErrors])
.then(processGetAddrResponse, common_1.processErrorResponse);
}
async getMultipleAddresses(indexes) {
if (indexes.length === 0)
throw new Error("Indexes array must not be empty");
const addresses = {
return_code: common_1.LedgerError.NoErrors,
error_message: (0, common_1.errorCodeToString)(common_1.LedgerError.NoErrors),
addr: {},
};
for (const index of indexes) {
const addr = await this.getAddressAndPubKey(`m/44'/134'/${index}'`);
if (addr.return_code !== common_1.LedgerError.NoErrors)
return { return_code: addr.return_code, error_message: addr.error_message, addr: [] };
addresses.addr[index] = { pubKey: addr.pubKey, address: addr.address };
}
return addresses;
}
async showAddressAndPubKey(path) {
const serializedPath = (0, common_1.serializePath)(path);
return await this.transport
.send(config_1.CLA, 1 /* INS.GET_ADDR_PUBKEY */, common_1.P1_VALUES.SHOW_ADDRESS_IN_DEVICE, 0, serializedPath, [common_1.LedgerError.NoErrors])
.then(processGetAddrResponse, common_1.processErrorResponse);
}
async 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;
}
// Check supported sign instructions
if (ins !== 2 /* INS.SIGN_TXN */) {
// Error here
}
return await this.transport
.send(config_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.subarray(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
let errorMessage = (0, common_1.errorCodeToString)(returnCode);
if (returnCode === common_1.LedgerError.BadKeyHandle ||
returnCode === common_1.LedgerError.DataIsInvalid ||
returnCode === common_1.LedgerError.SignVerifyError) {
errorMessage = `${errorMessage} : ${response.subarray(0, response.length - 2).toString("ascii")}`;
}
if (returnCode === common_1.LedgerError.NoErrors && response.length > 2) {
const signature = response.subarray(0, response.length - 2);
return {
signature,
return_code: returnCode,
error_message: errorMessage,
};
}
return {
return_code: returnCode,
error_message: errorMessage,
};
}, common_1.processErrorResponse);
}
async signImpl(path, message, ins) {
return await this.signGetChunks(path, message).then(async (chunks) => {
return await this.signSendChunk(1, chunks.length, chunks[0], ins).then(async (result) => {
for (let i = 1; i < chunks.length; i += 1) {
// eslint-disable-next-line no-await-in-loop,no-param-reassign
result = await this.signSendChunk(1 + i, chunks.length, chunks[i], ins);
if (result.return_code !== common_1.ERROR_CODE.NoError) {
break;
}
}
return {
return_code: result.return_code,
error_message: result.error_message,
signature: result.signature,
};
}, common_1.processErrorResponse);
});
}
async sign(path, message) {
return await this.signImpl(path, message, 2 /* INS.SIGN_TXN */);
}
async signMessage(path, message) {
return await this.signImpl(path, message, 3 /* INS.SIGN_MSG */);
}
async claimMessage(path, message) {
return await this.signImpl(path, message, 4 /* INS.CLAIM_MSG */);
}
}
exports.LiskApp = LiskApp;
/**
* Class to specify An account used to query the ledger.
*/
var SupportedCoin;
(function (SupportedCoin) {
/**
* @see https://lisk.io
*/
SupportedCoin[SupportedCoin["LISK"] = 134] = "LISK";
})(SupportedCoin || (SupportedCoin = {}));
class LedgerAccount {
constructor() {
this._account = 0;
this._coinIndex = SupportedCoin.LISK; // LISK
}
/**
* Specify the account number
* @param {number} newAccount
* @returns {this}
*/
account(newAccount) {
this.assertValidPath(newAccount);
this._account = newAccount;
return this;
}
/**
* Specify the coin index. At the moment will force alwais Lisk.
* @see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
* @param {number} newIndex
* @returns {this}
*/
coinIndex(newIndex) {
this.assertValidPath(newIndex);
// this._coinIndex = newIndex;
this._coinIndex = SupportedCoin.LISK;
return this;
}
/**
* Derive the path using hardened entries.
* @returns {string} defines the path in buffer form.
*/
derivePath() {
const pathArray = `m/44'/${this._coinIndex}'/${this._account}'`;
return pathArray;
}
/**
* Asserts that the given param is a valid path (integer > 0)
*/
assertValidPath(n) {
if (!Number.isInteger(n)) {
throw new Error("Param must be an integer");
}
if (n < 0) {
throw new Error("Param must be greater than zero");
}
}
}
exports.LedgerAccount = LedgerAccount;