@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
287 lines • 14.7 kB
JavaScript
;
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