@onekeyfe/hd-core 1.2.2-alpha.0 → 1.2.2-alpha.10

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.
Files changed (38) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +67 -0
  3. package/__tests__/deviceUploadNft.test.ts +27 -0
  4. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +322 -21
  5. package/__tests__/open-wallet-session.test.ts +613 -80
  6. package/__tests__/pro2HostAssetPackage.test.ts +108 -1
  7. package/__tests__/protocol-v2.test.ts +231 -49
  8. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  9. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  12. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  13. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  14. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  15. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  16. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  17. package/dist/core/RequestQueue.d.ts +2 -0
  18. package/dist/core/RequestQueue.d.ts.map +1 -1
  19. package/dist/core/index.d.ts +2 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/index.d.ts +12 -1
  22. package/dist/index.js +1454 -1144
  23. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  24. package/dist/utils/patch.d.ts +1 -1
  25. package/dist/utils/patch.d.ts.map +1 -1
  26. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/api/FirmwareUpdateV4.ts +73 -23
  29. package/src/api/OpenWalletSession.ts +0 -3
  30. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
  31. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  32. package/src/api/protocol-v2/DeviceUploadNft.ts +4 -1
  33. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +17 -3
  34. package/src/core/RequestQueue.ts +16 -1
  35. package/src/core/index.ts +48 -21
  36. package/src/data/messages/messages-protocol-v2.json +1471 -1367
  37. package/src/protocols/protocol-v2/walletSession.ts +157 -38
  38. package/src/utils/pro2HostAssetPackage.ts +143 -58
@@ -63,32 +63,41 @@ const negotiateEventlessWalletSession = async (device: Device) => {
63
63
  const getDeviceSession = async (device: Device, request: DeviceSessionGet) =>
64
64
  device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
65
65
 
66
+ const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard];
67
+ const CARDANO_SEED_DOMAINS = [
68
+ DeviceSessionSeedDomain.SeedDomain_Standard,
69
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
70
+ ];
71
+
72
+ const buildDeviceSessionSeedDomains = (deriveCardano?: boolean): DeviceSessionSeedDomain[] =>
73
+ deriveCardano === true ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS;
74
+
75
+ const deviceSessionHasCardano = (message: { seed_domains?: DeviceSessionSeedDomain[] }) =>
76
+ Array.isArray(message.seed_domains) &&
77
+ message.seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano);
78
+
79
+ // origin/dev Get is read/resume only. Seed generation lives on AskPassphrase.
66
80
  const buildDeviceSessionGetRequest = ({
67
81
  sessionId,
68
82
  expectedPassphraseState,
69
- deriveCardano,
70
83
  }: {
71
84
  sessionId?: string;
72
85
  expectedPassphraseState?: string;
73
- deriveCardano?: boolean;
74
86
  } = {}): DeviceSessionGet => ({
75
87
  ...(sessionId ? { session_id: sessionId } : {}),
76
88
  ...(expectedPassphraseState ? { btc_test_address: expectedPassphraseState } : {}),
77
- seed_domains:
78
- deriveCardano === undefined
79
- ? []
80
- : [
81
- DeviceSessionSeedDomain.SeedDomain_Standard,
82
- ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
83
- ],
84
89
  });
85
90
 
86
91
  const askDevicePassphrase = async (
87
92
  device: Device,
88
- requestPayload: DeviceSessionAskPassphrase,
93
+ requestPayload: Omit<DeviceSessionAskPassphrase, 'seed_domains'>,
94
+ deriveCardano?: boolean,
89
95
  onStatusRefreshed?: () => void
90
96
  ) => {
91
- await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload);
97
+ await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
98
+ ...requestPayload,
99
+ seed_domains: buildDeviceSessionSeedDomains(deriveCardano),
100
+ });
92
101
  await refreshProtocolV2DeviceStatus(device);
93
102
  onStatusRefreshed?.();
94
103
  };
@@ -160,7 +169,22 @@ const selectDeviceSession = async (
160
169
  interaction: attachPinInteraction,
161
170
  });
