UNPKG

@ledgerhq/live-common

Version:
194 lines • 10.8 kB
"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