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.d.cts
CHANGED
|
@@ -506,11 +506,13 @@ interface DecaneConnect {
|
|
|
506
506
|
off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
|
|
507
507
|
}
|
|
508
508
|
interface WrappedShareMeta {
|
|
509
|
-
unlockMethod: 'passkey' | 'pin';
|
|
509
|
+
unlockMethod: 'passkey' | 'pin' | 'password';
|
|
510
510
|
credentialId?: string;
|
|
511
511
|
prfSalt?: string;
|
|
512
512
|
pinSalt?: string;
|
|
513
513
|
pinIterations?: number;
|
|
514
|
+
passwordSalt?: string;
|
|
515
|
+
passwordIterations?: number;
|
|
514
516
|
}
|
|
515
517
|
/**
|
|
516
518
|
* Who the user is, as the identity provider just described them.
|
package/dist/index.d.ts
CHANGED
|
@@ -506,11 +506,13 @@ interface DecaneConnect {
|
|
|
506
506
|
off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
|
|
507
507
|
}
|
|
508
508
|
interface WrappedShareMeta {
|
|
509
|
-
unlockMethod: 'passkey' | 'pin';
|
|
509
|
+
unlockMethod: 'passkey' | 'pin' | 'password';
|
|
510
510
|
credentialId?: string;
|
|
511
511
|
prfSalt?: string;
|
|
512
512
|
pinSalt?: string;
|
|
513
513
|
pinIterations?: number;
|
|
514
|
+
passwordSalt?: string;
|
|
515
|
+
passwordIterations?: number;
|
|
514
516
|
}
|
|
515
517
|
/**
|
|
516
518
|
* Who the user is, as the identity provider just described them.
|
package/dist/index.js
CHANGED
|
@@ -2886,9 +2886,6 @@ async function deriveKeyFromPin(pin, salt) {
|
|
|
2886
2886
|
["encrypt", "decrypt"]
|
|
2887
2887
|
);
|
|
2888
2888
|
}
|
|
2889
|
-
function validatePin(pin) {
|
|
2890
|
-
if (!/^\d{4,8}$/.test(pin)) throw new Error("PIN must be 4\u20138 digits");
|
|
2891
|
-
}
|
|
2892
2889
|
|
|
2893
2890
|
// src/social/crypto/passkey.ts
|
|
2894
2891
|
init_errors();
|
|
@@ -3702,21 +3699,38 @@ async function createDecaneConnect(config) {
|
|
|
3702
3699
|
}
|
|
3703
3700
|
}
|
|
3704
3701
|
events.on("session-expired", () => dropLiveSession());
|
|
3705
|
-
async function
|
|
3706
|
-
if (!config.
|
|
3707
|
-
const
|
|
3708
|
-
|
|
3709
|
-
const
|
|
3710
|
-
const
|
|
3702
|
+
async function passwordSetup() {
|
|
3703
|
+
if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
|
|
3704
|
+
const password = await config.promptUnlockPassword({ kind: "set" });
|
|
3705
|
+
validateUnlockPassword(password);
|
|
3706
|
+
const passwordSalt = generateRandomBytes(32);
|
|
3707
|
+
const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
|
|
3711
3708
|
return {
|
|
3712
|
-
wrappingKey,
|
|
3709
|
+
wrappingKey: shareKey,
|
|
3710
|
+
tokenKey,
|
|
3713
3711
|
meta: {
|
|
3714
|
-
unlockMethod: "
|
|
3715
|
-
|
|
3716
|
-
|
|
3712
|
+
unlockMethod: "password",
|
|
3713
|
+
passwordSalt: bufferToB64u(passwordSalt),
|
|
3714
|
+
passwordIterations: PBKDF2_ITERATIONS
|
|
3717
3715
|
}
|
|
3718
3716
|
};
|
|
3719
3717
|
}
|
|
3718
|
+
async function attachUnlockToken(userId, jwt, tokenKey) {
|
|
3719
|
+
try {
|
|
3720
|
+
const stored = await storage.getShare(userId);
|
|
3721
|
+
if (!stored) return;
|
|
3722
|
+
const { token } = await api.issueUnlockToken(jwt);
|
|
3723
|
+
const unlockTokenIv = generateRandomBytes(12);
|
|
3724
|
+
const wrappedUnlockToken = await aesGcmEncrypt(
|
|
3725
|
+
tokenKey,
|
|
3726
|
+
new TextEncoder().encode(token),
|
|
3727
|
+
unlockTokenIv
|
|
3728
|
+
);
|
|
3729
|
+
await storage.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
|
|
3730
|
+
} catch (e) {
|
|
3731
|
+
console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3720
3734
|
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
3721
3735
|
const stored = await storage.getShare(userId);
|
|
3722
3736
|
if (!stored) {
|
|
@@ -3773,13 +3787,21 @@ async function createDecaneConnect(config) {
|
|
|
3773
3787
|
const wrappingKey = await importAesKey(prfOutput);
|
|
3774
3788
|
const iv = generateRandomBytes(12);
|
|
3775
3789
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3790
|
+
const existing = await storage.getShare(userId);
|
|
3776
3791
|
await storage.putShare({
|
|
3792
|
+
...existing,
|
|
3777
3793
|
userId,
|
|
3778
3794
|
wrappedDeviceShare,
|
|
3779
3795
|
iv,
|
|
3780
3796
|
unlockMethod: "passkey",
|
|
3781
3797
|
credentialId,
|
|
3782
3798
|
prfSalt,
|
|
3799
|
+
pinSalt: void 0,
|
|
3800
|
+
pinIterations: void 0,
|
|
3801
|
+
passwordSalt: void 0,
|
|
3802
|
+
passwordIterations: void 0,
|
|
3803
|
+
wrappedUnlockToken: void 0,
|
|
3804
|
+
unlockTokenIv: void 0,
|
|
3783
3805
|
version: 1
|
|
3784
3806
|
});
|
|
3785
3807
|
try {
|
|
@@ -3805,11 +3827,44 @@ async function createDecaneConnect(config) {
|
|
|
3805
3827
|
);
|
|
3806
3828
|
}
|
|
3807
3829
|
}
|
|
3830
|
+
async function persistProvisionedShare(userId, jwt, deviceShare) {
|
|
3831
|
+
if (isPasskeySupported()) {
|
|
3832
|
+
try {
|
|
3833
|
+
await registerPasskeyForDevice(userId, jwt, deviceShare);
|
|
3834
|
+
return;
|
|
3835
|
+
} catch (e) {
|
|
3836
|
+
console.warn(
|
|
3837
|
+
"decane-connect-kit: passkey provisioning failed; falling back to an unlock password so the share is still stored.",
|
|
3838
|
+
e
|
|
3839
|
+
);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
if (!config.promptUnlockPassword) {
|
|
3843
|
+
console.error(
|
|
3844
|
+
"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."
|
|
3845
|
+
);
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
const { wrappingKey, tokenKey, meta } = await passwordSetup();
|
|
3849
|
+
const iv = generateRandomBytes(12);
|
|
3850
|
+
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3851
|
+
await storage.putShare({
|
|
3852
|
+
userId,
|
|
3853
|
+
wrappedDeviceShare,
|
|
3854
|
+
iv,
|
|
3855
|
+
unlockMethod: "password",
|
|
3856
|
+
passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
|
|
3857
|
+
passwordIterations: meta.passwordIterations,
|
|
3858
|
+
version: 1
|
|
3859
|
+
});
|
|
3860
|
+
await attachUnlockToken(userId, jwt, tokenKey);
|
|
3861
|
+
}
|
|
3808
3862
|
async function firstTimeSetup(userId, jwt) {
|
|
3809
3863
|
prewarmAttestation(teeUrl);
|
|
3810
3864
|
let unlockMethod;
|
|
3811
3865
|
let wrappingKey;
|
|
3812
3866
|
let meta = {};
|
|
3867
|
+
let unlockTokenKey = null;
|
|
3813
3868
|
if (isPasskeySupported()) {
|
|
3814
3869
|
const prfSalt = generateRandomBytes(32);
|
|
3815
3870
|
try {
|
|
@@ -3823,18 +3878,18 @@ async function createDecaneConnect(config) {
|
|
|
3823
3878
|
prfSalt: bufferToB64u(prfSalt)
|
|
3824
3879
|
};
|
|
3825
3880
|
} else {
|
|
3826
|
-
({ wrappingKey, meta } = await
|
|
3827
|
-
unlockMethod = "
|
|
3881
|
+
({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
|
|
3882
|
+
unlockMethod = "password";
|
|
3828
3883
|
}
|
|
3829
3884
|
} catch (e) {
|
|
3830
3885
|
if (e.name === "UserCancelledError")
|
|
3831
3886
|
throw new UserCancelledError();
|
|
3832
|
-
({ wrappingKey, meta } = await
|
|
3833
|
-
unlockMethod = "
|
|
3887
|
+
({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
|
|
3888
|
+
unlockMethod = "password";
|
|
3834
3889
|
}
|
|
3835
3890
|
} else {
|
|
3836
|
-
({ wrappingKey, meta } = await
|
|
3837
|
-
unlockMethod = "
|
|
3891
|
+
({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
|
|
3892
|
+
unlockMethod = "password";
|
|
3838
3893
|
}
|
|
3839
3894
|
const provisioningStartedAt = Date.now();
|
|
3840
3895
|
const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
|
|
@@ -3866,8 +3921,11 @@ async function createDecaneConnect(config) {
|
|
|
3866
3921
|
prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
|
|
3867
3922
|
pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
|
|
3868
3923
|
pinIterations: meta.pinIterations,
|
|
3924
|
+
passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
|
|
3925
|
+
passwordIterations: meta.passwordIterations,
|
|
3869
3926
|
version: 1
|
|
3870
3927
|
});
|
|
3928
|
+
if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
|
|
3871
3929
|
const serverIv = generateRandomBytes(12);
|
|
3872
3930
|
await withDeadline(api.putShare(jwt, {
|
|
3873
3931
|
ciphertext: shareSet.server,
|
|
@@ -3965,8 +4023,7 @@ async function createDecaneConnect(config) {
|
|
|
3965
4023
|
const tee = new TeeClient(teeUrl);
|
|
3966
4024
|
const provisioned = await tee.provisionDeviceShare(jwt);
|
|
3967
4025
|
if (provisioned) {
|
|
3968
|
-
await
|
|
3969
|
-
});
|
|
4026
|
+
await persistProvisionedShare(userId, jwt, provisioned);
|
|
3970
4027
|
let addrs;
|
|
3971
4028
|
try {
|
|
3972
4029
|
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
@@ -4096,6 +4153,12 @@ async function createDecaneConnect(config) {
|
|
|
4096
4153
|
const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4097
4154
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4098
4155
|
}
|
|
4156
|
+
async function canOfferPasskey() {
|
|
4157
|
+
if (!isPasskeySupported()) return false;
|
|
4158
|
+
if (await passkeyDeviceCandidate()) return true;
|
|
4159
|
+
if (!persisted?.userId) return false;
|
|
4160
|
+
return await storage.getShare(persisted.userId) == null;
|
|
4161
|
+
}
|
|
4099
4162
|
async function rememberProfile(userId, profile) {
|
|
4100
4163
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4101
4164
|
if (!Object.keys(incoming).length) return;
|
|
@@ -4111,6 +4174,28 @@ async function createDecaneConnect(config) {
|
|
|
4111
4174
|
if (!stored.credentialId || !stored.prfSalt) return null;
|
|
4112
4175
|
return stored;
|
|
4113
4176
|
}
|
|
4177
|
+
async function passkeyLoginWithoutShare(userId) {
|
|
4178
|
+
events.emit("wallet-unlocking");
|
|
4179
|
+
const options = await api.getWebAuthnLoginOptions(userId, rpId);
|
|
4180
|
+
if (!options) throw new NewDeviceError();
|
|
4181
|
+
const { response } = await authenticateWithPasskey({
|
|
4182
|
+
optionsJSON: options,
|
|
4183
|
+
prfSalt: generateRandomBytes(32)
|
|
4184
|
+
});
|
|
4185
|
+
const result = await api.postWebAuthnLogin(userId, response, window.location.origin);
|
|
4186
|
+
const addresses = await returningUserUnlock(userId, result.jwt);
|
|
4187
|
+
auth = { jwt: result.jwt, userId, addresses };
|
|
4188
|
+
persisted = { userId, addresses };
|
|
4189
|
+
savePersisted(PERSIST_KEY, persisted);
|
|
4190
|
+
events.emit("connected");
|
|
4191
|
+
persistLiveSession();
|
|
4192
|
+
const stored = await storage.getShare(userId);
|
|
4193
|
+
return {
|
|
4194
|
+
addresses,
|
|
4195
|
+
isNewUser: false,
|
|
4196
|
+
...stored?.profile ? { profile: stored.profile } : {}
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4114
4199
|
async function passkeyLogin(stored) {
|
|
4115
4200
|
const userId = stored.userId;
|
|
4116
4201
|
events.emit("wallet-unlocking");
|
|
@@ -4270,12 +4355,15 @@ async function createDecaneConnect(config) {
|
|
|
4270
4355
|
return handleAuthResult(result);
|
|
4271
4356
|
},
|
|
4272
4357
|
async canSignInWithPasskey() {
|
|
4273
|
-
return
|
|
4358
|
+
return canOfferPasskey();
|
|
4274
4359
|
},
|
|
4275
4360
|
async signInWithPasskey() {
|
|
4276
4361
|
const candidate = await passkeyDeviceCandidate();
|
|
4277
|
-
if (
|
|
4278
|
-
|
|
4362
|
+
if (candidate) return passkeyLogin(candidate);
|
|
4363
|
+
if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
|
|
4364
|
+
return passkeyLoginWithoutShare(persisted.userId);
|
|
4365
|
+
}
|
|
4366
|
+
throw new NewDeviceError();
|
|
4279
4367
|
},
|
|
4280
4368
|
/**
|
|
4281
4369
|
* Turn on "unlock with your password" for this device.
|