@ledgerhq/revault-sdk 0.27.16 → 0.28.1

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/README.md +4 -7
  2. package/lib/bake.cjs +1 -1
  3. package/lib/bake.d.cts +1 -1
  4. package/lib/bake.d.ts +1 -1
  5. package/lib/bake.js +1 -1
  6. package/lib/{crypto-BxlPOWD9.cjs → crypto-B4xS0N1u.cjs} +2 -2
  7. package/lib/{crypto-BxlPOWD9.cjs.map → crypto-B4xS0N1u.cjs.map} +1 -1
  8. package/lib/{crypto-BHYSptBe.js → crypto-BZw0pZ1Z.js} +2 -2
  9. package/lib/{crypto-BHYSptBe.js.map → crypto-BZw0pZ1Z.js.map} +1 -1
  10. package/lib/crypto.cjs +1 -1
  11. package/lib/crypto.js +1 -1
  12. package/lib/{device-DBzLykrI.js → device-Bo6m3O1I.js} +51 -3
  13. package/lib/device-Bo6m3O1I.js.map +1 -0
  14. package/lib/{device-B6QZOEDT.cjs → device-dbZH_1Pt.cjs} +63 -2
  15. package/lib/device-dbZH_1Pt.cjs.map +1 -0
  16. package/lib/device.cjs +3 -1
  17. package/lib/device.d.cts +2 -2
  18. package/lib/device.d.ts +2 -2
  19. package/lib/device.js +2 -2
  20. package/lib/engine.cjs +195 -41
  21. package/lib/engine.cjs.map +1 -1
  22. package/lib/engine.d.cts +2 -2
  23. package/lib/engine.d.ts +2 -2
  24. package/lib/engine.js +195 -41
  25. package/lib/engine.js.map +1 -1
  26. package/lib/{index-sWTlrbOl.d.cts → index-C5AAKtIf.d.cts} +49 -4
  27. package/lib/{index-B7SUQ6NW.d.cts → index-D_EED8aS.d.cts} +1 -1
  28. package/lib/{index-CuWiM97X.d.ts → index-Ds_t9ilJ.d.ts} +49 -4
  29. package/lib/{index-DLUJ9Etg.d.ts → index-aYbRTZvr.d.ts} +1 -1
  30. package/lib/onboarding.cjs +1 -1
  31. package/lib/onboarding.d.cts +1 -1
  32. package/lib/onboarding.d.ts +1 -1
  33. package/lib/onboarding.js +1 -1
  34. package/lib/{types-BN2MzD_5.d.ts → types-ALsH_YtN.d.ts} +1 -1
  35. package/lib/{types-BXAwSr1F.d.cts → types-DP-rmxvy.d.cts} +1 -1
  36. package/package.json +2 -2
  37. package/lib/device-B6QZOEDT.cjs.map +0 -1
  38. package/lib/device-DBzLykrI.js.map +0 -1
package/lib/engine.js CHANGED
@@ -1,6 +1,7 @@
1
- import { a as DisconnectedDevice, c as APPID_VAULT_ADMINISTRATOR, d as SCRIPT_IDS, f as STR_ROLES, i as withDevicePolling, l as PAGINATED_STATUS, n as isTransportStatusError, o as LockedDeviceError, s as createApiDevice, u as PATHS } from "./device-DBzLykrI.js";
1
+ import { c as LockedDeviceError, d as PAGINATED_STATUS, f as PATHS, i as isTransportStatusError, l as createApiDevice, m as STR_ROLES, o as withDevicePolling, p as SCRIPT_IDS, r as isPskMissingError, s as DisconnectedDevice, u as APPID_VAULT_ADMINISTRATOR } from "./device-Bo6m3O1I.js";
2
2
  import { i as fetchAllCursor, r as fetchAll, t as buildTrpcContext } from "./utils-CW0gdJME.js";
3
3
  import invariant from "invariant";
4
+ import { match } from "ts-pattern";
4
5
  import { from, lastValueFrom } from "rxjs";
