UNPKG

@ledgerhq/live-common

Version:
163 lines (147 loc) 5.22 kB
import { Observable, of, throwError } from "rxjs"; import URL from "url"; import Transport, { TransportStatusError } from "@ledgerhq/hw-transport"; import type { FinalFirmware, OsuFirmware } from "@ledgerhq/types-live"; import type { DeviceInfo, SocketEvent } from "@ledgerhq/types-live"; import { version as livecommonversion } from "../../../../package.json"; import { getEnv } from "@ledgerhq/live-env"; import { LocalTracer } from "@ledgerhq/logs"; import { createDeviceSocket } from "../../../socket"; import { catchError, filter, map } from "rxjs/operators"; import { ManagerFirmwareNotEnoughSpaceError, UserRefusedFirmwareUpdate, DeviceOnDashboardExpected, ManagerDeviceLockedError, } from "@ledgerhq/errors"; import { LOG_TYPE, UnresponsiveCmdEvent } from "../core"; export type InstallFirmwareCommandRequest = { targetId: DeviceInfo["targetId"]; firmware: OsuFirmware | FinalFirmware; }; type FilteredSocketEvent = | { type: "bulk-progress"; progress: number; index: number; total: number; } | { type: "device-permission-requested"; }; export type InstallFirmwareCommandEvent = | { type: "progress"; progress: number; } | { type: "allowSecureChannelRequested"; } | { type: "firmwareInstallPermissionRequested"; } | { type: "firmwareInstallPermissionGranted"; } | UnresponsiveCmdEvent; /** * Creates a scriptrunner connection with the /install API endpoint of the HSM in order to install (upload) * an OSU (operating system updater). * This is the same endpoint that is used to install applications, however the parameters that are * passed are different. Besides that, the emitted events are semantically different. This is why * this is a dedicated command to OSU installations. * * @param transport The transport object to contact the device * @param param1 The firmware details to be installed * @returns An observable that emits the events according to the progression of the firmware installation */ export function installFirmwareCommand( transport: Transport, { targetId, firmware }: InstallFirmwareCommandRequest, ): Observable<InstallFirmwareCommandEvent> { const tracer = new LocalTracer(LOG_TYPE, { function: "installFirmwareCommand" }); tracer.trace("Starting", { targetId, osuFirmware: firmware, }); return createDeviceSocket(transport, { url: URL.format({ pathname: `${getEnv("BASE_SOCKET_URL")}/install`, query: { targetId, livecommonversion, perso: firmware.perso, firmware: firmware.firmware, firmwareKey: firmware.firmware_key, }, }), unresponsiveExpectedDuringBulk: true, context: tracer.getContext(), }).pipe( catchError(error => { tracer.trace("Socket firmware error", { error }); return remapSocketFirmwareError(error); }), filter<SocketEvent, FilteredSocketEvent>((e): e is FilteredSocketEvent => { return e.type === "bulk-progress" || e.type === "device-permission-requested"; }), map<FilteredSocketEvent, InstallFirmwareCommandEvent>(e => { if (e.type === "bulk-progress") { return e.index === e.total - 1 ? { // the penultimate APDU of the bulk part of the installation is a blocking apdu and // requires user validation type: "firmwareInstallPermissionRequested", } : e.index === e.total ? { // the last APDU of the bulk part of the instalation means that the user validated // the installation of the OSU firmware type: "firmwareInstallPermissionGranted", } : { type: "progress", progress: e.progress, }; } // then type is "device-permission-requested" return { type: "allowSecureChannelRequested" }; }), catchError(error => { tracer.trace("Socket unresponsive error", { error }); return remapSocketUnresponsiveError(error); }), ); } const remapSocketUnresponsiveError: ( e: Error, ) => Observable<InstallFirmwareCommandEvent> | Observable<never> = (e: Error) => { if (e instanceof ManagerDeviceLockedError) { return of({ type: "unresponsive" }); } return throwError(e); }; const remapSocketFirmwareError: (e: Error) => Observable<never> = (e: Error) => { if (!e || !e.message) return throwError(() => e); if (e.message.startsWith("invalid literal")) { // hack to detect the case you're not in good condition (not in dashboard) return throwError(() => new DeviceOnDashboardExpected()); } const status = e instanceof TransportStatusError ? e.statusCode.toString(16) : (e as Error).message.slice((e as Error).message.length - 4); switch (status) { case "6a84": case "5103": return throwError(() => new ManagerFirmwareNotEnoughSpaceError()); case "6a85": case "5102": return throwError(() => new UserRefusedFirmwareUpdate()); case "6985": case "5501": return throwError(() => new UserRefusedFirmwareUpdate()); default: return throwError(() => e); } };