162
171
  onStatusRefreshed?.();
163
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
172
+ if (deriveCardano === true) {
173
+ await askDevicePassphrase(
174
+ device,
175
+ { passphrase: '', on_device: false },
176
+ true,
177
+ onStatusRefreshed
178
+ );
179
+ // Firmware AskPassphrase Success clears unlocked_by_attach_to_pin.
180
+ // Identity is still the Attach PIN wallet; keep the SDK flag so a later
181
+ // passphrase picker still locks instead of prompting.
182
+ if (device.features) {
183
+ device.features.unlockedAttachPin = true;
184
+ }
185
+ }
186
+ const attachPinSession = await getDeviceSession(device, buildDeviceSessionGetRequest());
187
+ return Object.assign(attachPinSession, { viaAttachPin: true as const });
164
188
  }
165
189
 
166
190
  if (hasHostPassphrase) {
@@ -170,9 +194,10 @@ const selectDeviceSession = async (
170
194
  passphrase: hostPassphrase,
171
195
  on_device: false,
172
196
  },
197
+ deriveCardano,
173
198
  onStatusRefreshed
174
199
  );
175
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
200
+ return getDeviceSession(device, buildDeviceSessionGetRequest());
176
201
  }
177
202
 
178
203
  const passphraseOnDeviceInteraction = device.createProtocolV2UiPhaseMetadata?.(
@@ -183,8 +208,8 @@ const selectDeviceSession = async (
183
208
  ...metadata,
184
209
  ...(passphraseOnDeviceInteraction ? { interaction: passphraseOnDeviceInteraction } : {}),
185
210
  });
186
- await askDevicePassphrase(device, { on_device: true }, onStatusRefreshed);
187
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
211
+ await askDevicePassphrase(device, { on_device: true }, deriveCardano, onStatusRefreshed);
212
+ return getDeviceSession(device, buildDeviceSessionGetRequest());
188
213
  };
189
214
 
