@zondax/ledger-js
Version:
TS / Node API for apps running on Ledger devices
250 lines • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const bip32_1 = require("./bip32");
const common_1 = require("./common");
const consts_1 = require("./consts");
const responseError_1 = require("./responseError");
/**
* Base class for interacting with a Ledger device.
*/
class BaseApp {
/**
* Constructs a new BaseApp instance.
* @param transport - The transport mechanism to communicate with the device.
* @param params - The constructor parameters.
*/
constructor(transport, params) {
if (transport == null) {
throw new Error('Transport has not been defined');
}
this.transport = transport;
this.CLA = params.cla;
this.INS = params.ins;
this.P1_VALUES = params.p1Values;
this.CHUNK_SIZE = params.chunkSize;
this.REQUIRED_PATH_LENGTHS = params.acceptedPathLengths;
this.CUSTOM_APP_ERROR_DESCRIPTION = params.customAppErrorDescription;
}
/**
* Serializes a derivation path into a buffer.
* @param path - The derivation path in string format.
* @returns A buffer representing the serialized path.
*/
serializePath(path) {
return (0, bip32_1.serializePath)(path, this.REQUIRED_PATH_LENGTHS);
}
/**
* Prepares chunks of data to be sent to the device.
* @param path - The derivation path.
* @param message - The message to be sent.
* @returns An array of buffers that are ready to be sent.
*/
prepareChunks(path, message) {
const serializedPathBuffer = this.serializePath(path);
const chunks = this.messageToChunks(message);
chunks.unshift(serializedPathBuffer);
return chunks;
}
/**
* Splits a buffer into chunks of `this.CHUNK_SIZE` size.
* @param message - The message to be chunked.
* @returns An array of buffers, each representing a chunk of the original message.
*/
messageToChunks(message) {
const chunks = [];
const messageBuffer = Buffer.from(message);
for (let i = 0; i < messageBuffer.length; i += this.CHUNK_SIZE) {
const end = Math.min(i + this.CHUNK_SIZE, messageBuffer.length);
chunks.push(messageBuffer.subarray(i, end));
}
return chunks;
}
/**
* Sends a chunk of data to the device and handles the response.
* Determines the payload type based on the chunk index and sends the chunk to the device.
* Processes the response from the device.
*
* @param ins - The instruction byte.
* @param p2 - P2 parameter byte.
* @param chunkIdx - The current chunk index.
* @param chunkNum - The total number of chunks.
* @param chunk - The chunk data as a buffer.
* @returns A promise that resolves to the processed response from the device.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async sendGenericChunk(ins, p2, chunkIdx, chunkNum, chunk) {
let payloadType = consts_1.PAYLOAD_TYPE.ADD;
if (chunkIdx === 1) {
payloadType = consts_1.PAYLOAD_TYPE.INIT;
}
if (chunkIdx === chunkNum) {
payloadType = consts_1.PAYLOAD_TYPE.LAST;
}
const statusList = [consts_1.LedgerError.NoErrors, consts_1.LedgerError.DataIsInvalid, consts_1.LedgerError.BadKeyHandle];
let responseBuffer;
try {
responseBuffer = await this.transport.send(this.CLA, ins, payloadType, p2, chunk, statusList);
}
catch (e) {
// In case transport.send send throws an Error, we still want our custom ResponseError
let statusCode = e.statusCode || e.returnCode;
const buffer = Buffer.alloc(2);
buffer.writeUInt16BE(statusCode, 0);
return (0, common_1.processResponse)(buffer, this.CUSTOM_APP_ERROR_DESCRIPTION);
}
return (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION);
}
/**
* Sends a chunk of data to the device and handles the response.
* This method determines the payload type based on the chunk index and sends the chunk to the device.
* It then processes the response from the device.
*
* @param ins - The instruction byte.
* @param chunkIdx - The current chunk index.
* @param chunkNum - The total number of chunks.
* @param chunk - The chunk data as a buffer.
* @returns A promise that resolves to the processed response from the device.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async signSendChunk(ins, chunkIdx, chunkNum, chunk) {
return this.sendGenericChunk(ins, 0, chunkIdx, chunkNum, chunk);
}
/**
* Retrieves the version information from the device.
* @returns A promise that resolves to the version information.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async getVersion() {
try {
const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_VERSION, 0, 0);
const response = (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION);
// valid options are
// test mode: 1 byte
// major, minor, patch: 3 byte total
// device locked: 1 byte
// targetId: 4 bytes
// total: 5 or 9 bytes
// -----
// test mode: 1 byte
// major, minor, patch: 6 byte total
// device locked: 1 byte
// targetId: 4 bytes
// total: 8 or 12 bytes
// -----
// test mode: 1 byte
// major, minor, patch: 12 byte total
// device locked: 1 byte
// targetId: 4 bytes
// total: 14 or 18 bytes
let testMode;
let major, minor, patch;
if (response.length() === 5 || response.length() === 9) {
testMode = response.readBytes(1).readUInt8() !== 0;
major = response.readBytes(1).readUInt8();
minor = response.readBytes(1).readUInt8();
patch = response.readBytes(1).readUInt8();
}
else if (response.length() === 8 || response.length() === 12) {
testMode = response.readBytes(1).readUInt8() !== 0;
major = response.readBytes(2).readUInt16BE();
minor = response.readBytes(2).readUInt16BE();
patch = response.readBytes(2).readUInt16BE();
}
else if (response.length() === 14 || response.length() === 18) {
testMode = response.readBytes(1).readUInt8() !== 0;
major = response.readBytes(4).readUInt32BE();
minor = response.readBytes(4).readUInt32BE();
patch = response.readBytes(4).readUInt32BE();
}
else {
throw new responseError_1.ResponseError(consts_1.LedgerError.TechnicalProblem, 'Invalid response length');
}
const deviceLocked = response.readBytes(1).readUInt8() === 1;
let targetId = '';
if (response.length() >= 4) {
targetId = response.readBytes(4).readUInt32BE().toString(16).padStart(8, '0');
}
return {
testMode,
major,
minor,
patch,
deviceLocked,
targetId,
};
}
catch (error) {
throw (0, common_1.processErrorResponse)(error);
}
}
/**
* Retrieves application information from the device.
* @returns A promise that resolves to the application information.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async appInfo() {
try {
const responseBuffer = await this.transport.send(consts_1.LEDGER_DASHBOARD_CLA, 0x01, 0, 0);
const response = (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION);
const formatId = response.readBytes(1).readUInt8();
if (formatId !== 1) {
throw new responseError_1.ResponseError(consts_1.LedgerError.TechnicalProblem, 'Format ID not recognized');
}
const appNameLen = response.readBytes(1).readUInt8();
const appName = response.readBytes(appNameLen).toString('ascii');
const appVersionLen = response.readBytes(1).readUInt8();
const appVersion = response.readBytes(appVersionLen).toString('ascii');
const flagLen = response.readBytes(1).readUInt8();
const flagsValue = response.readBytes(flagLen).readUInt8();
return {
appName,
appVersion,
flagLen,
flagsValue,
flagRecovery: (flagsValue & 1) !== 0,
flagSignedMcuCode: (flagsValue & 2) !== 0,
flagOnboarded: (flagsValue & 4) !== 0,
flagPINValidated: (flagsValue & 128) !== 0,
};
}
catch (error) {
throw (0, common_1.processErrorResponse)(error);
}
}
/**
* Retrieves device information from the device.
* @returns A promise that resolves to the device information.
* @throws {ResponseError} If the response from the device indicates an error.
*/
async deviceInfo() {
try {
const responseBuffer = await this.transport.send(0xe0, 0x01, 0, 0, Buffer.from([]), [consts_1.LedgerError.NoErrors, 0x6e00]);
const response = (0, common_1.processResponse)(responseBuffer, this.CUSTOM_APP_ERROR_DESCRIPTION);
const targetId = response.readBytes(4).toString('hex');
const secureElementVersionLen = response.readBytes(1).readUInt8();
const seVersion = response.readBytes(secureElementVersionLen).toString();
const flagsLen = response.readBytes(1).readUInt8();
const flag = response.readBytes(flagsLen).toString('hex');
const mcuVersionLen = response.readBytes(1).readUInt8();
let tmp = response.readBytes(mcuVersionLen);
// Patch issue in mcu version
// Find the first zero byte and trim the buffer up to that point
const firstZeroIndex = tmp.indexOf(0);
if (firstZeroIndex !== -1) {
tmp = tmp.subarray(0, firstZeroIndex);
}
const mcuVersion = tmp.toString();
return {
targetId,
seVersion,
flag,
mcuVersion,
};
}
catch (error) {
throw (0, common_1.processErrorResponse)(error);
}
}
}
exports.default = BaseApp;
//# sourceMappingURL=app.js.map