@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.
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
- package/__tests__/device-lifecycle-events.test.ts +67 -0
- package/__tests__/deviceUploadNft.test.ts +27 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +322 -21
- package/__tests__/open-wallet-session.test.ts +613 -80
- package/__tests__/pro2HostAssetPackage.test.ts +108 -1
- package/__tests__/protocol-v2.test.ts +231 -49
- package/__tests__/protocolV2FileWrite.test.ts +40 -0
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
- package/dist/api/helpers/protocolV2FileWrite.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/RequestQueue.d.ts +2 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +1454 -1144
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +73 -23
- package/src/api/OpenWalletSession.ts +0 -3
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
- package/src/api/helpers/protocolV2FileWrite.ts +11 -5
- package/src/api/protocol-v2/DeviceUploadNft.ts +4 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +17 -3
- package/src/core/RequestQueue.ts +16 -1
- package/src/core/index.ts +48 -21
- package/src/data/messages/messages-protocol-v2.json +1471 -1367
- package/src/protocols/protocol-v2/walletSession.ts +157 -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',
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
44
|
-
const
|
|
45
|
-
const
|
|
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(
|
|
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
|
-
|
|
200
|
+
let candidate = hashTable[hash] - 1;
|
|
201
|
+
matchChain[inputOffset] = candidate;
|
|
177
202
|
hashTable[hash] = inputOffset + 1;
|
|
178
203
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 -
|
|
202
|
-
|
|
238
|
+
inputOffset - bestCandidate,
|
|
239
|
+
bestMatchEnd - inputOffset - LZ4_MIN_MATCH
|
|
203
240
|
);
|
|
204
|
-
inputOffset =
|
|
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
|
-
|
|
214
|
-
|
|
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,
|
|
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);
|