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.cjs CHANGED
@@ -3053,6 +3053,7 @@ function bufFromBytes(u) {
3053
3053
  }
3054
3054
  async function registerWebAuthnCredential(opts) {
3055
3055
  const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
3056
+ if (opts.allowDuplicate) delete baseOptions.excludeCredentials;
3056
3057
  const optionsJSON = {
3057
3058
  ...baseOptions,
3058
3059
  extensions: {
@@ -3061,11 +3062,7 @@ async function registerWebAuthnCredential(opts) {
3061
3062
  }
3062
3063
  };
3063
3064
  const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
3064
- try {
3065
- await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
3066
- } catch (e) {
3067
- 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);
3068
- }
3065
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
3069
3066
  return { credentialId, prfOutput };
3070
3067
  }
3071
3068
 
@@ -3855,16 +3852,24 @@ async function createDecaneConnect(config) {
3855
3852
  const pin = await config.promptPin();
3856
3853
  return deriveKeyFromPin(pin, stored.pinSalt);
3857
3854
  }
3858
- async function registerPasskeyForDevice(userId, jwt, deviceShare) {
3855
+ async function registerPasskeyForDevice(userId, jwt, deviceShare, opts = {}) {
3859
3856
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
3860
3857
  const prfSalt = generateRandomBytes(32);
3861
- const { credentialId, prfOutput } = await registerWebAuthnCredential({
3862
- api,
3863
- jwt,
3864
- rpId,
3865
- rpName,
3866
- prfSalt
3867
- });
3858
+ let credentialId;
3859
+ let prfOutput;
3860
+ try {
3861
+ ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3862
+ api,
3863
+ jwt,
3864
+ rpId,
3865
+ rpName,
3866
+ prfSalt,
3867
+ ...opts.allowDuplicate ? { allowDuplicate: true } : {}
3868
+ }));
3869
+ } catch (e) {
3870
+ if (e?.name !== "InvalidStateError" || opts.allowDuplicate) throw e;
3871
+ return adoptExistingPasskey(userId, jwt, deviceShare);
3872
+ }
3868
3873
  let key = prfOutput;
3869
3874
  if (!key) {
3870
3875
  try {
@@ -3877,7 +3882,10 @@ async function createDecaneConnect(config) {
3877
3882
  }
3878
3883
  }
3879
3884
  if (!key) throw new WebAuthnNotSupportedError();
3880
- const wrappingKey = await importAesKey(key);
3885
+ await storeShareUnderPasskey(userId, jwt, deviceShare, credentialId, key, prfSalt);
3886
+ }
3887
+ async function storeShareUnderPasskey(userId, jwt, deviceShare, credentialId, prfOutput, prfSalt) {
3888
+ const wrappingKey = await importAesKey(prfOutput);
3881
3889
  const iv = generateRandomBytes(12);
3882
3890
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3883
3891
  const existing = await storage.getShare(userId);
@@ -3909,6 +3917,29 @@ async function createDecaneConnect(config) {
3909
3917
  console.warn("decane-connect-kit: wrapped share backup upload failed; user can retry from settings.", e);
3910
3918
  }
3911
3919
  }
3920
+ async function adoptExistingPasskey(userId, jwt, deviceShare) {
3921
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
3922
+ if (!options) {
3923
+ console.warn(
3924
+ "decane-connect-kit: this authenticator holds a credential the server does not know; registering a fresh one."
3925
+ );
3926
+ return registerPasskeyForDevice(userId, jwt, deviceShare, { allowDuplicate: true });
3927
+ }
3928
+ const prfSalt = generateRandomBytes(32);
3929
+ const { response, prfOutput } = await authenticateWithPasskey({
3930
+ optionsJSON: options,
3931
+ prfSalt
3932
+ });
3933
+ if (!prfOutput) throw new WebAuthnNotSupportedError();
3934
+ await storeShareUnderPasskey(
3935
+ userId,
3936
+ jwt,
3937
+ deviceShare,
3938
+ b64uToBuffer(response.id),
3939
+ prfOutput,
3940
+ prfSalt
3941
+ );
3942
+ }
3912
3943
  async function registerPasskeyAndBackup(userId, jwt, deviceShare) {
3913
3944
  if (!isPasskeySupported()) return;
3914
3945
  try {
@@ -3938,19 +3969,26 @@ async function createDecaneConnect(config) {
3938
3969
  );
3939
3970
  return;
3940
3971
  }
3941
- const { wrappingKey, tokenKey, meta } = await passwordSetup();
3942
- const iv = generateRandomBytes(12);
3943
- const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3944
- await storage.putShare({
3945
- userId,
3946
- wrappedDeviceShare,
3947
- iv,
3948
- unlockMethod: "password",
3949
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3950
- passwordIterations: meta.passwordIterations,
3951
- version: 1
3952
- });
3953
- await attachUnlockToken(userId, jwt, tokenKey);
3972
+ try {
3973
+ const { wrappingKey, tokenKey, meta } = await passwordSetup();
3974
+ const iv = generateRandomBytes(12);
3975
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3976
+ await storage.putShare({
3977
+ userId,
3978
+ wrappedDeviceShare,
3979
+ iv,
3980
+ unlockMethod: "password",
3981
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3982
+ passwordIterations: meta.passwordIterations,
3983
+ version: 1
3984
+ });
3985
+ await attachUnlockToken(userId, jwt, tokenKey);
3986
+ } catch (e) {
3987
+ console.warn(
3988
+ "decane-connect-kit: no unlock password was set, so this device stored no share. The session works; the next visit will provision again.",
3989
+ e
3990
+ );
3991
+ }
3954
3992
  }
3955
3993
  async function firstTimeSetup(userId, jwt) {
3956
3994
  prewarmAttestation(teeUrl);