decane-connect-kit 2.16.0 → 2.18.0

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/dist/index.js CHANGED
@@ -2971,6 +2971,7 @@ function bufFromBytes(u) {
2971
2971
  }
2972
2972
  async function registerWebAuthnCredential(opts) {
2973
2973
  const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
2974
+ if (opts.allowDuplicate) delete baseOptions.excludeCredentials;
2974
2975
  const optionsJSON = {
2975
2976
  ...baseOptions,
2976
2977
  extensions: {
@@ -2979,11 +2980,7 @@ async function registerWebAuthnCredential(opts) {
2979
2980
  }
2980
2981
  };
2981
2982
  const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
2982
- try {
2983
- await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2984
- } catch (e) {
2985
- console.warn("decane-connect-kit: WebAuthn server-side registration failed; PRF-based unlock still works, but rotateShares()/exportPortableBackup() will not until this is retried.", e);
2986
- }
2983
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2987
2984
  return { credentialId, prfOutput };
2988
2985
  }
2989
2986
 
@@ -3773,16 +3770,24 @@ async function createDecaneConnect(config) {
3773
3770
  const pin = await config.promptPin();
3774
3771
  return deriveKeyFromPin(pin, stored.pinSalt);
3775
3772
  }
3776
- async function registerPasskeyForDevice(userId, jwt, deviceShare) {
3773
+ async function registerPasskeyForDevice(userId, jwt, deviceShare, opts = {}) {
3777
3774
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
3778
3775
  const prfSalt = generateRandomBytes(32);
3779
- const { credentialId, prfOutput } = await registerWebAuthnCredential({
3780
- api,
3781
- jwt,
3782
- rpId,
3783
- rpName,
3784
- prfSalt
3785
- });
3776
+ let credentialId;
3777
+ let prfOutput;
3778
+ try {
3779
+ ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3780
+ api,
3781
+ jwt,
3782
+ rpId,
3783
+ rpName,
3784
+ prfSalt,
3785
+ ...opts.allowDuplicate ? { allowDuplicate: true } : {}
3786
+ }));
3787
+ } catch (e) {
3788
+ if (e?.name !== "InvalidStateError" || opts.allowDuplicate) throw e;
3789
+ return adoptExistingPasskey(userId, jwt, deviceShare);
3790
+ }
3786
3791
  let key = prfOutput;
3787
3792
  if (!key) {
3788
3793
  try {
@@ -3795,7 +3800,10 @@ async function createDecaneConnect(config) {
3795
3800
  }
3796
3801
  }
3797
3802
  if (!key) throw new WebAuthnNotSupportedError();
3798
- const wrappingKey = await importAesKey(key);
3803
+ await storeShareUnderPasskey(userId, jwt, deviceShare, credentialId, key, prfSalt);
3804
+ }
3805
+ async function storeShareUnderPasskey(userId, jwt, deviceShare, credentialId, prfOutput, prfSalt) {
3806
+ const wrappingKey = await importAesKey(prfOutput);
3799
3807
  const iv = generateRandomBytes(12);
3800
3808
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3801
3809
  const existing = await storage.getShare(userId);
@@ -3827,6 +3835,29 @@ async function createDecaneConnect(config) {
3827
3835
  console.warn("decane-connect-kit: wrapped share backup upload failed; user can retry from settings.", e);
3828
3836
  }
3829
3837
  }
3838
+ async function adoptExistingPasskey(userId, jwt, deviceShare) {
3839
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
3840
+ if (!options) {
3841
+ console.warn(
3842
+ "decane-connect-kit: this authenticator holds a credential the server does not know; registering a fresh one."
3843
+ );
3844
+ return registerPasskeyForDevice(userId, jwt, deviceShare, { allowDuplicate: true });
3845
+ }
3846
+ const prfSalt = generateRandomBytes(32);
3847
+ const { response, prfOutput } = await authenticateWithPasskey({
3848
+ optionsJSON: options,
3849
+ prfSalt
3850
+ });
3851
+ if (!prfOutput) throw new WebAuthnNotSupportedError();
3852
+ await storeShareUnderPasskey(
3853
+ userId,
3854
+ jwt,
3855
+ deviceShare,
3856
+ b64uToBuffer(response.id),
3857
+ prfOutput,
3858
+ prfSalt
3859
+ );
3860
+ }
3830
3861
  async function registerPasskeyAndBackup(userId, jwt, deviceShare) {
3831
3862
  if (!isPasskeySupported()) return;
3832
3863
  try {
@@ -3856,19 +3887,26 @@ async function createDecaneConnect(config) {
3856
3887
  );
3857
3888
  return;
3858
3889
  }
3859
- const { wrappingKey, tokenKey, meta } = await passwordSetup();
3860
- const iv = generateRandomBytes(12);
3861
- const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3862
- await storage.putShare({
3863
- userId,
3864
- wrappedDeviceShare,
3865
- iv,
3866
- unlockMethod: "password",
3867
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3868
- passwordIterations: meta.passwordIterations,
3869
- version: 1
3870
- });
3871
- await attachUnlockToken(userId, jwt, tokenKey);
3890
+ try {
3891
+ const { wrappingKey, tokenKey, meta } = await passwordSetup();
3892
+ const iv = generateRandomBytes(12);
3893
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3894
+ await storage.putShare({
3895
+ userId,
3896
+ wrappedDeviceShare,
3897
+ iv,
3898
+ unlockMethod: "password",
3899
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3900
+ passwordIterations: meta.passwordIterations,
3901
+ version: 1
3902
+ });
3903
+ await attachUnlockToken(userId, jwt, tokenKey);
3904
+ } catch (e) {
3905
+ console.warn(
3906
+ "decane-connect-kit: no unlock password was set, so this device stored no share. The session works; the next visit will provision again.",
3907
+ e
3908
+ );
3909
+ }
3872
3910
  }
3873
3911
  async function firstTimeSetup(userId, jwt) {
3874
3912
  prewarmAttestation(teeUrl);