@onekeyfe/hd-core 1.2.0-alpha.147 → 1.2.0-alpha.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/device-lifecycle-events.test.ts +128 -2
- package/__tests__/device-utils.test.ts +8 -0
- package/__tests__/deviceUploadNft.test.ts +3 -0
- package/__tests__/protocol-v2-resources.test.ts +35 -89
- package/__tests__/protocol-v2-unlock-policy.test.ts +72 -0
- package/__tests__/protocol-v2.test.ts +51 -188
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +4 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +14 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +202 -314
- package/dist/protocols/protocol-v2/resources.d.ts +11 -7
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/settings.d.ts +0 -13
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +61 -238
- package/src/api/PromptWebDeviceAccess.ts +2 -2
- package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
- package/src/api/device/DeviceRebootToBootloader.ts +3 -1
- package/src/api/protocol-v2/DeviceReboot.ts +5 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +5 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +5 -1
- package/src/core/index.ts +28 -106
- package/src/device/Device.ts +59 -8
- package/src/index.ts +0 -4
- package/src/protocols/protocol-v2/resources.ts +92 -102
- package/src/types/settings.ts +0 -15
- package/src/utils/deviceInfoUtils.ts +7 -2
package/src/core/index.ts
CHANGED
|
@@ -415,10 +415,6 @@ const onCallDevice = async (
|
|
|
415
415
|
if (method.payload?.onlyConnectBleDevice) {
|
|
416
416
|
preWarmCallbackTask?.resolve();
|
|
417
417
|
Log.debug('Call API - only connect ble device: ', device?.mainId);
|
|
418
|
-
// This early return bypasses the normal-path releaseTask at the end of the
|
|
419
|
-
// call; without it the task leaks and haunts every later queue snapshot
|
|
420
|
-
// and cancel sweep (field log: a completed task lingered for 6 minutes).
|
|
421
|
-
requestQueue.releaseTask(method.responseID);
|
|
422
418
|
return createResponseMessage(method.responseID, true, null);
|
|
423
419
|
}
|
|
424
420
|
|
|
@@ -918,16 +914,32 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
|
|
|
918
914
|
return true;
|
|
919
915
|
}
|
|
920
916
|
|
|
921
|
-
function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
|
|
922
|
-
const
|
|
917
|
+
export function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
|
|
918
|
+
const typedError = error as { errorCode?: unknown; message?: unknown };
|
|
919
|
+
const message =
|
|
920
|
+
typeof typedError?.message === 'string' ? typedError.message : String(error ?? '');
|
|
923
921
|
return (
|
|
924
922
|
method.payload.connectProtocol === 'V2' &&
|
|
923
|
+
typedError?.errorCode === HardwareErrorCode.RuntimeError &&
|
|
925
924
|
message.includes('Device protocol mismatch') &&
|
|
926
925
|
message.includes('expected V2') &&
|
|
927
926
|
message.includes('did not respond to expected protocol')
|
|
928
927
|
);
|
|
929
928
|
}
|
|
930
929
|
|
|
930
|
+
export function isRetryableBleConnectionError(method: BaseMethod, error: unknown) {
|
|
931
|
+
if (method.device?.wasInterruptedByUser()) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
const typedError = error as { errorCode?: unknown };
|
|
935
|
+
return (
|
|
936
|
+
typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
|
|
937
|
+
typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
|
|
938
|
+
isRetryableBleProtocolV2ProbeError(method, error) ||
|
|
939
|
+
isMissingDetectedProtocolV2Error(method, error)
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
931
943
|
export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unknown) {
|
|
932
944
|
const typedError = error as { errorCode?: unknown; message?: unknown };
|
|
933
945
|
return (
|
|
@@ -942,61 +954,11 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
|
|
|
942
954
|
* If the Bluetooth connection times out, retry up to 6 times
|
|
943
955
|
* @param retryCount - Current retry count (default 0)
|
|
944
956
|
*/
|
|
945
|
-
|
|
946
|
-
// transport that never settles (field case: Electron main lost an IPC reply,
|
|
947
|
-
// "reply was never sent" after 5 minutes) hangs the call forever and cancel()
|
|
948
|
-
// only takes effect at poll checkpoints. Race acquire against a deadline and
|
|
949
|
-
// the caller's abort signal so the hang is bounded and cancel is immediate.
|
|
950
|
-
const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
|
|
951
|
-
|
|
952
|
-
function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
|
|
953
|
-
return new Promise<T>((resolve, reject) => {
|
|
954
|
-
let settled = false;
|
|
955
|
-
const settle = (fn: () => void) => {
|
|
956
|
-
if (settled) return;
|
|
957
|
-
settled = true;
|
|
958
|
-
clearTimeout(deadline);
|
|
959
|
-
abortSignal?.removeEventListener('abort', onAbort);
|
|
960
|
-
fn();
|
|
961
|
-
};
|
|
962
|
-
const onAbort = () =>
|
|
963
|
-
settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
|
|
964
|
-
const deadline = setTimeout(
|
|
965
|
-
() =>
|
|
966
|
-
settle(() =>
|
|
967
|
-
reject(
|
|
968
|
-
ERRORS.TypedError(
|
|
969
|
-
HardwareErrorCode.BleTimeoutError,
|
|
970
|
-
`BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
|
|
971
|
-
)
|
|
972
|
-
)
|
|
973
|
-
),
|
|
974
|
-
BLE_ACQUIRE_DEADLINE_MS
|
|
975
|
-
);
|
|
976
|
-
// Attach before any early return so a late settlement of acquirePromise
|
|
977
|
-
// is always consumed — an abort or deadline must never leave the acquire
|
|
978
|
-
// rejection unhandled.
|
|
979
|
-
acquirePromise.then(
|
|
980
|
-
value => settle(() => resolve(value)),
|
|
981
|
-
error => settle(() => reject(error))
|
|
982
|
-
);
|
|
983
|
-
if (abortSignal) {
|
|
984
|
-
if (abortSignal.aborted) {
|
|
985
|
-
onAbort();
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
abortSignal.addEventListener('abort', onAbort);
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
async function connectDeviceForBle(
|
|
994
|
-
method: BaseMethod,
|
|
995
|
-
device: Device,
|
|
996
|
-
abortSignal?: AbortSignal,
|
|
997
|
-
retryCount = 0
|
|
998
|
-
) {
|
|
957
|
+
async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
|
|
999
958
|
try {
|
|
959
|
+
if (device.wasInterruptedByUser()) {
|
|
960
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
961
|
+
}
|
|
1000
962
|
if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
|
|
1001
963
|
await device.release();
|
|
1002
964
|
}
|
|
@@ -1006,43 +968,9 @@ async function connectDeviceForBle(
|
|
|
1006
968
|
!device.commands ||
|
|
1007
969
|
device.commands.disposed;
|
|
1008
970
|
if (shouldAcquire) {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
// legitimately block on a user-driven system bonding prompt for longer
|
|
1013
|
-
// than any sane deadline. Other envs keep the plain acquire unchanged.
|
|
1014
|
-
const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
|
|
1015
|
-
// A cancel landing during the retry backoff must not start a new acquire.
|
|
1016
|
-
if (useAcquireGuards && abortSignal?.aborted) {
|
|
1017
|
-
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
1018
|
-
}
|
|
1019
|
-
if (!useAcquireGuards) {
|
|
1020
|
-
await device.acquire(method.payload.connectProtocol, {
|
|
1021
|
-
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1022
|
-
});
|
|
1023
|
-
} else {
|
|
1024
|
-
try {
|
|
1025
|
-
await raceBleAcquire(
|
|
1026
|
-
device.acquire(method.payload.connectProtocol, {
|
|
1027
|
-
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1028
|
-
}),
|
|
1029
|
-
abortSignal
|
|
1030
|
-
);
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
// A deadline hit means the transport is wedged mid-acquire; drop the
|
|
1033
|
-
// link before the retry so it cold-connects instead of stacking a
|
|
1034
|
-
// second connect onto the half-open one.
|
|
1035
|
-
if (
|
|
1036
|
-
err.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
1037
|
-
device.mainId &&
|
|
1038
|
-
device.deviceConnector
|
|
1039
|
-
) {
|
|
1040
|
-
await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
|
|
1041
|
-
device.markTransportDisconnected();
|
|
1042
|
-
}
|
|
1043
|
-
throw err;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
971
|
+
await device.acquire(method.payload.connectProtocol, {
|
|
972
|
+
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
973
|
+
});
|
|
1046
974
|
}
|
|
1047
975
|
if (method.payload?.onlyConnectBleDevice) {
|
|
1048
976
|
if (shouldAcquire) {
|
|
@@ -1078,17 +1006,11 @@ async function connectDeviceForBle(
|
|
|
1078
1006
|
// next attempt skip acquire and initialize onto the link we just cut.
|
|
1079
1007
|
device.markTransportDisconnected();
|
|
1080
1008
|
}
|
|
1081
|
-
if (
|
|
1082
|
-
(err.errorCode === HardwareErrorCode.BleTimeoutError ||
|
|
1083
|
-
err.errorCode === HardwareErrorCode.BleConnectedError ||
|
|
1084
|
-
isRetryableBleProtocolV2ProbeError(method, err) ||
|
|
1085
|
-
requiresColdReconnect) &&
|
|
1086
|
-
retryCount < 6
|
|
1087
|
-
) {
|
|
1009
|
+
if (isRetryableBleConnectionError(method, err) && retryCount < 6) {
|
|
1088
1010
|
const nextRetry = retryCount + 1;
|
|
1089
1011
|
Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
|
|
1090
1012
|
await wait(3000);
|
|
1091
|
-
await connectDeviceForBle(method, device,
|
|
1013
|
+
await connectDeviceForBle(method, device, nextRetry);
|
|
1092
1014
|
} else {
|
|
1093
1015
|
throw err;
|
|
1094
1016
|
}
|
|
@@ -1193,7 +1115,7 @@ const ensureConnected = async (
|
|
|
1193
1115
|
if (abort()) {
|
|
1194
1116
|
return;
|
|
1195
1117
|
}
|
|
1196
|
-
await connectDeviceForBle(method, device
|
|
1118
|
+
await connectDeviceForBle(method, device);
|
|
1197
1119
|
}
|
|
1198
1120
|
resolve(device);
|
|
1199
1121
|
return;
|
package/src/device/Device.ts
CHANGED
|
@@ -235,6 +235,12 @@ export class Device extends EventEmitter {
|
|
|
235
235
|
*/
|
|
236
236
|
private deviceAcquired = false;
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Set by interruptionFromUser() so an in-flight acquire/initialize cannot
|
|
240
|
+
* finish the link and send Cancel after the caller already aborted.
|
|
241
|
+
*/
|
|
242
|
+
private interruptedByUser = false;
|
|
243
|
+
|
|
238
244
|
/** Canonical device-state cache; legacy Features is a compatibility projection. */
|
|
239
245
|
private stateStore = new DeviceStateStore();
|
|
240
246
|
|
|
@@ -429,6 +435,7 @@ export class Device extends EventEmitter {
|
|
|
429
435
|
expectedProtocol?: HardwareConnectProtocol,
|
|
430
436
|
options?: { throwOnRunPromiseError?: boolean; forceProtocolDetection?: boolean }
|
|
431
437
|
) {
|
|
438
|
+
this.throwIfInterruptedByUser();
|
|
432
439
|
const env = DataManager.getSettings('env');
|
|
433
440
|
const mainIdKey = DataManager.isBleConnect(env) ? 'id' : 'session';
|
|
434
441
|
const previousProtocol = this.originalDescriptor.protocolType;
|
|
@@ -483,6 +490,15 @@ export class Device extends EventEmitter {
|
|
|
483
490
|
if (detectedProtocol) {
|
|
484
491
|
this.originalDescriptor.protocolType = detectedProtocol;
|
|
485
492
|
}
|
|
493
|
+
if (this.interruptedByUser) {
|
|
494
|
+
const session = this.mainId;
|
|
495
|
+
if (session && this.deviceConnector?.disconnect) {
|
|
496
|
+
await this.deviceConnector.disconnect(session).catch(disconnectError => {
|
|
497
|
+
Log.debug('Ignored disconnect after user cancel during acquire', disconnectError);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
501
|
+
}
|
|
486
502
|
this.deviceAcquired = true;
|
|
487
503
|
this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
|
|
488
504
|
|
|
@@ -903,6 +919,7 @@ export class Device extends EventEmitter {
|
|
|
903
919
|
}
|
|
904
920
|
|
|
905
921
|
async initialize(options?: InitOptions) {
|
|
922
|
+
this.throwIfInterruptedByUser();
|
|
906
923
|
// Protocol V2 does not support legacy Initialize; use its dedicated flow.
|
|
907
924
|
if (this.isProtocolV2()) {
|
|
908
925
|
this.passphraseState = options?.passphraseState;
|
|
@@ -1445,6 +1462,7 @@ export class Device extends EventEmitter {
|
|
|
1445
1462
|
Log.debug('[Device] run error:', 'Device is running, but will cancel previous operate');
|
|
1446
1463
|
}
|
|
1447
1464
|
|
|
1465
|
+
this.interruptedByUser = false;
|
|
1448
1466
|
options = parseRunOptions(options);
|
|
1449
1467
|
|
|
1450
1468
|
const runPromise = createDeferred<void>();
|
|
@@ -1574,21 +1592,24 @@ export class Device extends EventEmitter {
|
|
|
1574
1592
|
|
|
1575
1593
|
async interruptionFromUser() {
|
|
1576
1594
|
const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1595
|
+
this.interruptedByUser = true;
|
|
1577
1596
|
const cleanupPromise = this.runCleanupPromise;
|
|
1578
1597
|
const { cancelableAction } = this;
|
|
1579
|
-
const env = DataManager.getSettings('env');
|
|
1580
1598
|
if (cancelableAction) {
|
|
1581
1599
|
await cancelableAction(error);
|
|
1582
|
-
} else if (
|
|
1583
|
-
this.isProtocolV2() &&
|
|
1584
|
-
(DataManager.isBleConnect(env) ||
|
|
1585
|
-
DataManager.isBrowserWebUsb(env) ||
|
|
1586
|
-
DataManager.isDesktopWebUsb(env)) &&
|
|
1587
|
-
this.hasDeviceAcquire()
|
|
1588
|
-
) {
|
|
1600
|
+
} else if (this.shouldSendFallbackProtocolCancel()) {
|
|
1589
1601
|
await this.commands?.cancelDevice?.().catch(cancelError => {
|
|
1590
1602
|
Log.debug('Protocol V2 fallback cancel error', cancelError);
|
|
1591
1603
|
});
|
|
1604
|
+
} else if (!this.hasDeviceAcquire()) {
|
|
1605
|
+
// Pairing / connect-native / probe: drop the physical link only.
|
|
1606
|
+
// Never acquire or send protocol Cancel just to abort setup.
|
|
1607
|
+
if (this.mainId && this.deviceConnector?.disconnect) {
|
|
1608
|
+
await this.deviceConnector.disconnect(this.mainId).catch(disconnectError => {
|
|
1609
|
+
Log.debug('Ignored disconnect during user cancel without acquire', disconnectError);
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
this.markTransportDisconnected();
|
|
1592
1613
|
}
|
|
1593
1614
|
await this.commands?.cancel();
|
|
1594
1615
|
|
|
@@ -1652,6 +1673,36 @@ export class Device extends EventEmitter {
|
|
|
1652
1673
|
return typeof this.originalDescriptor.session === 'string';
|
|
1653
1674
|
}
|
|
1654
1675
|
|
|
1676
|
+
wasInterruptedByUser() {
|
|
1677
|
+
return this.interruptedByUser;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
private throwIfInterruptedByUser() {
|
|
1681
|
+
if (this.interruptedByUser) {
|
|
1682
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Protocol Cancel is only for an acquired session that is already in a
|
|
1688
|
+
* user-facing prompt. Connect, probe and initialize must not send Cancel
|
|
1689
|
+
* and must not re-acquire just to deliver one.
|
|
1690
|
+
*/
|
|
1691
|
+
private shouldSendFallbackProtocolCancel() {
|
|
1692
|
+
if (!this.hasDeviceAcquire() || !this.isProtocolV2()) {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
if (!this.hasOpenProtocolV2UiInteraction()) {
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
const env = DataManager.getSettings('env');
|
|
1699
|
+
return (
|
|
1700
|
+
DataManager.isBleConnect(env) ||
|
|
1701
|
+
DataManager.isBrowserWebUsb(env) ||
|
|
1702
|
+
DataManager.isDesktopWebUsb(env)
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1655
1706
|
hasDeviceAcquire() {
|
|
1656
1707
|
const env = DataManager.getSettings('env');
|
|
1657
1708
|
if (DataManager.isBleConnect(env)) {
|
package/src/index.ts
CHANGED
|
@@ -21,10 +21,6 @@ export { executeCallback, cleanupCallback };
|
|
|
21
21
|
export { preloadSessionCache } from './device/Device';
|
|
22
22
|
export { projectFeatures as projectDeviceStateFeatures } from './device/DeviceStateProjector';
|
|
23
23
|
export { getMethodSupportedProtocols } from './api/utils';
|
|
24
|
-
export {
|
|
25
|
-
parseProtocolV2ResourceManifest,
|
|
26
|
-
selectProtocolV2ResourceManifestFiles,
|
|
27
|
-
} from './protocols/protocol-v2/resources';
|
|
28
24
|
export { prepareFirmwareUpdateV4MemoryHost } from './api/firmware/FirmwareMemoryHost';
|
|
29
25
|
export type {
|
|
30
26
|
FirmwareMemoryArtifact,
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
IProtocolV2Resources,
|
|
5
|
-
} from '../../types';
|
|
6
|
-
import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
|
|
1
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
|
+
|
|
3
|
+
import type { IProtocolV2Resources, IVersionArray } from '../../types';
|
|
7
4
|
|
|
8
5
|
export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH =
|
|
9
6
|
'vol0:/loaders/bootloader/boot_resource.okpkg';
|
|
10
7
|
export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
|
|
11
8
|
export const PROTOCOL_V2_ROM_PARAMS_PACKAGE_PATH = 'vol0:/loaders/rom/params.okpkg';
|
|
9
|
+
export const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE = 0x5f90;
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (normalized.length !== expectedLength || !/^[0-9a-f]+$/.test(normalized)) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
`Invalid Pro2 resource ${field}: expected ${expectedLength} hexadecimal characters`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
11
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_VERSION = 1;
|
|
12
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET = 0x6c;
|
|
13
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_SIZE = 64;
|
|
14
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET = 0x200;
|
|
15
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET = 0x240;
|
|
16
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE = 64;
|
|
17
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_TYPE = 'RESC';
|
|
27
18
|
|
|
28
19
|
/** Validate a complete Pro2 stable resource set from remote configuration. */
|
|
29
20
|
export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources | undefined {
|
|
@@ -58,114 +49,113 @@ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources |
|
|
|
58
49
|
};
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
const
|
|
62
|
-
'vol0:/bundles/',
|
|
63
|
-
'vol0:/loaders/rom/',
|
|
64
|
-
] as const;
|
|
52
|
+
const PROTOCOL_V2_RESOURCE_DEVICE_ROOTS = ['vol0:/bundles/', 'vol0:/loaders/rom/'] as const;
|
|
65
53
|
|
|
66
|
-
function
|
|
54
|
+
function isAllowedResourceDevicePath(path: string): boolean {
|
|
67
55
|
if (
|
|
68
|
-
!path.endsWith('.okpkg') ||
|
|
69
56
|
path.includes('\\') ||
|
|
70
57
|
path.includes('//') ||
|
|
58
|
+
[...path].some(char => {
|
|
59
|
+
const code = char.charCodeAt(0);
|
|
60
|
+
return code <= 0x1f || code === 0x7f;
|
|
61
|
+
}) ||
|
|
71
62
|
path.split('/').some(part => part === '.' || part === '..')
|
|
72
63
|
) {
|
|
73
64
|
return false;
|
|
74
65
|
}
|
|
75
|
-
if (path ===
|
|
66
|
+
if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH) {
|
|
76
67
|
return true;
|
|
77
68
|
}
|
|
78
|
-
return
|
|
69
|
+
return (
|
|
70
|
+
path.endsWith('.okpkg') && PROTOCOL_V2_RESOURCE_DEVICE_ROOTS.some(root => path.startsWith(root))
|
|
71
|
+
);
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return value;
|
|
74
|
+
function readAscii(bytes: Uint8Array, offset: number, length: number): string {
|
|
75
|
+
return Array.from(bytes.slice(offset, offset + length))
|
|
76
|
+
.map(byte => String.fromCharCode(byte))
|
|
77
|
+
.join('');
|
|
86
78
|
}
|
|
87
79
|
|
|
88
|
-
function
|
|
89
|
-
const
|
|
80
|
+
function readResourceDevicePath(bytes: Uint8Array): string {
|
|
81
|
+
const metadata = bytes.slice(
|
|
82
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET,
|
|
83
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_SIZE
|
|
84
|
+
);
|
|
85
|
+
const terminator = metadata.indexOf(0);
|
|
86
|
+
const pathBytes = terminator === -1 ? metadata : metadata.slice(0, terminator);
|
|
87
|
+
const padding = terminator === -1 ? new Uint8Array(0) : metadata.slice(terminator);
|
|
90
88
|
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
path.split('/').some(part => !part || part === '.' || part === '..')
|
|
89
|
+
pathBytes.byteLength === 0 ||
|
|
90
|
+
Array.from(pathBytes).some(byte => byte < 0x20 || byte > 0x7e) ||
|
|
91
|
+
Array.from(padding).some(byte => byte !== 0)
|
|
95
92
|
) {
|
|
96
|
-
throw new Error(
|
|
93
|
+
throw new Error('Invalid Pro2 RESOURCE package device path metadata');
|
|
94
|
+
}
|
|
95
|
+
const path = readAscii(pathBytes, 0, pathBytes.byteLength);
|
|
96
|
+
if (!isAllowedResourceDevicePath(path)) {
|
|
97
|
+
throw new Error(`Invalid Pro2 RESOURCE package device path: ${path}`);
|
|
97
98
|
}
|
|
98
99
|
return path;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const file = value as Partial<IProtocolV2ResourceManifestFile>;
|
|
109
|
-
const archivePath = assertManifestRelativePath(file.archive_path, `files[${index}].archive_path`);
|
|
110
|
-
const originalName =
|
|
111
|
-
file.original_name === undefined
|
|
112
|
-
? archivePath.split('/').pop() ?? archivePath
|
|
113
|
-
: assertManifestRelativePath(file.original_name, `files[${index}].original_name`);
|
|
114
|
-
if (originalName.includes('/')) {
|
|
115
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}].original_name`);
|
|
116
|
-
}
|
|
117
|
-
const devicePath = assertManifestString(file.device_path, `files[${index}].device_path`);
|
|
118
|
-
if (!isAllowedManifestDevicePath(devicePath)) {
|
|
119
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}].device_path`);
|
|
120
|
-
}
|
|
121
|
-
if (!Number.isSafeInteger(file.size) || Number(file.size) <= 0) {
|
|
122
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}].size`);
|
|
123
|
-
}
|
|
124
|
-
const digest = normalizeHex(file.sha256, SHA256_HEX_LENGTH, `files[${index}].sha256`);
|
|
125
|
-
if (!archivePath.endsWith('.okpkg') || !originalName.endsWith('.okpkg')) {
|
|
126
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}] package extension`);
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
archive_path: archivePath,
|
|
130
|
-
original_name: originalName,
|
|
131
|
-
device_path: devicePath,
|
|
132
|
-
size: Number(file.size),
|
|
133
|
-
sha256: digest,
|
|
134
|
-
...(file.signed === undefined ? {} : { signed: file.signed }),
|
|
135
|
-
...(file.sig_algo === undefined ? {} : { sig_algo: file.sig_algo }),
|
|
136
|
-
...(file.payload_version === undefined ? {} : { payload_version: file.payload_version }),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
102
|
+
export type ProtocolV2ResourcePackageHeader = {
|
|
103
|
+
version: IVersionArray;
|
|
104
|
+
payloadLength: number;
|
|
105
|
+
devicePath: string;
|
|
106
|
+
payloadHash: string;
|
|
107
|
+
headerHash: string;
|
|
108
|
+
};
|
|
139
109
|
|
|
140
|
-
export function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
110
|
+
export function parseProtocolV2ResourcePackageHeader(
|
|
111
|
+
bytes: Uint8Array,
|
|
112
|
+
packageSize: number
|
|
113
|
+
): ProtocolV2ResourcePackageHeader {
|
|
114
|
+
if (bytes.byteLength < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE) {
|
|
115
|
+
throw new Error('Pro2 RESOURCE package is shorter than its header');
|
|
116
|
+
}
|
|
117
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
118
|
+
const headerVersion = view.getUint32(0x04, true);
|
|
119
|
+
const headerLength = view.getUint32(0x0c, true);
|
|
120
|
+
const payloadLength = view.getUint32(0x14, true);
|
|
151
121
|
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
122
|
+
readAscii(bytes, 0, 4) !== 'OKPP' ||
|
|
123
|
+
readAscii(bytes, 0x08, 4) !== PROTOCOL_V2_RESOURCE_PACKAGE_TYPE ||
|
|
124
|
+
headerVersion !== PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_VERSION ||
|
|
125
|
+
headerLength !== PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE ||
|
|
126
|
+
payloadLength <= 0 ||
|
|
127
|
+
headerLength + payloadLength !== packageSize
|
|
155
128
|
) {
|
|
156
|
-
throw new Error('Invalid Pro2
|
|
129
|
+
throw new Error('Invalid Pro2 RESOURCE package header');
|
|
157
130
|
}
|
|
131
|
+
|
|
132
|
+
const packedVersion = view.getUint32(0x10, true);
|
|
158
133
|
return {
|
|
159
|
-
|
|
134
|
+
version: [
|
|
135
|
+
Math.floor(packedVersion / 0x10000) % 0x100,
|
|
136
|
+
Math.floor(packedVersion / 0x100) % 0x100,
|
|
137
|
+
packedVersion % 0x100,
|
|
138
|
+
],
|
|
139
|
+
payloadLength,
|
|
140
|
+
devicePath: readResourceDevicePath(bytes),
|
|
141
|
+
payloadHash: bytesToHex(
|
|
142
|
+
bytes.slice(
|
|
143
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET,
|
|
144
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE
|
|
145
|
+
)
|
|
146
|
+
),
|
|
147
|
+
headerHash: bytesToHex(
|
|
148
|
+
bytes.slice(
|
|
149
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET,
|
|
150
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE
|
|
151
|
+
)
|
|
152
|
+
),
|
|
160
153
|
};
|
|
161
154
|
}
|
|
162
155
|
|
|
163
|
-
export function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
targetsToUpdate: readonly FirmwareUpdateV4Target[];
|
|
169
|
-
}): IProtocolV2ResourceManifestFile[] {
|
|
170
|
-
return targetsToUpdate.includes('resource') ? [...manifest.files] : [];
|
|
156
|
+
export function parseProtocolV2ResourcePackage(
|
|
157
|
+
binary: ArrayBuffer | Uint8Array
|
|
158
|
+
): ProtocolV2ResourcePackageHeader {
|
|
159
|
+
const bytes = binary instanceof Uint8Array ? binary : new Uint8Array(binary);
|
|
160
|
+
return parseProtocolV2ResourcePackageHeader(bytes, bytes.byteLength);
|
|
171
161
|
}
|
package/src/types/settings.ts
CHANGED
|
@@ -74,21 +74,6 @@ export type IProtocolV2Resources = {
|
|
|
74
74
|
source: IProtocolV2ResourceSource;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
export type IProtocolV2ResourceManifestFile = {
|
|
78
|
-
archive_path: string;
|
|
79
|
-
original_name?: string;
|
|
80
|
-
device_path: string;
|
|
81
|
-
size: number;
|
|
82
|
-
sha256: string;
|
|
83
|
-
signed?: boolean;
|
|
84
|
-
sig_algo?: string;
|
|
85
|
-
payload_version?: string | null;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type IProtocolV2ResourceManifest = {
|
|
89
|
-
files: IProtocolV2ResourceManifestFile[];
|
|
90
|
-
};
|
|
91
|
-
|
|
92
77
|
/** STM32 firmware config */
|
|
93
78
|
export type IFirmwareReleaseInfo = {
|
|
94
79
|
required: boolean;
|
|
@@ -30,8 +30,13 @@ export const getDeviceTypeByBleName = (name?: string): IDeviceType => {
|
|
|
30
30
|
if (/^T/i.test(name)) return EDeviceType.Touch;
|
|
31
31
|
if (/^Touch/i.test(name)) return EDeviceType.Touch;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
if (/\
|
|
33
|
+
const compactName = name.replace(/[\s-]/g, '');
|
|
34
|
+
if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name) || /^(?:OneKey)?Pro2/i.test(compactName)) {
|
|
35
|
+
return EDeviceType.Pro2;
|
|
36
|
+
}
|
|
37
|
+
if (/\bNeo\b/i.test(name) || /^Neo/i.test(name) || /^(?:OneKey)?Neo/i.test(compactName)) {
|
|
38
|
+
return EDeviceType.Neo;
|
|
39
|
+
}
|
|
35
40
|
if (/\bPro\b/i.test(name) || /^Pro/i.test(name)) return EDeviceType.Pro;
|
|
36
41
|
|
|
37
42
|
return EDeviceType.Unknown;
|