UNPKG

@ledgerhq/live-common

Version:
146 lines • 7.36 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 = fetchImage; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const errors_1 = require("@ledgerhq/errors"); const devices_1 = require("@ledgerhq/devices"); const pako_1 = require("pako"); const deviceAccess_1 = require("./deviceAccess"); const getDeviceInfo_1 = __importDefault(require("./getDeviceInfo")); const customLockScreenFetchHash_1 = __importDefault(require("./customLockScreenFetchHash")); const getAppAndVersion_1 = __importDefault(require("./getAppAndVersion")); const isDashboardName_1 = require("./isDashboardName"); const attemptToQuitApp_1 = __importDefault(require("./attemptToQuitApp")); const errors_2 = require("../errors"); const MAX_APDU_SIZE = 240; function fetchImage({ deviceId, deviceName, request, }) { const { backupHash, allowedEmpty = false, deviceModelId } = request; const sub = (0, deviceAccess_1.withDevice)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(transport => 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)(transport)) .pipe((0, operators_1.mergeMap)(async () => { timeoutSub.unsubscribe(); // Fetch the image hash from the device const imgHash = await (0, customLockScreenFetchHash_1.default)(transport); subscriber.next({ type: "currentImageHash", imgHash }); // We don't have an image to backup if (imgHash === "") { if (allowedEmpty) { subscriber.complete(); return; } else { return subscriber.error(new errors_2.ImageDoesNotExistOnDevice(undefined, { productName: (0, devices_1.getDeviceModel)(deviceModelId).productName, })); } } else if (backupHash === imgHash) { subscriber.next({ type: "imageAlreadyBackedUp" }); subscriber.complete(); return; } // If we are here, either we didn't provide a backupHash or we are // not up to date with the device, in either case, continue to fetch const imageLengthResponse = await transport.send(0xe0, 0x64, 0x00, 0x00); const imageLengthStatus = imageLengthResponse.readUInt16BE(imageLengthResponse.length - 2); if (imageLengthStatus !== errors_1.StatusCodes.OK) { // this answer success even when no image is set, but the length of the image is 0 return subscriber.error(new errors_1.TransportError("Unexpected device response", imageLengthStatus.toString(16))); } const imageLength = imageLengthResponse.readUInt32BE(0); if (imageLength === 0) { // It should never happen since we fetched the hash earlier but hey. return subscriber.error(new errors_2.ImageDoesNotExistOnDevice(undefined, { productName: (0, devices_1.getDeviceModel)(deviceModelId).productName, })); } let imageBuffer = Buffer.from([]); let currentOffset = 0; while (currentOffset < imageLength) { subscriber.next({ type: "progress", progress: (currentOffset + 1) / imageLength, }); // 253 bytes for data, 2 for status const chunkSize = Math.min(MAX_APDU_SIZE - 2, imageLength - currentOffset); const chunkRequest = Buffer.alloc(5); chunkRequest.writeUInt32BE(currentOffset); chunkRequest.writeUInt8(chunkSize, 4); const imageChunk = await transport.send(0xe0, 0x65, 0x00, 0x00, chunkRequest); const chunkStatus = imageChunk.readUInt16BE(imageChunk.length - 2); if (chunkStatus !== errors_1.StatusCodes.OK) { // TODO: map all proper errors return subscriber.error(new errors_1.TransportError("Unexpected device response", chunkStatus.toString(16))); } imageBuffer = Buffer.concat([ imageBuffer, imageChunk.slice(0, imageChunk.length - 2), ]); currentOffset += chunkSize; } const hexImage = await parseCustomLockScreenImageFormat(imageBuffer); subscriber.next({ type: "imageFetched", hexImage }); 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)(transport)).pipe((0, operators_1.concatMap)(appAndVersion => { return !(0, isDashboardName_1.isDashboardName)(appAndVersion.name) ? (0, attemptToQuitApp_1.default)(transport, appAndVersion) : (0, rxjs_1.of)({ type: "appDetected", }); })); } return (0, rxjs_1.throwError)(() => e); })) .subscribe(subscriber); return () => { timeoutSub.unsubscribe(); sub.unsubscribe(); }; })); return sub; } // transforms from a Custom Lock Screen binary image format to an LLM hex string format const parseCustomLockScreenImageFormat = async (staxImageBuffer) => { // const width = staxImageBuffer.readUint16LE(0); // always 400 // const height = staxImageBuffer.readUint16LE(2); // always 672 const bppCompressionByte = staxImageBuffer.readUInt8(4); // const bpp = bppCompressionByte >> 4; // always 2 const compression = bppCompressionByte & 0x0f; const dataLengthBuffer = Buffer.from([ staxImageBuffer.readUInt8(5), staxImageBuffer.readUInt8(6), staxImageBuffer.readUInt8(7), 0x00, ]); const dataLength = dataLengthBuffer.readUInt32LE(); const imageData = staxImageBuffer.slice(8); if (compression === 0) { return imageData.toString("hex"); } let uncompressedImageData = Buffer.from([]); let offset = 0; while (offset < dataLength) { const currentChunkSize = imageData.readUInt16LE(offset); offset += 2; const chunk = imageData.slice(offset, offset + currentChunkSize); const uncompressedChunk = await (0, pako_1.ungzip)(chunk); uncompressedImageData = Buffer.concat([uncompressedImageData, uncompressedChunk]); offset += currentChunkSize; } return uncompressedImageData.toString("hex"); }; //# sourceMappingURL=customLockScreenFetch.js.map