UNPKG

@ledgerhq/live-common

Version:
287 lines • 14.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.listApps = void 0; const devices_1 = require("@ledgerhq/devices"); const errors_1 = require("@ledgerhq/errors"); const rxjs_1 = require("rxjs"); const types_live_1 = require("@ledgerhq/types-live"); const logs_1 = require("@ledgerhq/logs"); const listApps_1 = __importDefault(require("../hw/listApps")); const customLockScreenFetchSize_1 = __importDefault(require("../hw/customLockScreenFetchSize")); const currencies_1 = require("../currencies"); const api_1 = __importDefault(require("../manager/api")); const getDeviceNameUseCase_1 = require("../device/use-cases/getDeviceNameUseCase"); const getLatestFirmwareForDeviceUseCase_1 = require("../device/use-cases/getLatestFirmwareForDeviceUseCase"); const getProviderIdUseCase_1 = require("../device/use-cases/getProviderIdUseCase"); const polyfill_1 = require("./polyfill"); const isCustomLockScreenSupported_1 = require("../device/use-cases/isCustomLockScreenSupported"); const ids_1 = require("@ledgerhq/client-ids/ids"); // Hash discrepancies for these apps do NOT indicate a potential update, // these apps have a mechanism that makes their hash change every time. const appsWithDynamicHashes = ["Fido U2F", "Security Key"]; // Empty hash data means we won't have information on the app. const emptyHashData = "0".repeat(64); const listApps = ({ transport, deviceInfo, deviceProxyModel, managerDevModeEnabled, forceProvider, managerApiRepository, }) => { const tracer = new logs_1.LocalTracer("list-apps", { transport: transport.getTraceContext() }); tracer.trace("Using new version", { deviceInfo }); if (deviceInfo.isOSU || deviceInfo.isBootloader) { return (0, rxjs_1.throwError)(() => new errors_1.UnexpectedBootloader("")); } const deviceModelId = transport?.deviceModel?.id || (0, devices_1.identifyTargetId)(deviceInfo.targetId)?.id || deviceProxyModel; return new rxjs_1.Observable(o => { let sub; async function main() { const provider = (0, getProviderIdUseCase_1.getProviderIdUseCase)({ deviceInfo, forceProvider }); if (!deviceModelId) throw new Error("Bad usage of listAppsV2: missing deviceModelId"); const deviceModel = (0, devices_1.getDeviceModel)(deviceModelId); const bytesPerBlock = deviceModel.getBlockSize(deviceInfo.version); /** The following are several asynchronous sequences running in parallel */ /** * Sequence 1: obtain the full data regarding apps installed on the device * -> list raw data of apps installed on device * -> then filter apps (eliminate language packs and such) * -> then fetch matching app metadata using apps' hashes */ let listAppsResponsePromise; function listAppsWithSingleCommand() { return (0, listApps_1.default)(transport); } function listAppsWithManagerApi() { return new Promise((resolve, reject) => { // TODO: migrate this ManagerAPI call to ManagerApiRepository sub = api_1.default.listInstalledApps(transport, { targetId: deviceInfo.targetId, perso: "perso_11", }).subscribe({ next: e => { switch (e.type) { case "result": resolve(e.payload); break; case "device-permission-granted": case "device-permission-requested": o.next(e); break; case "device-id": { // Normalize deviceId to DeviceId object (SocketEvent may have string or DeviceId) const deviceIdValue = typeof e.deviceId === "string" ? ids_1.DeviceId.fromString(e.deviceId) : e.deviceId; o.next({ type: "device-id", deviceId: deviceIdValue, }); break; } } }, error: reject, }); }); } if (deviceInfo.managerAllowed) { // If the user has already allowed a secure channel during this session we can directly // ask the device for the installed applications instead of going through a scriptrunner, // this is a performance optimization, part of a larger rework with Manager API v2. tracer.trace("Using direct apdu listapps"); listAppsResponsePromise = listAppsWithSingleCommand().catch(e => { // For some old versions of the firmware, the listapps command is not supported. // In this case, we fallback to the scriptrunner listapps. if (e instanceof errors_1.TransportStatusError && [ errors_1.StatusCodes.CLA_NOT_SUPPORTED, errors_1.StatusCodes.INS_NOT_SUPPORTED, errors_1.StatusCodes.UNKNOWN_APDU, 0x6e01, // No StatusCodes definition 0x6d01, // No StatusCodes definition ].includes(e.statusCode)) { tracer.trace("Fallback to scriptrunner listapps"); return listAppsWithManagerApi(); } throw e; }); } else { // Fallback to original web-socket list apps tracer.trace("Using scriptrunner listapps"); listAppsResponsePromise = listAppsWithManagerApi(); } const filteredListAppsPromise = listAppsResponsePromise.then(result => { // Empty HashData can come from apps that are not real apps (such as language packs) // or custom applications that have been sideloaded. return result .filter(({ hash_code_data }) => hash_code_data !== emptyHashData) .map(({ hash, name }) => ({ hash, name })); }); const listAppsAndMatchesPromise = filteredListAppsPromise.then(result => { const hashes = result.map(({ hash }) => hash); const matches = result.length ? managerApiRepository .getAppsByHash(hashes) .then(matches => matches.map(appV2 => (appV2 ? (0, polyfill_1.mapApplicationV2ToApp)(appV2) : null))) : []; return Promise.all([result, matches]); }); /** * Sequence 2: get information about current and latest firmware available * for the device */ const deviceVersionPromise = managerApiRepository.getDeviceVersion({ targetId: deviceInfo.targetId, providerId: provider, }); const currentFirmwarePromise = deviceVersionPromise.then(deviceVersion => managerApiRepository.getCurrentFirmware({ deviceId: deviceVersion.id, version: deviceInfo.version, providerId: provider, })); const latestFirmwarePromise = currentFirmwarePromise.then(currentFirmware => (0, getLatestFirmwareForDeviceUseCase_1.getLatestFirmwareForDeviceUseCase)(deviceInfo, managerApiRepository).then(updateAvailable => ({ ...currentFirmware, updateAvailable, }))); /** * Sequence 3: get catalog of apps available for the device */ const catalogForDevicesPromise = managerApiRepository .catalogForDevice({ provider, targetId: deviceInfo.targetId, firmwareVersion: deviceInfo.version, }) .then(apps => apps.map(polyfill_1.mapApplicationV2ToApp)); /** * Sequence 4: list all currencies, sorted by market cp */ const sortedCryptoCurrenciesPromise = (0, currencies_1.currenciesByMarketcap)((0, currencies_1.listCryptoCurrencies)(managerDevModeEnabled, true)); /** * Sequence 5: get language pack available for the device */ const languagePackForDevicePromise = managerApiRepository.getLanguagePackagesForDevice(deviceInfo, forceProvider); /* Running all sequences 1 2 3 4 5 defined above in parallel */ const [[listApps, matches], catalogForDevice, firmware, sortedCryptoCurrencies, languages] = await Promise.all([ listAppsAndMatchesPromise, catalogForDevicesPromise, latestFirmwarePromise, sortedCryptoCurrenciesPromise, languagePackForDevicePromise, ]); (0, polyfill_1.calculateDependencies)(); /** * Associate a market cap sorting index to each app of the catalog of * available apps. */ catalogForDevice.forEach(app => { const crypto = app.currencyId && (0, currencies_1.findCryptoCurrencyById)(app.currencyId); if (crypto) { app.indexOfMarketCap = sortedCryptoCurrencies.indexOf(crypto); } }); /** * Aggregate the data obtained above to build the list of installed apps * with their full metadata. */ const installedList = []; listApps.forEach((item, index) => { if (item == null) return; const { name: localName, hash: localHash } = item; const matchFromHash = matches[index]; if (matchFromHash && matchFromHash.hash === localHash) { installedList.push(matchFromHash); return; } // If the hash is not static (ex: Fido app) we need to find the app by its name using the catalog const matchFromCatalog = catalogForDevice.find(({ name }) => name === localName); tracer.trace(`Falling back to catalog for ${localName}`, { localName, matchFromCatalog: Boolean(matchFromCatalog), }); if (matchFromCatalog) { installedList.push(matchFromCatalog); } }); tracer.trace("Installed and in catalog apps", { installedApps: installedList.length, inCatalogApps: catalogForDevice.length, }); // Abused somewhere else const appByName = catalogForDevice.reduce((result, app) => { result[app.name] = app; return result; }, {}); const installedAppNames = {}; // Polyfill more data on the app installed const installed = installedList.map(({ name, hash, bytes, version }) => { installedAppNames[name] = true; const appInCatalog = appByName[name]; const updateAvailable = appInCatalog?.hash !== hash; const ignoreUpdate = appsWithDynamicHashes.includes(name); const updated = ignoreUpdate || !updateAvailable; const availableVersion = appInCatalog?.version || ""; const blocks = Math.ceil((bytes || appInCatalog.bytes || 0) / bytesPerBlock); return { name, updated, blocks, hash, version, availableVersion, }; }); // Used to hide apps that are dev tools if user didn't opt-in. const appsListNames = catalogForDevice .filter(({ isDevTools, name }) => managerDevModeEnabled || !isDevTools || name in installedAppNames) .map(({ name }) => name); /** * Obtain remaining metadata: * - Ledger Stax/Europa custom picture: number of blocks taken in storage * - Installed language pack * - Device name * */ // Stax/Europa specific, account for the size of the CLS for the storage bar. let customImageBlocks = 0; if ((0, isCustomLockScreenSupported_1.isCustomLockScreenSupported)(deviceModelId) && !deviceInfo.isRecoveryMode) { const customImageSize = await (0, customLockScreenFetchSize_1.default)(transport); if (customImageSize) { customImageBlocks = Math.ceil(customImageSize / bytesPerBlock); } } const languageId = deviceInfo.languageId || types_live_1.languageIds.english; const installedLanguagePack = languages.find(lang => lang.language === types_live_1.idsToLanguage[languageId]); // Will not prompt user since we've allowed secure channel already. const deviceName = await (0, getDeviceNameUseCase_1.getDeviceName)(transport); const result = { appByName, appsListNames, deviceInfo, deviceModelId, installed, installedAvailable: !!installedList, installedLanguagePack, // Not strictly listApps content. firmware, customImageBlocks, deviceName, }; o.next({ type: "result", result, }); } main().then(function onfulfilled() { o.complete(); }, function onrejected(e) { o.error(e); }); return () => { if (sub) sub.unsubscribe(); }; }); }; exports.listApps = listApps; //# sourceMappingURL=listApps.js.map