@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
163 lines (147 loc) • 5.22 kB
text/typescript
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);
}
};