@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
194 lines • 10.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = loadImage;
exports.generateCustomLockScreenImageFormat = generateCustomLockScreenImageFormat;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const errors_1 = require("@ledgerhq/errors");
const devices_1 = require("@ledgerhq/devices");
const getDeviceInfo_1 = __importDefault(require("./getDeviceInfo"));
const errors_2 = require("../errors");
const getAppAndVersion_1 = __importDefault(require("./getAppAndVersion"));
const isDashboardName_1 = require("./isDashboardName");
const attemptToQuitApp_1 = __importDefault(require("./attemptToQuitApp"));
const customLockScreenFetchSize_1 = __importDefault(require("./customLockScreenFetchSize"));
const customLockScreenFetchHash_1 = __importDefault(require("./customLockScreenFetchHash"));
const pako_1 = require("pako");
const screenSpecs_1 = require("../device/use-cases/screenSpecs");
const device_management_kit_1 = require("@ledgerhq/device-management-kit");
const core_1 = require("../deviceSDK/transports/core");
const MAX_APDU_SIZE = 255;
const COMPRESS_CHUNK_SIZE = 2048;
/**
* Type guard to check if the given error is a DeviceDisconnectedWhileSendingError.
* Ensures that the error is an object, is not null, and matches the expected structure.
* This is used to identify specific disconnection errors from the DMK device.
*/
const isDmkDeviceDisconnectedError = (err) => typeof err === "object" &&
err !== null &&
(err instanceof device_management_kit_1.DeviceDisconnectedWhileSendingError ||
("_tag" in err && err._tag === "DeviceDisconnectedWhileSendingError"));
function loadImage({ deviceId, deviceName, request, }) {
const { hexImage, padImage = true, deviceModelId } = request;
const screenSpecs = (0, screenSpecs_1.getScreenSpecs)(deviceModelId);
const sub = (0, core_1.withTransport)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(({ transportRef }) => new rxjs_1.Observable(subscriber => {
const timeoutSub = (0, rxjs_1.of)({ type: "unresponsiveDevice" })
.pipe((0, operators_1.delay)(1000))
.subscribe(e => subscriber.next(e));
const sub = (0, rxjs_1.from)((0, getDeviceInfo_1.default)(transportRef.current))
.pipe((0, operators_1.mergeMap)(async () => {
timeoutSub.unsubscribe();
const imageData = await generateCustomLockScreenImageFormat(hexImage, true, !!padImage, screenSpecs);
const imageLength = imageData.length;
const imageSize = Buffer.alloc(4);
imageSize.writeUIntBE(imageLength, 0, 4);
subscriber.next({ type: "loadImagePermissionRequested" });
const createImageResponse = await transportRef.current.send(0xe0, 0x60, 0x00, 0x00, imageSize, [errors_1.StatusCodes.NOT_ENOUGH_SPACE, errors_1.StatusCodes.USER_REFUSED_ON_DEVICE, errors_1.StatusCodes.OK]);
const createImageStatus = createImageResponse.readUInt16BE(createImageResponse.length - 2);
const createImageStatusStr = createImageStatus.toString(16);
// reads last 2 bytes which correspond to the status
if (createImageStatus === errors_1.StatusCodes.USER_REFUSED_ON_DEVICE) {
return subscriber.error(new errors_2.ImageLoadRefusedOnDevice(createImageStatusStr, {
productName: (0, devices_1.getDeviceModel)(deviceModelId).productName,
}));
}
else if (createImageStatus === errors_1.StatusCodes.NOT_ENOUGH_SPACE) {
return subscriber.error(new errors_1.ManagerNotEnoughSpaceError());
}
else if (createImageStatus !== errors_1.StatusCodes.OK) {
return subscriber.error(new errors_1.TransportError("Unexpected device response", createImageStatusStr));
}
let currentOffset = 0;
// offset in number of charaters
while (currentOffset < imageLength) {
subscriber.next({
type: "progress",
progress: (currentOffset + 1) / imageLength,
});
// chunkSize in number of bytes
const chunkSize = Math.min(MAX_APDU_SIZE - 4, imageLength - currentOffset);
// we subtract 4 because the first 4 bytes of the data part of the apdu are used for
// passing the offset of the chunk
const chunkDataBuffer = imageData.slice(currentOffset, currentOffset + chunkSize);
const chunkOffsetBuffer = Buffer.alloc(4);
chunkOffsetBuffer.writeUIntBE(currentOffset, 0, 4);
const apduData = Buffer.concat([chunkOffsetBuffer, chunkDataBuffer]);
await transportRef.current.send(0xe0, 0x61, 0x00, 0x00, apduData);
currentOffset += chunkSize;
}
subscriber.next({ type: "commitImagePermissionRequested" });
const commitResponse = await transportRef.current.send(0xe0, 0x62, 0x00, 0x00, Buffer.from([]), [0x9000, 0x5501]);
const commitStatus = commitResponse.readUInt16BE(commitResponse.length - 2);
const commitStatusStr = commitStatus.toString(16);
// reads last 2 bytes which correspond to the status
if (commitStatus === 0x5501) {
return subscriber.error(new errors_2.ImageCommitRefusedOnDevice(commitStatusStr, {
productName: (0, devices_1.getDeviceModel)(deviceModelId).productName,
}));
}
else if (commitStatus !== 0x9000) {
return subscriber.error(new errors_1.TransportError("Unexpected device response", commitStatusStr));
}
// Fetch image size
const imageBytes = await (0, customLockScreenFetchSize_1.default)(transportRef.current);
// Fetch image hash
const imageHash = await (0, customLockScreenFetchHash_1.default)(transportRef.current);
subscriber.next({
type: "imageLoaded",
imageSize: imageBytes,
imageHash,
});
subscriber.complete();
}), (0, operators_1.catchError)((e) => {
if (e instanceof errors_1.DeviceOnDashboardExpected ||
(e &&
e instanceof errors_1.TransportStatusError &&
[0x6e00, 0x6d00, 0x6e01, 0x6d01, 0x6d02].includes(e.statusCode))) {
return (0, rxjs_1.from)((0, getAppAndVersion_1.default)(transportRef.current)).pipe((0, operators_1.concatMap)(appAndVersion => {
return !(0, isDashboardName_1.isDashboardName)(appAndVersion.name)
? (0, attemptToQuitApp_1.default)(transportRef.current, appAndVersion)
: (0, rxjs_1.of)({
type: "appDetected",
});
}));
}
return (0, rxjs_1.throwError)(() => e);
}))
.subscribe(subscriber);
return () => {
timeoutSub.unsubscribe();
sub.unsubscribe();
};
})).pipe((0, operators_1.catchError)(err => {
if (err.name === "TimeoutError" || isDmkDeviceDisconnectedError(err)) {
return (0, rxjs_1.throwError)(() => new errors_1.DisconnectedDevice());
}
return (0, rxjs_1.throwError)(() => err);
}));
return sub;
}
function padHexImage(hexImage, screenSpecs) {
// hexImage is a string that is a hex representation of the image data
// each character is a pixel (between 0 and 15) and it starts from the top right
// corner, goes down the column and then to the next column, until the bottom left
// We need to pad the image on the edges to match the screen specs.
const sourceWidth = screenSpecs.width - screenSpecs.paddingLeft - screenSpecs.paddingRight;
const sourceHeight = screenSpecs.height - screenSpecs.paddingTop - screenSpecs.paddingBottom;
const destHeight = screenSpecs.height;
let result = "";
// add right padding
result += "0".repeat(screenSpecs.paddingRight * destHeight);
// add the image data
for (let columnIndex = 0; columnIndex < sourceWidth; columnIndex++) {
const column = hexImage.slice(columnIndex * sourceHeight, (columnIndex + 1) * sourceHeight);
const topPadding = "0".repeat(screenSpecs.paddingTop);
const paddingBottom = "0".repeat(screenSpecs.paddingBottom);
result += topPadding + column + paddingBottom;
}
// add left padding
result += "0".repeat(screenSpecs.paddingLeft * destHeight);
return result;
}
const bitsPerPixelToBppIndicator = {
1: 0,
4: 2,
};
async function generateCustomLockScreenImageFormat(hexImage, compressImage, padImage, screenSpecs) {
const width = screenSpecs.width;
const height = screenSpecs.height;
const bpp = bitsPerPixelToBppIndicator[screenSpecs.bitsPerPixel];
const compression = compressImage ? 1 : 0;
const header = Buffer.alloc(8);
header.writeUInt16LE(width, 0); // width
header.writeUInt16LE(height, 2); // height
header.writeUInt8((bpp << 4) | compression, 4);
const paddedHexImage = padImage ? padHexImage(hexImage, screenSpecs) : hexImage;
const imgData = Buffer.from(paddedHexImage, "hex");
if (!compressImage) {
const dataLength = imgData.length;
header.writeUInt8(dataLength & 0xff, 5); // lowest byte
header.writeUInt8((dataLength >> 8) & 0xff, 6); // middle byte
header.writeUInt8((dataLength >> 16) & 0xff, 7); // biggest byte
return Buffer.concat([header, imgData]);
}
const chunkedImgData = [];
for (let i = 0; i < imgData.length; i += COMPRESS_CHUNK_SIZE) {
chunkedImgData.push(imgData.slice(i, i + COMPRESS_CHUNK_SIZE));
}
const compressedChunkedImgData = await Promise.all(chunkedImgData.map(async (chunk) => {
const compressedChunk = await (0, pako_1.gzip)(chunk);
const compressedChunkSize = Buffer.alloc(2);
compressedChunkSize.writeUInt16LE(compressedChunk.length);
return Buffer.concat([compressedChunkSize, compressedChunk]);
}));
const compressedData = Buffer.concat(compressedChunkedImgData);
const dataLength = compressedData.length;
header.writeUInt8(dataLength & 0xff, 5); // lowest byte
header.writeUInt8((dataLength >> 8) & 0xff, 6); // middle byte
header.writeUInt8((dataLength >> 16) & 0xff, 7); // biggest byte
return Buffer.concat([header, Buffer.concat(compressedChunkedImgData)]);
}
//# sourceMappingURL=customLockScreenLoad.js.map