5
6
  import { createTRPCClient, httpLink } from "@trpc/client";
6
7
  import superjson from "superjson";
@@ -62,6 +63,43 @@ const buildAttestation = (params) => {
62
63
  totalLengthBuffer.writeUInt16BE(data.length, 0);
63
64
  return Buffer.concat([totalLengthBuffer, data]);
64
65
  };
66
+ const buildPskRefreshPayload = (params) => {
67
+ const sigBuffer = Buffer.from(params.handshakeAttestation.signature, "base64");
68
+ const sigLengthBuffer = Buffer.alloc(1);
69
+ sigLengthBuffer.writeUIntBE(sigBuffer.length, 0, 1);
70
+ const certBuffer = Buffer.from(params.handshakeAttestation.certificate, "base64");
71
+ const certLengthBuffer = Buffer.alloc(1);
72
+ certLengthBuffer.writeUIntBE(certBuffer.length, 0, 1);
73
+ const pubBuffer = Buffer.from(params.handshakeAttestation.attestationPub, "base64");
74
+ const certChainData = Buffer.concat([
75
+ sigLengthBuffer,
76
+ sigBuffer,
77
+ pubBuffer,
78
+ certLengthBuffer,
79
+ certBuffer
80
+ ]);
81
+ const handshakeBuffer = Buffer.from(params.handshake, "base64");
82
+ invariant(handshakeBuffer.length <= 255, `PSK refresh handshake too long: ${handshakeBuffer.length} bytes (max 255)`);
83
+ const handshakeLengthBuffer = Buffer.alloc(1);
84
+ handshakeLengthBuffer.writeUInt8(handshakeBuffer.length, 0);
85
+ const encryptedPayloadBuffer = Buffer.from(params.encryptedPairingPayload, "base64");
86
+ invariant(encryptedPayloadBuffer.length <= 255, `PSK encrypted payload too long: ${encryptedPayloadBuffer.length} bytes (max 255)`);
87
+ const encryptedPayloadLengthBuffer = Buffer.alloc(1);
88
+ encryptedPayloadLengthBuffer.writeUInt8(encryptedPayloadBuffer.length, 0);
89
+ const certChainDataLengthBuff = Buffer.alloc(2);
90
+ certChainDataLengthBuff.writeUInt16BE(certChainData.length, 0);
91
+ const data = Buffer.concat([
92
+ handshakeLengthBuffer,
93
+ handshakeBuffer,
94
+ certChainDataLengthBuff,
95
+ certChainData,
96
+ encryptedPayloadLengthBuffer,
97
+ encryptedPayloadBuffer
98
+ ]);
99
+ const totalLengthBuffer = Buffer.alloc(2);
100
+ totalLengthBuffer.writeUInt16BE(data.length, 0);
101
+ return Buffer.concat([totalLengthBuffer, data]);
102
+ };
65
103
  /* v8 ignore next -- @preserve */
66
104
  const genericRetryOnError = (err) => {
67
105
  if (err instanceof LockedDeviceError) return true;
@@ -211,8 +249,10 @@ function createRealDevice(options, logger) {
211
249
  181
212
250
  ];
213
251
  const res = await run((transport) => transport.exchange(Buffer.from(apdu)));
214
- const hex = Buffer.from(res).toString("hex");
215
- return hex !== "6f45" && hex !== "6a87";
252
+ const swHex = res.subarray(res.length - 2).toString("hex");
253
+ if (swHex === "6f45" || swHex === "6a87") return "NO_PAIRING";
254
+ if (swHex === "6065") return "PSK_MISSING";
255
+ return "OK";
216
256
  },
217
257
  initPairingHandshake: async () => {
218
258
  const res = await run((transport) => transport.send(224, 70, 0, 0));
@@ -318,6 +358,21 @@ function createRealDevice(options, logger) {
318
358
  0
319
359
  ], final));
320
360
  },
