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 +112 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +112 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
3788
|
-
if (!config.
|
|
3789
|
-
const
|
|
3790
|
-
|
|
3791
|
-
const
|
|
3792
|
-
const
|
|
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: "
|
|
3797
|
-
|
|
3798
|
-
|
|
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
|
|
3909
|
-
unlockMethod = "
|
|
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
|
|
3915
|
-
unlockMethod = "
|
|
3969
|
+
({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
|
|
3970
|
+
unlockMethod = "password";
|
|
3916
3971
|
}
|
|
3917
3972
|
} else {
|
|
3918
|
-
({ wrappingKey, meta } = await
|
|
3919
|
-
unlockMethod = "
|
|
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
|
|
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
|
|
4440
|
+
return canOfferPasskey();
|
|
4356
4441
|
},
|
|
4357
4442
|
async signInWithPasskey() {
|
|
4358
4443
|
const candidate = await passkeyDeviceCandidate();
|
|
4359
|
-
if (
|
|
4360
|
-
|
|
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.
|