decane-connect-kit 2.12.1 → 2.14.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
@@ -2968,9 +2968,6 @@ async function deriveKeyFromPin(pin, salt) {
2968
2968
  ["encrypt", "decrypt"]
2969
2969
  );
2970
2970
  }
2971
- function validatePin(pin) {
2972
- if (!/^\d{4,8}$/.test(pin)) throw new Error("PIN must be 4\u20138 digits");
2973
- }
2974
2971
 
2975
2972
  // src/social/crypto/passkey.ts
2976
2973
  var import_browser2 = require("@simplewebauthn/browser");
@@ -3784,21 +3781,38 @@ async function createDecaneConnect(config) {
3784
3781
  }
3785
3782
  }
3786
3783
  events.on("session-expired", () => dropLiveSession());
3787
- async function pinSetup() {
3788
- if (!config.promptPin) throw new PromptPinNotConfiguredError();
3789
- const pin = await config.promptPin();
3790
- validatePin(pin);
3791
- const pinSalt = generateRandomBytes(32);
3792
- const wrappingKey = await deriveKeyFromPin(pin, pinSalt);
3784
+ async function passwordSetup() {
3785
+ if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
3786
+ const password = await config.promptUnlockPassword({ kind: "set" });
3787
+ validateUnlockPassword(password);
3788
+ const passwordSalt = generateRandomBytes(32);
3789
+ const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
3793
3790
  return {
3794
- wrappingKey,
3791
+ wrappingKey: shareKey,
3792
+ tokenKey,
3795
3793
  meta: {
3796
- unlockMethod: "pin",
3797
- pinSalt: bufferToB64u(pinSalt),
3798
- pinIterations: 6e5
3794
+ unlockMethod: "password",
3795
+ passwordSalt: bufferToB64u(passwordSalt),
3796
+ passwordIterations: PBKDF2_ITERATIONS
3799
3797
  }
3800
3798
  };
3801
3799
  }
3800
+ async function attachUnlockToken(userId, jwt, tokenKey) {
3801
+ try {
3802
+ const stored = await storage.getShare(userId);
3803
+ if (!stored) return;
3804
+ const { token } = await api.issueUnlockToken(jwt);
3805
+ const unlockTokenIv = generateRandomBytes(12);
3806
+ const wrappedUnlockToken = await aesGcmEncrypt(
3807
+ tokenKey,
3808
+ new TextEncoder().encode(token),
3809
+ unlockTokenIv
3810
+ );
3811
+ await storage.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
3812
+ } catch (e) {
3813
+ console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
3814
+ }
3815
+ }
3802
3816
  async function persistRotatedDeviceShare(userId, deviceShare) {
3803
3817
  const stored = await storage.getShare(userId);
3804
3818
  if (!stored) {
@@ -3855,13 +3869,21 @@ async function createDecaneConnect(config) {
3855
3869
  const wrappingKey = await importAesKey(prfOutput);
3856
3870
  const iv = generateRandomBytes(12);
3857
3871
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3872
+ const existing = await storage.getShare(userId);
3858
3873
  await storage.putShare({
3874
+ ...existing,
3859
3875
  userId,
3860
3876
  wrappedDeviceShare,
3861
3877
  iv,
3862
3878
  unlockMethod: "passkey",
3863
3879
  credentialId,
3864
3880
  prfSalt,
3881
+ pinSalt: void 0,
3882
+ pinIterations: void 0,
3883
+ passwordSalt: void 0,
3884
+ passwordIterations: void 0,
3885
+ wrappedUnlockToken: void 0,
3886
+ unlockTokenIv: void 0,
3865
3887
  version: 1
3866
3888
  });
3867
3889
  try {
@@ -3887,11 +3909,44 @@ async function createDecaneConnect(config) {
3887
3909
  );
3888
3910
  }
3889
3911
  }
3912
+ async function persistProvisionedShare(userId, jwt, deviceShare) {
3913
+ if (isPasskeySupported()) {
3914
+ try {
3915
+ await registerPasskeyForDevice(userId, jwt, deviceShare);
3916
+ return;
3917
+ } catch (e) {
3918
+ console.warn(
3919
+ "decane-connect-kit: passkey provisioning failed; falling back to an unlock password so the share is still stored.",
3920
+ e
3921
+ );
3922
+ }
3923
+ }
3924
+ if (!config.promptUnlockPassword) {
3925
+ console.error(
3926
+ "decane-connect-kit: this device could not store its share \u2014 no passkey and no promptUnlockPassword. The session works, but every visit will re-provision and no unlock will be offered. Configure promptUnlockPassword."
3927
+ );
3928
+ return;
3929
+ }
3930
+ const { wrappingKey, tokenKey, meta } = await passwordSetup();
3931
+ const iv = generateRandomBytes(12);
3932
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3933
+ await storage.putShare({
3934
+ userId,
3935
+ wrappedDeviceShare,
3936
+ iv,
3937
+ unlockMethod: "password",
3938
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3939
+ passwordIterations: meta.passwordIterations,
3940
+ version: 1
3941
+ });
3942
+ await attachUnlockToken(userId, jwt, tokenKey);
3943
+ }
3890
3944
  async function firstTimeSetup(userId, jwt) {
3891
3945
  prewarmAttestation(teeUrl);
3892
3946
  let unlockMethod;
3893
3947
  let wrappingKey;
3894
3948
  let meta = {};
3949
+ let unlockTokenKey = null;
3895
3950
  if (isPasskeySupported()) {
3896
3951
  const prfSalt = generateRandomBytes(32);
3897
3952
  try {
@@ -3905,18 +3960,18 @@ async function createDecaneConnect(config) {
3905
3960
  prfSalt: bufferToB64u(prfSalt)
3906
3961
  };
3907
3962
  } else {
3908
- ({ wrappingKey, meta } = await pinSetup());
3909
- unlockMethod = "pin";
3963
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3964
+ unlockMethod = "password";
3910
3965
  }
3911
3966
  } catch (e) {
3912
3967
  if (e.name === "UserCancelledError")
3913
3968
  throw new UserCancelledError();
3914
- ({ wrappingKey, meta } = await pinSetup());
3915
- unlockMethod = "pin";
3969
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3970
+ unlockMethod = "password";
3916
3971
  }
3917
3972
  } else {
3918
- ({ wrappingKey, meta } = await pinSetup());
3919
- unlockMethod = "pin";
3973
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3974
+ unlockMethod = "password";
3920
3975
  }
3921
3976
  const provisioningStartedAt = Date.now();
3922
3977
  const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
@@ -3948,8 +4003,11 @@ async function createDecaneConnect(config) {
3948
4003
  prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
3949
4004
  pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
3950
4005
  pinIterations: meta.pinIterations,
4006
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4007
+ passwordIterations: meta.passwordIterations,
3951
4008
  version: 1
3952
4009
  });
4010
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
3953
4011
  const serverIv = generateRandomBytes(12);
3954
4012
  await withDeadline(api.putShare(jwt, {
3955
4013
  ciphertext: shareSet.server,
@@ -4047,8 +4105,7 @@ async function createDecaneConnect(config) {
4047
4105
  const tee = new TeeClient(teeUrl);
4048
4106
  const provisioned = await tee.provisionDeviceShare(jwt);
4049
4107
  if (provisioned) {
4050
- await registerPasskeyAndBackup(userId, jwt, provisioned).catch(() => {
4051
- });
4108
+ await persistProvisionedShare(userId, jwt, provisioned);
4052
4109
  let addrs;
4053
4110
  try {
4054
4111
  addrs = await session.start(provisioned, jwt, recordedAddresses);
@@ -4178,6 +4235,12 @@ async function createDecaneConnect(config) {
4178
4235
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4179
4236
  return candidates.length === 1 ? candidates[0] : null;
4180
4237
  }
4238
+ async function canOfferPasskey() {
4239
+ if (!isPasskeySupported()) return false;
4240
+ if (await passkeyDeviceCandidate()) return true;
4241
+ if (!persisted?.userId) return false;
4242
+ return await storage.getShare(persisted.userId) == null;
4243
+ }
4181
4244
  async function rememberProfile(userId, profile) {
4182
4245
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4183
4246
  if (!Object.keys(incoming).length) return;
@@ -4193,6 +4256,28 @@ async function createDecaneConnect(config) {
4193
4256
  if (!stored.credentialId || !stored.prfSalt) return null;
4194
4257
  return stored;
4195
4258
  }
4259
+ async function passkeyLoginWithoutShare(userId) {
4260
+ events.emit("wallet-unlocking");
4261
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
4262
+ if (!options) throw new NewDeviceError();
4263
+ const { response } = await authenticateWithPasskey({
4264
+ optionsJSON: options,
4265
+ prfSalt: generateRandomBytes(32)
4266
+ });
4267
+ const result = await api.postWebAuthnLogin(userId, response, window.location.origin);
4268
+ const addresses = await returningUserUnlock(userId, result.jwt);
4269
+ auth = { jwt: result.jwt, userId, addresses };
4270
+ persisted = { userId, addresses };
4271
+ savePersisted(PERSIST_KEY, persisted);
4272
+ events.emit("connected");
4273
+ persistLiveSession();
4274
+ const stored = await storage.getShare(userId);
4275
+ return {
4276
+ addresses,
4277
+ isNewUser: false,
4278
+ ...stored?.profile ? { profile: stored.profile } : {}
4279
+ };
4280
+ }
4196
4281
  async function passkeyLogin(stored) {
4197
4282
  const userId = stored.userId;
4198
4283
  events.emit("wallet-unlocking");
@@ -4352,12 +4437,15 @@ async function createDecaneConnect(config) {
4352
4437
  return handleAuthResult(result);
4353
4438
  },
4354
4439
  async canSignInWithPasskey() {
4355
- return await passkeyDeviceCandidate() !== null;
4440
+ return canOfferPasskey();
4356
4441
  },
4357
4442
  async signInWithPasskey() {
4358
4443
  const candidate = await passkeyDeviceCandidate();
4359
- if (!candidate) throw new NewDeviceError();
4360
- return passkeyLogin(candidate);
4444
+ if (candidate) return passkeyLogin(candidate);
4445
+ if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
4446
+ return passkeyLoginWithoutShare(persisted.userId);
4447
+ }
4448
+ throw new NewDeviceError();
4361
4449
  },
4362
4450
  /**
4363
4451
  * Turn on "unlock with your password" for this device.