361
+ signRefreshPskAuth: async () => {
362
+ logger.info(`[device] signRefreshPskAuth`);
363
+ const res = await run((transport) => transport.send(224, 85, 0, 0));
364
+ return res.slice(0, res.length - 2).toString("base64");
365
+ },
366
+ finalizePskRefresh: async (params) => {
367
+ logger.info(`[device] finalizePskRefresh`);
368
+ const payload = buildPskRefreshPayload(params);
369
+ await run((transport) => sendByChunk(transport, [
370
+ 224,
371
+ 83,
372
+ 0,
373
+ 0
374
+ ], payload));
375
+ },
321
376
  startKKPatternAsResponder: async (params) => {
322
377
  logger.info(`[device] startKKPatternAsResponder`);
323
378
  const data = buildAttestation(params);
@@ -343,6 +398,18 @@ function createRealDevice(options, logger) {
343
398
  SCRIPT_IDS.ACCOUNT_MANAGER_SESSION
344
399
  ], data));
345
400
  },
401
+ startPskSession: async (params) => {
402
+ logger.info(`[device] startPskSession`);
403
+ invariant(params.nonce.length === 64, `PSK nonce must be 32 bytes (64 hex chars), got ${params.nonce.length} chars`);
404
+ invariant(params.partitionId.length === 32, `partitionId must be 16 bytes (32 hex chars), got ${params.partitionId.length} chars`);
405
+ const data = Buffer.concat([Buffer.from(params.nonce, "hex"), Buffer.from(params.partitionId, "hex")]);
406
+ await run((transport) => sendByChunk(transport, [
407
+ 224,
408
+ 80,
409
+ 0,
410
+ 0
411
+ ], data));
412
+ },
346
413
  validateVaultOperation: async (params) => {
347
414
  logger.info(`[device] validateVaultOperation`);
348
415
  const path = getPathFromPathString(PATHS.VALIDATION);
@@ -423,6 +490,37 @@ const createClient = (options) => {
423
490
  };
424
491
  //#endregion
425
492
  //#region src/engine/index.ts
493
+ const PSK_NONCE_BYTE_LEN = 32;
494
+ const openSecureChannel = (device, challenge) => {
495
+ if (challenge.handshake.length === PSK_NONCE_BYTE_LEN * 2) return device.startPskSession({
496
+ nonce: challenge.handshake,
497
+ partitionId: challenge.partitionId
498
+ });
499
+ return device.startKPatternAsResponder({
500
+ handshake: challenge.handshake,
501
+ handshakeAttestation: challenge.handshakeAttestation,
502
+ partitionId: challenge.partitionId
503
+ });
504
+ };
505
+ const pairDevice = async ({ device, getPairing }) => {
506
+ const pairing = await getPairing(await device.getConfidentialityKey());
507
+ await device.setPairing({ pairing });
508
+ };
509
+ const refreshPsk = async ({ device, getRefreshPskData }) => {
510
+ const eph = await device.initPairingHandshake();
511
+ const [deviceAttestation, validationKeySignature] = await Promise.all([device.getAttestation(), device.signRefreshPskAuth({ ephemeralKey: eph.pubKey })]);
512
+ const refresh = await getRefreshPskData({
513
+ psdEphemeralKey: Buffer.from(eph.pubKey, "hex").toString("base64"),
514
+ psdEphemeralKeyAttestation: {
515
+ codeHash: deviceAttestation.attestation.codeHash,
516
+ attestationPub: deviceAttestation.attestation.attestationPub,
517
+ certificate: deviceAttestation.attestation.certificate,
518
+ signature: eph.signature
519
+ },
520
+ validationKeySignature
521
+ });
522
+ await device.finalizePskRefresh(refresh);
523
+ };
426
524
  async function genericRegister({ device, name, role, getChallenge, submitChallenge, onScreenDisplayed }) {
427
525
  const [attestation, u2fPubKey, confidentialityKey] = await Promise.all([
428
526
  device.getAttestation(),
@@ -562,18 +660,55 @@ function createEngine(options, logger) {
562
660
  });
563
661
  if (engine.device && engine.__onboardingId) decorate((engine) => {
564
662
  const { device, client, __onboardingId } = engine;
663
+ const isPskEnabled = async () => {
664
+ try {
665
+ return await client.onboarding.getPskFeatureFlag.query({ onboardingId: __onboardingId }) === true;
666
+ } catch {
667
+ return false;
668
+ }
669
+ };
565
670
  async function enforcePairing() {
566
- if (await device.hasPairing()) return;
567
- const [{ pubKey }, confidentialityKey] = await Promise.all([device.getU2FPubKey(), device.getConfidentialityKey()]);
568
- const pairing = await client.onboarding.pairDevicePartition.mutate({
569
- pubKey,
570
- onboardingId: __onboardingId,
571
- data: {
572
- confidentialityKeyCurve25519: confidentialityKey.confidentialityKeyCurve25519,
573
- confidentialityKeySignature: confidentialityKey.confidentialityKeySignature
574
- }
575
- });
576
- await device.setPairing({ pairing });
671
+ const status = await device.hasPairing();
672
+ if (status === "OK") return;
673
+ const pskEnabled = await isPskEnabled();
674
+ let pubKeyPromise;
675
+ const getPubKey = async () => {
676
+ pubKeyPromise ??= device.getU2FPubKey().then(({ pubKey }) => pubKey);
677
+ return pubKeyPromise;
678
+ };
679
+ const pairCurrentDevice = async () => {
680
+ const pubKey = await getPubKey();
681
+ await pairDevice({
682
+ device,
683
+ getPairing: (confidentialityKey) => client.onboarding.pairDevicePartition.mutate({
684
+ pubKey,
685
+ onboardingId: __onboardingId,
686
+ data: {
687
+ confidentialityKeyCurve25519: confidentialityKey.confidentialityKeyCurve25519,
688
+ confidentialityKeySignature: confidentialityKey.confidentialityKeySignature
689
+ }
690
+ })
691
+ });
692
+ };
693
+ const refreshCurrentPsk = async () => {
694
+ const pubKey = await getPubKey();
695
+ await refreshPsk({
696
+ device,
697
+ getRefreshPskData: (data) => client.onboarding.refreshPsk.mutate({
698
+ pubKey,
699
+ onboardingId: __onboardingId,
700
+ data
701
+ })
702
+ });
703
+ };
704
+ await match([status, pskEnabled]).with(["NO_PAIRING", false], async () => {
705
+ await pairCurrentDevice();
706
+ }).with(["NO_PAIRING", true], async () => {
707
+ await pairCurrentDevice();
708
+ await refreshCurrentPsk();
709
+ }).with(["PSK_MISSING", false], async () => {}).with(["PSK_MISSING", true], async () => {
710
+ await refreshCurrentPsk();
711
+ }).exhaustive();
577
712
  }
578
713
  return {
579
714
  claimDevice: async (params) => {
@@ -635,11 +770,7 @@ function createEngine(options, logger) {
635
770
  reviewSharedOwner: async (challenge, params) => {
636
771
  await enforcePairing();
637
772
  const { pubKey } = await device.getU2FPubKey();
638
- await device.startKPatternAsResponder({
639
- handshake: challenge.handshake,
640
- handshakeAttestation: challenge.handshakeAttestation,
641
- partitionId: challenge.partitionId
642
- });
773
+ await openSecureChannel(device, challenge);
643
774
  await params?.onScreenDisplayed?.();
644
775
  const signature = await device.validateVaultOperation({ actions: challenge.wActions });
645
776
  await client.onboarding.submitReviewByMasterAdminSignature.mutate({
@@ -651,11 +782,7 @@ function createEngine(options, logger) {
651
782
  reviewExternalSigners: async (challenge, params) => {
652
783
  await enforcePairing();
653
784
  const { pubKey } = await device.getU2FPubKey();
654
- await device.startKPatternAsResponder({
655
- handshake: challenge.handshake,
656
- handshakeAttestation: challenge.handshakeAttestation,
657
- partitionId: challenge.partitionId
658
- });
785
+ await openSecureChannel(device, challenge);
659
786
  await params?.onScreenDisplayed?.();
660
787
  const signature = await device.validateVaultOperation({ actions: challenge.wActions });
661
788
  await client.onboarding.submitExternalSignersRegistration.mutate({
@@ -667,11 +794,7 @@ function createEngine(options, logger) {
667
794
  collectSeedFragment: async (challenge, params) => {
668
795
  await enforcePairing();
669
796
  const [{ pubKey }, { attestation }] = await Promise.all([device.getU2FPubKey(), device.getAttestation()]);
670
- await device.startKPatternAsResponder({
671
- handshake: challenge.handshake,
672
- handshakeAttestation: challenge.handshakeAttestation,
673
- partitionId: challenge.partitionId
674
- });
797
+ await openSecureChannel(device, challenge);
675
798
  await params?.onScreenDisplayed?.();
676
799
  const challengeResponse = await device.validateVaultOperation({ actions: challenge.wActions });
677
800
  const kkPattern = await device.startKKPatternAsResponder({
@@ -767,22 +890,53 @@ function createEngine(options, logger) {
767
890
  });
768
891
  if (engine.device && engine.__workspaceName && engine.__psdToken) decorate((engine) => {
769
892
  const { client, device } = engine;
893
+ const isPskEnabled = async () => {
894
+ try {
895
+ return await client.debug.getPskFeatureFlag.query() === true;
896
+ } catch {
897
+ return false;
898
+ }
899
+ };
770
900
  async function enforcePairing() {
771
- if (await device.hasPairing()) return;
772
- const confidentialityKey = await device.getConfidentialityKey();
773
- const pairing = await client.debug.pairDevicePartition.mutate({
774
- confidentialityKeyCurve25519: confidentialityKey.confidentialityKeyCurve25519,
775
- confidentialityKeySignature: confidentialityKey.confidentialityKeySignature
776
- });
777
- await device.setPairing({ pairing });
901
+ const status = await device.hasPairing();
902
+ if (status === "OK") return;
903
+ const pskEnabled = await isPskEnabled();
904
+ const pairCurrentDevice = async () => {
905
+ await pairDevice({
906
+ device,
907
+ getPairing: (confidentialityKey) => client.debug.pairDevicePartition.mutate({
908
+ confidentialityKeyCurve25519: confidentialityKey.confidentialityKeyCurve25519,
909
+ confidentialityKeySignature: confidentialityKey.confidentialityKeySignature
910
+ })
911
+ });
912
+ };
913
+ const refreshCurrentPsk = async () => {
914
+ await refreshPsk({
915
+ device,
916
+ getRefreshPskData: (data) => client.debug.refreshPsk.mutate(data)
917
+ });
918
+ };
919
+ await match([status, pskEnabled]).with(["NO_PAIRING", false], async () => {
920
+ await pairCurrentDevice();
921
+ }).with(["NO_PAIRING", true], async () => {
922
+ await pairCurrentDevice();
923
+ await refreshCurrentPsk();
924
+ }).with(["PSK_MISSING", false], async () => {}).with(["PSK_MISSING", true], async () => {
925
+ await refreshCurrentPsk();
926
+ }).exhaustive();
778
927
  }
779
928
  const approveChallenge = async (challenge, params) => {
780
929
  await enforcePairing();
781
- await device.startKPatternAsResponder({
782
- handshake: challenge.handshake,
783
- handshakeAttestation: challenge.handshakeAttestation,
784
- partitionId: challenge.partitionId
785
- });
930
+ try {
931
+ await openSecureChannel(device, challenge);
932
+ } catch (err) {
933
+ if (!isPskMissingError(err)) throw err;
934
+ if (!await isPskEnabled()) logger.warning("Discarding SW_PSK_MISSING: PSK secure channel is disabled");
935
+ else {
936
+ await enforcePairing();
937
+ await openSecureChannel(device, challenge);
938
+ }
939
+ }
786
940
  await params?.onScreenDisplayed?.();
787
941
  return await device.validateVaultOperation({ actions: challenge.wActions });
788
942
  };