190
215
  export async function getProtocolV2WalletSession(
@@ -209,6 +234,10 @@ export async function getProtocolV2WalletSession(
209
234
  const forceWalletSelection =
210
235
  options?.forceWalletSelection === true || options?.initSession === true;
211
236
  const readCurrentAttachPinSession = options?.readCurrentAttachPinSession === true;
237
+ const sessionIsAttachPinWallet = (session?: { viaAttachPin?: boolean }) =>
238
+ readCurrentAttachPinSession ||
239
+ session?.viaAttachPin === true ||
240
+ device.features?.unlockedAttachPin === true;
212
241
 
213
242
  if (forceWalletSelection) {
214
243
  if (options.onlyMainPin) {
@@ -244,6 +273,10 @@ export async function getProtocolV2WalletSession(
244
273
  const markWalletStatusRefreshed = () => {
245
274
  walletStatusRefreshed = true;
246
275
  };
276
+ if (options?.onlyMainPin && options.mainPinSelected !== true) {
277
+ await refreshProtocolV2DeviceStatus(device);
278
+ markWalletStatusRefreshed();
279
+ }
247
280
  let mainPinAuthenticated =
248
281
  options?.mainPinSelected === true ||
249
282
  (options?.onlyMainPin === true &&
@@ -259,6 +292,38 @@ export async function getProtocolV2WalletSession(
259
292
  }
260
293
  };
261
294
 
295
+ const sessionGetRequest = ({
296
+ sessionId,
297
+ expectedPassphraseState: passphraseState,
298
+ }: {
299
+ sessionId?: string;
300
+ expectedPassphraseState?: string;
301
+ } = {}) =>
302
+ buildDeviceSessionGetRequest({
303
+ sessionId,
304
+ expectedPassphraseState: passphraseState,
305
+ });
306
+
307
+ const askEmptyPassphraseAndGet = async ({
308
+ deriveCardano,
309
+ keepAttachPin,
310
+ }: {
311
+ deriveCardano?: boolean;
312
+ keepAttachPin?: boolean;
313
+ } = {}) => {
314
+ await askDevicePassphrase(
315
+ device,
316
+ { passphrase: '', on_device: false },
317
+ deriveCardano,
318
+ markWalletStatusRefreshed
319
+ );
320
+ if (keepAttachPin && device.features) {
321
+ device.features.unlockedAttachPin = true;
322
+ }
323
+ const session = await getDeviceSession(device, buildDeviceSessionGetRequest());
324
+ return keepAttachPin ? Object.assign(session, { viaAttachPin: true as const }) : session;
325
+ };
326
+
262
327
  const rejectMismatchedAttachPinWallet = async () => {
263
328
  const features = await refreshProtocolV2DeviceStatus(device);
264
329
  markWalletStatusRefreshed();
@@ -280,6 +345,19 @@ export async function getProtocolV2WalletSession(
280
345
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
281
346
  };
282
347
 
348
+ // The passphrase picker would prompt. Empty host AskPassphrase does not;
349
+ // that path is Attach PIN / standard Cardano. Switching to a different
350
+ // passphrase wallet still locks first.
351
+ const lockAttachPinBeforePassphraseSelection = async () => {
352
+ if (readCurrentAttachPinSession || options?.onlyMainPin) {
353
+ return;
354
+ }
355
+ if (device.features?.unlockedAttachPin !== true) {
356
+ return;
357
+ }
358
+ await rejectMismatchedAttachPinWallet();
359
+ };
360
+
283
361
  if (options?.onlyMainPin && options.rejectAttachPinForMainWallet) {
284
362
  await rejectMismatchedAttachPinWallet();
285
363
  }
@@ -297,7 +375,7 @@ export async function getProtocolV2WalletSession(
297
375
  }
298
376
  };
299
377
 
300
- const selectStandardWallet = async () => {
378
+ const selectStandardWallet = async (forceMainPin = false) => {
301
379
  if (device.features?.passphraseProtection === true) {
302
380
  // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
303
381
  await selectMainPin();
@@ -307,13 +385,16 @@ export async function getProtocolV2WalletSession(
307
385
  passphrase: '',
308
386
  on_device: false,
309
387
  },
388
+ options?.deriveCardano,
310
389
  markWalletStatusRefreshed
311
390
  );
312
391
  standardWalletSelected = true;
313
392
  } else if (!standardWalletSelected) {
314
393
  // Without passphrase protection there is no empty-passphrase selector.
315
- // Main PIN selection is the only authoritative switch back to the standard wallet.
316
- await selectMainPin(true);
394
+ // An unlocked non-Attach-PIN device is already in the only available wallet context.
395
+ // Force Main PIN only when recovering from a mismatched cached standard session.
396
+ await selectMainPin(forceMainPin);
397
+ standardWalletSelected = true;
317
398
  }
318
399
  };
319
400
 
@@ -329,10 +410,7 @@ export async function getProtocolV2WalletSession(
329
410
  device.clearInternalState();
330
411
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
331
412
  }
332
- response = await getDeviceSession(
333
- device,
334
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
335
- );
413
+ response = await getDeviceSession(device, sessionGetRequest());
336
414
  } else if (options?.onlyMainPin) {
337
415
  expectedPassphraseState = cachedStandardSession?.passphraseState;
338
416
  if (cachedStandardSession) {
@@ -342,10 +420,9 @@ export async function getProtocolV2WalletSession(
342
420
  }
343
421
  response = await getDeviceSession(
344
422
  device,
345
- buildDeviceSessionGetRequest({
423
+ sessionGetRequest({
346
424
  sessionId: cachedStandardSession.sessionId,
347
425
  expectedPassphraseState,
348
- deriveCardano: options?.deriveCardano,
349
426
  })
350
427
  );
351
428
  resumed = true;
@@ -360,19 +437,15 @@ export async function getProtocolV2WalletSession(
360
437
 
361
438
  if (!response) {
362
439
  await selectStandardWallet();
363
- response = await getDeviceSession(
364
- device,
365
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
366
- );
440
+ response = await getDeviceSession(device, sessionGetRequest());
367
441
  }
368
442
  } else if (cachedSessionId && expectedPassphraseState) {
369
443
  try {
370
444
  response = await getDeviceSession(
371
445
  device,
372
- buildDeviceSessionGetRequest({
446
+ sessionGetRequest({
373
447
  sessionId: cachedSessionId,
374
448
  expectedPassphraseState,
375
- deriveCardano: options?.deriveCardano,
376
449
  })
377
450
  );
378
451
  resumed = true;
@@ -387,9 +460,8 @@ export async function getProtocolV2WalletSession(
387
460
  try {
388
461
  response = await getDeviceSession(
389
462
  device,
390
- buildDeviceSessionGetRequest({
463
+ sessionGetRequest({
391
464
  expectedPassphraseState,
392
- deriveCardano: options?.deriveCardano,
393
465
  })
394
466
  );
395
467
  } catch (error) {
@@ -404,6 +476,7 @@ export async function getProtocolV2WalletSession(
404
476
  device.clearInternalState();
405
477
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
406
478
  }
479
+ await lockAttachPinBeforePassphraseSelection();
407
480
  response = await selectDeviceSession(
408
481
  device,
409
482
  expectedPassphraseState,
@@ -432,13 +505,11 @@ export async function getProtocolV2WalletSession(
432
505
  }
433
506
  if (options?.onlyMainPin) {
434
507
  device.clearStandardInternalState?.();
435
- await selectStandardWallet();
436
- response = await getDeviceSession(
437
- device,
438
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
439
- );
508
+ await selectStandardWallet(true);
509
+ response = await getDeviceSession(device, sessionGetRequest());
440
510
  } else {
441
511
  device.clearInternalState();
512
+ await lockAttachPinBeforePassphraseSelection();
442
513
  response = await selectDeviceSession(
443
514
  device,
444
515
  expectedPassphraseState,
@@ -464,6 +535,54 @@ export async function getProtocolV2WalletSession(
464
535
  }
465
536
  }
466
537
 
538
+ // origin/dev generates Cardano on AskPassphrase, not Get. Empty host
539
+ // passphrase is the Attach PIN / standard-wallet secret. Hidden wallets
540
+ // still need a real passphrase Ask. Passphrase-off Get auto-requests Cardano.
541
+ if (options?.deriveCardano === true && !deviceSessionHasCardano(message)) {
542
+ if (options?.resumeOnly) {
543
+ device.clearInternalState();
544
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
545
+ }
546
+ resumed = false;
547
+ const previousAddress = message.btc_test_address;
548
+ if (sessionIsAttachPinWallet(response)) {
549
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true, keepAttachPin: true });
550
+ } else if (device.features?.passphraseProtection === false) {
551
+ response = await getDeviceSession(device, buildDeviceSessionGetRequest());
552
+ } else if (options?.onlyMainPin) {
553
+ await selectMainPin();
554
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true });
555
+ } else {
556
+ await lockAttachPinBeforePassphraseSelection();
557
+ response = await selectDeviceSession(
558
+ device,
559
+ expectedPassphraseState,
560
+ true,
561
+ markWalletStatusRefreshed
562
+ );
563
+ }
564
+ message = response.message;
565
+ try {
566
+ assertCompleteDeviceSession(message);
567
+ } catch (error) {
568
+ if (options?.onlyMainPin) {
569
+ device.clearStandardInternalState?.();
570
+ } else {
571
+ device.clearInternalState();
572
+ }
573
+ throw error;
574
+ }
575
+ if (previousAddress && previousAddress !== message.btc_test_address) {
576
+ await rejectMismatchedAttachPinWallet();
577
+ clearCurrentWalletSession();
578
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
579
+ }
580
+ if (!deviceSessionHasCardano(message)) {
581
+ clearCurrentWalletSession();
582
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
583
+ }
584
+ }
585
+
467
586
  const internalStateArgs = [
468
587
  true,
469
588
  message.btc_test_address,
@@ -478,7 +597,7 @@ export async function getProtocolV2WalletSession(
478
597
  }
479
598
 
480
599
  let unlockedAttachPin: boolean | undefined;
481
- if (readCurrentAttachPinSession) {
600
+ if (readCurrentAttachPinSession || sessionIsAttachPinWallet(response)) {
482
601
  unlockedAttachPin = true;
483
602
  } else if (mainPinAuthenticated) {
484
603
  unlockedAttachPin = false;
@@ -1,30 +1,24 @@
1
1
  import { sha3_512 } from '@noble/hashes/sha3';
2
2
  import semver from 'semver';
3
3
 
4
- // The raw LZ4 block encoder below is adapted from lz4-lite 1.1.2.
5
- //
6
- // MIT License
7
- // Copyright (c) 2026 Alexander Vukov
8
- // Permission is hereby granted, free of charge, to any person obtaining a copy
9
- // of this software and associated documentation files (the "Software"), to deal
10
- // in the Software without restriction, including without limitation the rights
11
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
- // copies of the Software, and to permit persons to whom the Software is
13
- // furnished to do so, subject to the following conditions:
14
- // The above copyright notice and this permission notice shall be included in all
15
- // copies or substantial portions of the Software.
16
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- // SOFTWARE.
4
+ /**
5
+ * Builds the unsigned host-asset package consumed by Pro2 firmware:
6
+ *
7
+ * OKPP RESOURCE container
8
+ * └── OKAR archive
9
+ * └── independently compressed raw LZ4 blocks
10
+ *
11
+ * OKPP and OKAR are OneKey formats. Their constants mirror firmware-pro2's
12
+ * payload_package headers; the LZ4 bytes inside each block follow the standard
13
+ * raw block format and deliberately do not use an LZ4 frame or size prefix.
14
+ */
23
15
 
24
16
  /* eslint-disable no-bitwise */
25
17
 
26
18
  export const PRO2_HOST_ASSET_PACKAGE_MIN_VERSION = '1.0.1';
27
19
 
20
+ // OKPP container layout. The fixed header has seven empty signature slots even
21
+ // for a host-generated unsigned package.
28
22
  const CONTAINER_HEADER_SIZE = 0x5f90;
29
23
  const CONTAINER_HEADER_HASH_INPUT_LENGTH = 0x240;
30
24
  const CONTAINER_HASH_SECTION_OFFSET = 0x200;
@@ -33,6 +27,9 @@ const CONTAINER_HEADER_MAGIC = 0x50504b4f;
33
27
  const CONTAINER_HEADER_VERSION = 1;
34
28
  const CONTAINER_RESOURCE_TYPE_MAGIC = 0x43534552;
35
29
  const CONTAINER_ED25519_SIGNATURE_ALGORITHM = 0x71717171;
30
+ const HOST_ASSET_PACKAGE_MAX_SIZE = 4 * 1024 * 1024;
31
+
32
+ // OKAR archive layout.
36
33
  const ARCHIVE_MAGIC = 0x52414b4f;
37
34
  const ARCHIVE_VERSION = 1;
38
35
  const ARCHIVE_HEADER_SIZE = 42;
@@ -40,16 +37,9 @@ const ARCHIVE_ENTRY_SIZE = 296;
40
37
  const ARCHIVE_ENTRY_NAME_MAX_LENGTH = 255;
41
38
  const ARCHIVE_COMPRESS_LZ4_BLOCKED = 1;
42
39
  const ARCHIVE_ALIGNMENT = 4;
43
- const LZ4_BLOCK_SIZE_LOG2 = 12;
44
- const LZ4_MIN_MATCH = 4;
45
- const LZ4_LAST_LITERALS = 5;
46
- const LZ4_MATCH_FIND_LIMIT = 12;
47
- const LZ4_MAX_OFFSET = 0xffff;
48
- const LZ4_LENGTH_MASK = 15;
49
- const LZ4_HASH_LOG = 16;
50
- const LZ4_HASH_MULTIPLIER = 2654435761;
51
- const LZ4_SKIP_TRIGGER = 6;
52
- const HOST_ASSET_PACKAGE_MAX_SIZE = 4 * 1024 * 1024;
40
+ const LZ4_PREFERRED_BLOCK_SIZE_LOG2 = 14;
41
+ const LZ4_FALLBACK_BLOCK_SIZE_LOG2 = 13;
42
+ const LZ4_COMPRESSED_BLOCK_SIZE_MAX = 1 << 14;
53
43
 
54
44
  export type Pro2HostAssetPackageEntry = {
55
45
  name: string;
@@ -70,10 +60,6 @@ export function supportsPro2HostAssetPackage(firmwareVersion: string | undefined
70
60
  );
71
61
  }
72
62
 
73
- function align(value: number): number {
74
- return (value + ARCHIVE_ALIGNMENT - 1) & ~(ARCHIVE_ALIGNMENT - 1);
75
- }
76
-
77
63
  function concatBytes(parts: Uint8Array[]): Uint8Array {
78
64
  const output = new Uint8Array(parts.reduce((length, part) => length + part.byteLength, 0));
79
65
  let offset = 0;
@@ -84,6 +70,41 @@ function concatBytes(parts: Uint8Array[]): Uint8Array {
84
70
  return output;
85
71
  }
86
72
 
73
+ // Raw LZ4 block encoder
74
+ // ---------------------
75
+ //
76
+ // This encoder is adapted from lz4-lite 1.1.2 and intentionally kept local so
77
+ // every SDK runtime uses the same dependency-free implementation. Keep this
78
+ // section isolated from the OneKey package writer below. Firmware requires raw
79
+ // blocks here; replacing it with an LZ4 frame encoder is not compatible.
80
+ //
81
+ // MIT License
82
+ // Copyright (c) 2026 Alexander Vukov
83
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
84
+ // of this software and associated documentation files (the "Software"), to deal
85
+ // in the Software without restriction, including without limitation the rights
86
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87
+ // copies of the Software, and to permit persons to whom the Software is
88
+ // furnished to do so, subject to the following conditions:
89
+ // The above copyright notice and this permission notice shall be included in all
90
+ // copies or substantial portions of the Software.
91
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
92
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
93
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
94
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
95
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
96
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
97
+ // SOFTWARE.
98
+
99
+ const LZ4_MIN_MATCH = 4;
100
+ const LZ4_LAST_LITERALS = 5;
101
+ const LZ4_MATCH_FIND_LIMIT = 12;
102
+ const LZ4_MAX_OFFSET = 0xffff;
103
+ const LZ4_LENGTH_MASK = 15;
104
+ const LZ4_HASH_LOG = 16;
105
+ const LZ4_HASH_MULTIPLIER = 2654435761;
106
+ const LZ4_MAX_SEARCH_DEPTH = 64;
107
+
87
108
  function writeExtendedLength(output: Uint8Array, offset: number, length: number): number {
88
109
  let remaining = length;
89
110
  let nextOffset = offset;
@@ -159,7 +180,11 @@ function emitLastLiterals(
159
180
  return copyBytes(output, nextOffset, input, anchor, literalLength);
160
181
  }
161
182
 
162
- function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Array {
183
+ function compressRawLz4Block(
184
+ input: Uint8Array,
185
+ hashTable: Uint32Array,
186
+ matchChain: Int32Array
187
+ ): Uint8Array {
163
188
  const output = new Uint8Array(input.byteLength + Math.floor(input.byteLength / 255) + 16);
164
189
  const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
165
190
  const matchFindLimit = input.byteLength - LZ4_MATCH_FIND_LIMIT;
@@ -167,42 +192,65 @@ function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Ar
167
192
  let anchor = 0;
168
193
  let inputOffset = 0;
169
194
  let outputOffset = 0;
170
- let searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
171
195
 
172
196
  hashTable.fill(0);
173
197
  while (inputOffset < matchFindLimit) {
174
198
  const sequence = inputView.getUint32(inputOffset, true);
175
199
  const hash = Math.imul(sequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
176
- const candidate = hashTable[hash] - 1;
200
+ let candidate = hashTable[hash] - 1;
201
+ matchChain[inputOffset] = candidate;
177
202
  hashTable[hash] = inputOffset + 1;
178
203
 
179
- const hasMatch = !(
180
- candidate < 0 ||
181
- inputOffset - candidate > LZ4_MAX_OFFSET ||
182
- inputView.getUint32(candidate, true) !== sequence
183
- );
184
- if (!hasMatch) {
185
- inputOffset += searchMatchCount >> LZ4_SKIP_TRIGGER;
186
- searchMatchCount += 1;
187
- } else {
188
- searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
189
- let matchEnd = inputOffset + LZ4_MIN_MATCH;
190
- let reference = candidate + LZ4_MIN_MATCH;
191
- while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
192
- matchEnd += 1;
193
- reference += 1;
204
+ let bestCandidate = -1;
205
+ let bestMatchEnd = inputOffset;
206
+ let searchDepth = 0;
207
+ while (
208
+ candidate >= 0 &&
209
+ inputOffset - candidate <= LZ4_MAX_OFFSET &&
210
+ searchDepth < LZ4_MAX_SEARCH_DEPTH
211
+ ) {
212
+ if (inputView.getUint32(candidate, true) === sequence) {
213
+ let matchEnd = inputOffset + LZ4_MIN_MATCH;
214
+ let reference = candidate + LZ4_MIN_MATCH;
215
+ while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
216
+ matchEnd += 1;
217
+ reference += 1;
218
+ }
219
+ if (matchEnd > bestMatchEnd) {
220
+ bestCandidate = candidate;
221
+ bestMatchEnd = matchEnd;
222
+ }
194
223
  }
224
+ candidate = matchChain[candidate];
225
+ searchDepth += 1;
226
+ }
227
+
228
+ if (bestCandidate < 0) {
229
+ inputOffset += 1;
230
+ } else {
231
+ const matchStart = inputOffset;
195
232
  outputOffset = emitSequence(
196
233
  output,
197
234
  outputOffset,
198
235
  input,
199
236
  anchor,
200
237
  inputOffset - anchor,
201
- inputOffset - candidate,
202
- matchEnd - inputOffset - LZ4_MIN_MATCH
238
+ inputOffset - bestCandidate,
239
+ bestMatchEnd - inputOffset - LZ4_MIN_MATCH
203
240
  );
204
- inputOffset = matchEnd;
241
+ inputOffset = bestMatchEnd;
205
242
  anchor = inputOffset;
243
+
244
+ for (
245
+ let skippedOffset = matchStart + 1;
246
+ skippedOffset < inputOffset && skippedOffset < matchFindLimit;
247
+ skippedOffset += 1
248
+ ) {
249
+ const skippedSequence = inputView.getUint32(skippedOffset, true);
250
+ const skippedHash = Math.imul(skippedSequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
251
+ matchChain[skippedOffset] = hashTable[skippedHash] - 1;
252
+ hashTable[skippedHash] = skippedOffset + 1;
253
+ }
206
254
  }
207
255
  }
208
256
 
@@ -210,27 +258,51 @@ function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Ar
210
258
  return output.slice(0, outputOffset);
211
259
  }
212
260
 
213
- function encodeLz4Blocked(data: Uint8Array): Uint8Array {
214
- const blockSize = 1 << LZ4_BLOCK_SIZE_LOG2;
261
+ // OneKey LZ4-blocked wrapper
262
+ // --------------------------
263
+ // The archive stores an 8-byte descriptor, one compressed-size value per
264
+ // block, then the concatenated raw blocks. Blocks are independent so firmware
265
+ // can validate and decompress them with bounded memory.
266
+ function encodeLz4BlockedWithBlockSize(
267
+ data: Uint8Array,
268
+ blockSizeLog2: number
269
+ ): Uint8Array | undefined {
270
+ const blockSize = 1 << blockSizeLog2;
215
271
  const blockCount = Math.ceil(data.byteLength / blockSize);
216
272
  const hashTable = new Uint32Array(1 << LZ4_HASH_LOG);
273
+ const matchChain = new Int32Array(blockSize);
217
274
  const blocks: Uint8Array[] = [];
218
275
  const header = new Uint8Array(8 + blockCount * 4);
219
276
  const headerView = new DataView(header.buffer);
220
277
  headerView.setUint16(0, blockCount, true);
221
- headerView.setUint16(2, LZ4_BLOCK_SIZE_LOG2, true);
278
+ headerView.setUint16(2, blockSizeLog2, true);
222
279
 
223
280
  for (let index = 0; index < blockCount; index += 1) {
224
281
  const block = compressRawLz4Block(
225
282
  data.subarray(index * blockSize, Math.min((index + 1) * blockSize, data.byteLength)),
226
- hashTable
283
+ hashTable,
284
+ matchChain
227
285
  );
286
+ if (block.byteLength > LZ4_COMPRESSED_BLOCK_SIZE_MAX) return undefined;
228
287
  headerView.setUint32(8 + index * 4, block.byteLength, true);
229
288
  blocks.push(block);
230
289
  }
231
290
  return concatBytes([header, ...blocks]);
232
291
  }
233
292
 
293
+ function encodeLz4Blocked(data: Uint8Array): Uint8Array {
294
+ const preferred = encodeLz4BlockedWithBlockSize(data, LZ4_PREFERRED_BLOCK_SIZE_LOG2);
295
+ if (preferred) return preferred;
296
+
297
+ const fallback = encodeLz4BlockedWithBlockSize(data, LZ4_FALLBACK_BLOCK_SIZE_LOG2);
298
+ if (!fallback) {
299
+ throw new Error('Pro2 host asset package LZ4 block exceeds the firmware buffer limit.');
300
+ }
301
+ return fallback;
302
+ }
303
+
304
+ // OKAR integrity helpers
305
+ // ----------------------
234
306
  const CRC32_TABLE = (() => {
235
307
  const table = new Uint32Array(256);
236
308
  for (let index = 0; index < table.length; index += 1) {
@@ -251,6 +323,12 @@ function crc32(data: Uint8Array): number {
251
323
  return (value ^ 0xffffffff) >>> 0;
252
324
  }
253
325
 
326
+ function align(value: number): number {
327
+ return (value + ARCHIVE_ALIGNMENT - 1) & ~(ARCHIVE_ALIGNMENT - 1);
328
+ }
329
+
330
+ // OKAR archive writer
331
+ // -------------------
254
332
  function buildArchive(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
255
333
  const textEncoder = new TextEncoder();
256
334
  let dataOffset = ARCHIVE_HEADER_SIZE + entries.length * ARCHIVE_ENTRY_SIZE;
@@ -289,6 +367,8 @@ function buildArchive(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
289
367
  return archive;
290
368
  }
291
369
 
370
+ // OKPP container writer
371
+ // ---------------------
292
372
  export function buildPro2HostAssetPackage(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
293
373
  if (entries.length === 0 || new Set(entries.map(entry => entry.name)).size !== entries.length) {
294
374
  throw new Error('Pro2 host asset package entries must have unique, non-empty names.');
@@ -303,6 +383,11 @@ export function buildPro2HostAssetPackage(entries: Pro2HostAssetPackageEntry[]):
303
383
  view.setUint32(0x0c, CONTAINER_HEADER_SIZE, true);
304
384
  view.setUint32(0x10, 1, true);
305
385
  view.setUint32(0x14, payload.byteLength, true);
386
+
387
+ // Host asset packages are unsigned, but firmware still requires a valid
388
+ // payload hash, header hash, and the Ed25519 algorithm discriminator. The
389
+ // zero-initialized header intentionally leaves sig_used_count and every
390
+ // signature slot empty.
306
391
  header.set(sha3_512(payload), CONTAINER_HASH_SECTION_OFFSET);
307
392
  header.set(sha3_512(header.subarray(0, CONTAINER_HEADER_HASH_INPUT_LENGTH)), 0x240);
308
393
  view.setUint32(CONTAINER_SIGNATURE_ALGORITHM_OFFSET, CONTAINER_ED25519_SIGNATURE_ALGORITHM, true);