@abraca/dabra 1.1.2 → 1.3.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/abracadabra-provider.cjs +313 -143
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +313 -143
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +81 -43
- package/package.json +1 -1
- package/src/BackgroundSyncManager.ts +2 -2
- package/src/CryptoIdentityKeystore.ts +335 -203
- package/src/IdentityDoc.ts +2 -2
- package/src/webrtc/AbracadabraWebRTC.ts +61 -29
- package/src/webrtc/types.ts +1 -1
|
@@ -7101,15 +7101,60 @@ const ristretto255_oprf = createORPF({
|
|
|
7101
7101
|
/**
|
|
7102
7102
|
* CryptoIdentityKeystore
|
|
7103
7103
|
*
|
|
7104
|
-
* Per-
|
|
7105
|
-
*
|
|
7106
|
-
*
|
|
7107
|
-
*
|
|
7108
|
-
*
|
|
7109
|
-
*
|
|
7110
|
-
*
|
|
7111
|
-
*
|
|
7112
|
-
|
|
7104
|
+
* Per-user Ed25519 keypair derived deterministically from a synced WebAuthn
|
|
7105
|
+
* passkey's PRF extension output. The same passkey on any device produces the
|
|
7106
|
+
* same identity — no private key storage needed.
|
|
7107
|
+
*
|
|
7108
|
+
* Derivation chain:
|
|
7109
|
+
* Synced Passkey → PRF(constant salt) → HKDF-SHA256 → Ed25519 seed → keypair
|
|
7110
|
+
*
|
|
7111
|
+
* IndexedDB is used only as a lightweight cache for the public key and
|
|
7112
|
+
* credential ID. Loss of IndexedDB is non-catastrophic — a passkey assertion
|
|
7113
|
+
* re-derives everything.
|
|
7114
|
+
*
|
|
7115
|
+
* Dependencies: @noble/ed25519, @noble/hashes (for HKDF), @noble/curves (for X25519)
|
|
7116
|
+
*/
|
|
7117
|
+
/**
|
|
7118
|
+
* Fixed PRF eval salt. Must be constant across all devices so the same synced
|
|
7119
|
+
* passkey produces the same PRF output everywhere.
|
|
7120
|
+
*
|
|
7121
|
+
* Value: SHA-256("abracadabra-prf-salt-v1"), precomputed.
|
|
7122
|
+
*/
|
|
7123
|
+
const PRF_SALT = new Uint8Array([
|
|
7124
|
+
183,
|
|
7125
|
+
121,
|
|
7126
|
+
65,
|
|
7127
|
+
162,
|
|
7128
|
+
231,
|
|
7129
|
+
133,
|
|
7130
|
+
29,
|
|
7131
|
+
250,
|
|
7132
|
+
94,
|
|
7133
|
+
193,
|
|
7134
|
+
171,
|
|
7135
|
+
182,
|
|
7136
|
+
80,
|
|
7137
|
+
26,
|
|
7138
|
+
92,
|
|
7139
|
+
133,
|
|
7140
|
+
117,
|
|
7141
|
+
253,
|
|
7142
|
+
62,
|
|
7143
|
+
63,
|
|
7144
|
+
146,
|
|
7145
|
+
201,
|
|
7146
|
+
225,
|
|
7147
|
+
167,
|
|
7148
|
+
78,
|
|
7149
|
+
186,
|
|
7150
|
+
113,
|
|
7151
|
+
252,
|
|
7152
|
+
242,
|
|
7153
|
+
177,
|
|
7154
|
+
18,
|
|
7155
|
+
207
|
|
7156
|
+
]);
|
|
7157
|
+
const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
|
|
7113
7158
|
function toBase64url(bytes) {
|
|
7114
7159
|
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
7115
7160
|
}
|
|
@@ -7121,61 +7166,88 @@ function fromBase64url(b64) {
|
|
|
7121
7166
|
}
|
|
7122
7167
|
const DB_NAME = "abracadabra:identity";
|
|
7123
7168
|
const STORE_NAME = "identity";
|
|
7124
|
-
const RECORD_KEY = "current";
|
|
7125
|
-
const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
|
|
7126
7169
|
function openDb$4() {
|
|
7127
7170
|
return new Promise((resolve, reject) => {
|
|
7128
|
-
const req = indexedDB.open(DB_NAME,
|
|
7171
|
+
const req = indexedDB.open(DB_NAME, 2);
|
|
7129
7172
|
req.onupgradeneeded = () => {
|
|
7130
|
-
req.result
|
|
7173
|
+
const db = req.result;
|
|
7174
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
|
|
7131
7175
|
};
|
|
7132
7176
|
req.onsuccess = () => resolve(req.result);
|
|
7133
7177
|
req.onerror = () => reject(req.error);
|
|
7134
7178
|
});
|
|
7135
7179
|
}
|
|
7136
|
-
async function
|
|
7180
|
+
async function dbGetAll(db) {
|
|
7181
|
+
return new Promise((resolve, reject) => {
|
|
7182
|
+
const store = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME);
|
|
7183
|
+
const results = [];
|
|
7184
|
+
const req = store.openCursor();
|
|
7185
|
+
req.onsuccess = () => {
|
|
7186
|
+
const cursor = req.result;
|
|
7187
|
+
if (cursor) {
|
|
7188
|
+
results.push({
|
|
7189
|
+
key: cursor.key,
|
|
7190
|
+
value: cursor.value
|
|
7191
|
+
});
|
|
7192
|
+
cursor.continue();
|
|
7193
|
+
} else resolve(results);
|
|
7194
|
+
};
|
|
7195
|
+
req.onerror = () => reject(req.error);
|
|
7196
|
+
});
|
|
7197
|
+
}
|
|
7198
|
+
async function dbGet(db, key) {
|
|
7137
7199
|
return new Promise((resolve, reject) => {
|
|
7138
|
-
const req = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(
|
|
7200
|
+
const req = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(key);
|
|
7139
7201
|
req.onsuccess = () => resolve(req.result);
|
|
7140
7202
|
req.onerror = () => reject(req.error);
|
|
7141
7203
|
});
|
|
7142
7204
|
}
|
|
7143
|
-
async function dbPut(db, value) {
|
|
7205
|
+
async function dbPut(db, key, value) {
|
|
7144
7206
|
return new Promise((resolve, reject) => {
|
|
7145
|
-
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(value,
|
|
7207
|
+
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(value, key);
|
|
7146
7208
|
req.onsuccess = () => resolve();
|
|
7147
7209
|
req.onerror = () => reject(req.error);
|
|
7148
7210
|
});
|
|
7149
7211
|
}
|
|
7150
|
-
async function
|
|
7212
|
+
async function dbClear(db) {
|
|
7151
7213
|
return new Promise((resolve, reject) => {
|
|
7152
|
-
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).
|
|
7214
|
+
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).clear();
|
|
7153
7215
|
req.onsuccess = () => resolve();
|
|
7154
7216
|
req.onerror = () => reject(req.error);
|
|
7155
7217
|
});
|
|
7156
7218
|
}
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
return
|
|
7219
|
+
/** Derive a 32-byte Ed25519 seed from raw PRF output. */
|
|
7220
|
+
function deriveEd25519Seed(prfOutput) {
|
|
7221
|
+
return hkdf(sha256, new Uint8Array(prfOutput), PRF_SALT, HKDF_INFO$2, 32);
|
|
7222
|
+
}
|
|
7223
|
+
function extractPrfOutput(credential) {
|
|
7224
|
+
const prfOutput = credential.getClientExtensionResults()?.prf?.results?.first;
|
|
7225
|
+
if (!prfOutput) throw new Error("WebAuthn PRF extension not available on this authenticator. A PRF-capable platform authenticator (e.g. iCloud Keychain) is required.");
|
|
7226
|
+
return prfOutput;
|
|
7160
7227
|
}
|
|
7161
7228
|
var CryptoIdentityKeystore = class {
|
|
7162
7229
|
/**
|
|
7163
|
-
*
|
|
7164
|
-
*
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7230
|
+
* Check whether the platform supports WebAuthn with PRF extension.
|
|
7231
|
+
* Call this before offering the "Secure with Passkey" option.
|
|
7232
|
+
*/
|
|
7233
|
+
static async isPrfAvailable() {
|
|
7234
|
+
if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
|
|
7235
|
+
try {
|
|
7236
|
+
if (!await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) return false;
|
|
7237
|
+
return true;
|
|
7238
|
+
} catch {
|
|
7239
|
+
return false;
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
/**
|
|
7243
|
+
* Create a synced discoverable passkey and derive the Ed25519 identity from
|
|
7244
|
+
* its PRF output. The passkey is stored in the platform credential manager
|
|
7245
|
+
* (e.g. iCloud Keychain) and syncs across devices automatically.
|
|
7170
7246
|
*
|
|
7171
|
-
*
|
|
7172
|
-
*
|
|
7173
|
-
* @param rpName - Human-readable relying party name.
|
|
7247
|
+
* Returns the base64url-encoded public key, X25519 public key, and credential ID.
|
|
7248
|
+
* The caller must register the public key with the server.
|
|
7174
7249
|
*/
|
|
7175
7250
|
async register(username, rpId, rpName) {
|
|
7176
|
-
const privateKey = _noble_ed25519.utils.randomPrivateKey();
|
|
7177
|
-
const publicKey = await _noble_ed25519.getPublicKeyAsync(privateKey);
|
|
7178
|
-
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
7179
7251
|
const credential = await navigator.credentials.create({ publicKey: {
|
|
7180
7252
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
7181
7253
|
rp: {
|
|
@@ -7194,151 +7266,222 @@ var CryptoIdentityKeystore = class {
|
|
|
7194
7266
|
alg: -257,
|
|
7195
7267
|
type: "public-key"
|
|
7196
7268
|
}],
|
|
7197
|
-
authenticatorSelection: {
|
|
7198
|
-
|
|
7269
|
+
authenticatorSelection: {
|
|
7270
|
+
residentKey: "required",
|
|
7271
|
+
requireResidentKey: true,
|
|
7272
|
+
userVerification: "required"
|
|
7273
|
+
},
|
|
7274
|
+
extensions: { prf: { eval: { first: PRF_SALT.buffer } } }
|
|
7199
7275
|
} });
|
|
7200
|
-
if (!credential) throw new Error("WebAuthn credential creation
|
|
7201
|
-
const
|
|
7202
|
-
|
|
7203
|
-
const
|
|
7204
|
-
const
|
|
7205
|
-
const
|
|
7206
|
-
name: "AES-GCM",
|
|
7207
|
-
iv
|
|
7208
|
-
}, aesKey, privateKey);
|
|
7276
|
+
if (!credential) throw new Error("WebAuthn credential creation cancelled");
|
|
7277
|
+
const seed = deriveEd25519Seed(extractPrfOutput(credential));
|
|
7278
|
+
const publicKey = await _noble_ed25519.getPublicKeyAsync(seed);
|
|
7279
|
+
const publicKeyB64 = toBase64url(publicKey);
|
|
7280
|
+
const x25519PubB64 = toBase64url(ed25519.utils.toMontgomery(publicKey));
|
|
7281
|
+
const credentialIdB64 = toBase64url(new Uint8Array(credential.rawId));
|
|
7209
7282
|
const db = await openDb$4();
|
|
7210
|
-
await dbPut(db, {
|
|
7283
|
+
await dbPut(db, credentialIdB64, {
|
|
7211
7284
|
username,
|
|
7212
|
-
publicKey:
|
|
7213
|
-
encryptedPrivateKey,
|
|
7214
|
-
iv,
|
|
7215
|
-
salt,
|
|
7285
|
+
publicKey: publicKeyB64,
|
|
7216
7286
|
credentialId: credential.rawId
|
|
7217
7287
|
});
|
|
7218
7288
|
db.close();
|
|
7219
|
-
|
|
7289
|
+
seed.fill(0);
|
|
7220
7290
|
return {
|
|
7221
|
-
publicKey:
|
|
7222
|
-
x25519PublicKey:
|
|
7291
|
+
publicKey: publicKeyB64,
|
|
7292
|
+
x25519PublicKey: x25519PubB64,
|
|
7293
|
+
credentialId: credentialIdB64
|
|
7223
7294
|
};
|
|
7224
7295
|
}
|
|
7225
7296
|
/**
|
|
7226
|
-
* Sign a base64url-encoded challenge using the
|
|
7227
|
-
*
|
|
7228
|
-
* This triggers a WebAuthn assertion (biometric / PIN prompt) to unlock the
|
|
7229
|
-
* private key via PRF → HKDF → AES-GCM decryption. The private key is
|
|
7230
|
-
* wiped from memory after signing.
|
|
7297
|
+
* Sign a base64url-encoded challenge using the Ed25519 key derived from
|
|
7298
|
+
* a passkey assertion. Triggers a WebAuthn prompt (biometric / PIN).
|
|
7231
7299
|
*
|
|
7232
7300
|
* @param challengeB64 - base64url-encoded challenge bytes from the server.
|
|
7301
|
+
* @param credentialIdHint - optional credential ID to select a specific passkey.
|
|
7233
7302
|
* @returns base64url-encoded Ed25519 signature (64 bytes).
|
|
7234
7303
|
*/
|
|
7235
|
-
async sign(challengeB64) {
|
|
7304
|
+
async sign(challengeB64, credentialIdHint) {
|
|
7305
|
+
const { seed } = await this._assertAndDerive(credentialIdHint);
|
|
7306
|
+
try {
|
|
7307
|
+
const challengeBytes = fromBase64url(challengeB64);
|
|
7308
|
+
return toBase64url(await _noble_ed25519.signAsync(challengeBytes, seed));
|
|
7309
|
+
} finally {
|
|
7310
|
+
seed.fill(0);
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
/**
|
|
7314
|
+
* Returns the cached base64url public key, or null if no identity is cached.
|
|
7315
|
+
*
|
|
7316
|
+
* Does NOT trigger a WebAuthn prompt. If the cache is empty (e.g. IndexedDB
|
|
7317
|
+
* cleared), returns null — the identity can be recovered via sign() or
|
|
7318
|
+
* a fresh register() with the same synced passkey.
|
|
7319
|
+
*/
|
|
7320
|
+
async getPublicKey(credentialIdHint) {
|
|
7236
7321
|
const db = await openDb$4();
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
if (!assertion) throw new Error("WebAuthn assertion failed");
|
|
7250
|
-
const prfOutput = assertion.getClientExtensionResults()?.prf?.results?.first;
|
|
7251
|
-
if (!prfOutput) throw new Error("PRF output not available from authenticator");
|
|
7252
|
-
const aesKey = await deriveAesKey(prfOutput, stored.salt);
|
|
7253
|
-
const privateKeyBytes = await crypto.subtle.decrypt({
|
|
7254
|
-
name: "AES-GCM",
|
|
7255
|
-
iv: stored.iv
|
|
7256
|
-
}, aesKey, stored.encryptedPrivateKey);
|
|
7257
|
-
const privateKey = new Uint8Array(privateKeyBytes);
|
|
7258
|
-
const challengeBytes = fromBase64url(challengeB64);
|
|
7259
|
-
const signature = await _noble_ed25519.signAsync(challengeBytes, privateKey);
|
|
7260
|
-
privateKey.fill(0);
|
|
7261
|
-
return toBase64url(signature);
|
|
7262
|
-
}
|
|
7263
|
-
/** Returns the stored base64url public key, or null if no identity exists. */
|
|
7264
|
-
async getPublicKey() {
|
|
7322
|
+
try {
|
|
7323
|
+
if (credentialIdHint) return (await dbGet(db, credentialIdHint))?.publicKey ?? null;
|
|
7324
|
+
const all = await dbGetAll(db);
|
|
7325
|
+
return all.length > 0 ? all[0].value.publicKey : null;
|
|
7326
|
+
} finally {
|
|
7327
|
+
db.close();
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
/**
|
|
7331
|
+
* Returns the locally-cached username label, or null if no identity is cached.
|
|
7332
|
+
*/
|
|
7333
|
+
async getUsername(credentialIdHint) {
|
|
7265
7334
|
const db = await openDb$4();
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7335
|
+
try {
|
|
7336
|
+
if (credentialIdHint) return (await dbGet(db, credentialIdHint))?.username ?? null;
|
|
7337
|
+
const all = await dbGetAll(db);
|
|
7338
|
+
return all.length > 0 ? all[0].value.username : null;
|
|
7339
|
+
} finally {
|
|
7340
|
+
db.close();
|
|
7341
|
+
}
|
|
7269
7342
|
}
|
|
7270
7343
|
/**
|
|
7271
|
-
*
|
|
7272
|
-
*
|
|
7273
|
-
* This is NOT the auth identifier (the public key is). It can be used as a
|
|
7274
|
-
* hint when calling POST /auth/register, or displayed before the user sets
|
|
7275
|
-
* a real display name via PATCH /users/me.
|
|
7344
|
+
* Updates the cached username for a given credential (or the first cached identity).
|
|
7345
|
+
* Call this after the user sets/changes their display name so it persists across devices.
|
|
7276
7346
|
*/
|
|
7277
|
-
async
|
|
7347
|
+
async setUsername(username, credentialIdHint) {
|
|
7278
7348
|
const db = await openDb$4();
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7349
|
+
try {
|
|
7350
|
+
if (credentialIdHint) {
|
|
7351
|
+
const stored = await dbGet(db, credentialIdHint);
|
|
7352
|
+
if (stored) await dbPut(db, credentialIdHint, {
|
|
7353
|
+
...stored,
|
|
7354
|
+
username
|
|
7355
|
+
});
|
|
7356
|
+
} else {
|
|
7357
|
+
const all = await dbGetAll(db);
|
|
7358
|
+
if (all.length > 0) await dbPut(db, all[0].key, {
|
|
7359
|
+
...all[0].value,
|
|
7360
|
+
username
|
|
7361
|
+
});
|
|
7362
|
+
}
|
|
7363
|
+
} finally {
|
|
7364
|
+
db.close();
|
|
7365
|
+
}
|
|
7282
7366
|
}
|
|
7283
|
-
/** Returns true if an identity is
|
|
7367
|
+
/** Returns true if an identity is cached in IndexedDB. */
|
|
7284
7368
|
async hasIdentity() {
|
|
7285
7369
|
const db = await openDb$4();
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7370
|
+
try {
|
|
7371
|
+
return (await dbGetAll(db)).length > 0;
|
|
7372
|
+
} finally {
|
|
7373
|
+
db.close();
|
|
7374
|
+
}
|
|
7289
7375
|
}
|
|
7290
|
-
/** Remove
|
|
7376
|
+
/** Remove cached identity record(s) from IndexedDB. The passkey itself
|
|
7377
|
+
* remains in the platform credential store. */
|
|
7291
7378
|
async clear() {
|
|
7292
7379
|
const db = await openDb$4();
|
|
7293
|
-
await
|
|
7380
|
+
await dbClear(db);
|
|
7294
7381
|
db.close();
|
|
7295
7382
|
}
|
|
7296
7383
|
/**
|
|
7297
|
-
* Returns the X25519 public key derived from the
|
|
7298
|
-
* Does NOT require WebAuthn — computed from the
|
|
7299
|
-
*
|
|
7300
|
-
* since nobleEd25519Curves.utils.toMontgomery only needs the public key.
|
|
7301
|
-
* Returns null if no identity is stored.
|
|
7384
|
+
* Returns the X25519 public key derived from the cached Ed25519 public key.
|
|
7385
|
+
* Does NOT require WebAuthn — computed from the cached public key only.
|
|
7386
|
+
* Returns null if no identity is cached.
|
|
7302
7387
|
*/
|
|
7303
7388
|
async getX25519PublicKey() {
|
|
7304
7389
|
const db = await openDb$4();
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7390
|
+
try {
|
|
7391
|
+
const all = await dbGetAll(db);
|
|
7392
|
+
if (all.length === 0) return null;
|
|
7393
|
+
const edPub = fromBase64url(all[0].value.publicKey);
|
|
7394
|
+
return ed25519.utils.toMontgomery(edPub);
|
|
7395
|
+
} finally {
|
|
7396
|
+
db.close();
|
|
7397
|
+
}
|
|
7310
7398
|
}
|
|
7311
7399
|
/**
|
|
7312
|
-
* Returns the X25519 private key derived from the
|
|
7313
|
-
* Requires WebAuthn assertion to
|
|
7400
|
+
* Returns the X25519 private key derived from the Ed25519 seed.
|
|
7401
|
+
* Requires a WebAuthn assertion to get the PRF output.
|
|
7314
7402
|
* The caller MUST wipe the returned Uint8Array after use.
|
|
7315
7403
|
*/
|
|
7316
|
-
async getX25519PrivateKey() {
|
|
7404
|
+
async getX25519PrivateKey(credentialIdHint) {
|
|
7405
|
+
const { seed } = await this._assertAndDerive(credentialIdHint);
|
|
7406
|
+
try {
|
|
7407
|
+
return ed25519.utils.toMontgomerySecret(seed);
|
|
7408
|
+
} finally {
|
|
7409
|
+
seed.fill(0);
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Trigger a WebAuthn assertion to derive (or re-derive) the identity and
|
|
7414
|
+
* update the IndexedDB cache. Returns the public key and credential ID.
|
|
7415
|
+
*
|
|
7416
|
+
* Use this when the cache is empty but you need the public key before
|
|
7417
|
+
* signing (e.g. to send it to the server for the challenge request).
|
|
7418
|
+
*/
|
|
7419
|
+
async deriveIdentity(credentialIdHint) {
|
|
7420
|
+
const { seed, publicKeyB64, credentialIdB64 } = await this._assertAndDerive(credentialIdHint);
|
|
7421
|
+
seed.fill(0);
|
|
7422
|
+
return {
|
|
7423
|
+
publicKey: publicKeyB64,
|
|
7424
|
+
credentialId: credentialIdB64
|
|
7425
|
+
};
|
|
7426
|
+
}
|
|
7427
|
+
/**
|
|
7428
|
+
* List all cached credential IDs. Useful for account switching UI.
|
|
7429
|
+
*/
|
|
7430
|
+
async listCachedIdentities() {
|
|
7317
7431
|
const db = await openDb$4();
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7432
|
+
try {
|
|
7433
|
+
return (await dbGetAll(db)).map(({ key, value }) => ({
|
|
7434
|
+
credentialId: key,
|
|
7435
|
+
publicKey: value.publicKey,
|
|
7436
|
+
username: value.username
|
|
7437
|
+
}));
|
|
7438
|
+
} finally {
|
|
7439
|
+
db.close();
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
/**
|
|
7443
|
+
* Perform a WebAuthn assertion with PRF, derive the Ed25519 seed, and
|
|
7444
|
+
* update the IndexedDB cache. Returns the seed (caller MUST wipe it).
|
|
7445
|
+
*/
|
|
7446
|
+
async _assertAndDerive(credentialIdHint) {
|
|
7447
|
+
const allowCredentials = [];
|
|
7448
|
+
if (credentialIdHint) allowCredentials.push({
|
|
7449
|
+
id: fromBase64url(credentialIdHint),
|
|
7450
|
+
type: "public-key"
|
|
7451
|
+
});
|
|
7452
|
+
else try {
|
|
7453
|
+
const db = await openDb$4();
|
|
7454
|
+
const all = await dbGetAll(db);
|
|
7455
|
+
db.close();
|
|
7456
|
+
for (const { value } of all) allowCredentials.push({
|
|
7457
|
+
id: value.credentialId,
|
|
7458
|
+
type: "public-key"
|
|
7459
|
+
});
|
|
7460
|
+
} catch {}
|
|
7321
7461
|
const assertion = await navigator.credentials.get({ publicKey: {
|
|
7322
7462
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
7323
|
-
allowCredentials:
|
|
7324
|
-
id: stored.credentialId,
|
|
7325
|
-
type: "public-key"
|
|
7326
|
-
}],
|
|
7463
|
+
...allowCredentials.length > 0 ? { allowCredentials } : {},
|
|
7327
7464
|
userVerification: "required",
|
|
7328
|
-
extensions: { prf: { eval: { first:
|
|
7465
|
+
extensions: { prf: { eval: { first: PRF_SALT.buffer } } }
|
|
7329
7466
|
} });
|
|
7330
|
-
if (!assertion) throw new Error("WebAuthn assertion
|
|
7331
|
-
const
|
|
7332
|
-
|
|
7333
|
-
const
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7467
|
+
if (!assertion) throw new Error("WebAuthn assertion cancelled");
|
|
7468
|
+
const seed = deriveEd25519Seed(extractPrfOutput(assertion));
|
|
7469
|
+
const publicKeyB64 = toBase64url(await _noble_ed25519.getPublicKeyAsync(seed));
|
|
7470
|
+
const credentialIdB64 = toBase64url(new Uint8Array(assertion.rawId));
|
|
7471
|
+
try {
|
|
7472
|
+
const db = await openDb$4();
|
|
7473
|
+
await dbPut(db, credentialIdB64, {
|
|
7474
|
+
username: (await dbGet(db, credentialIdB64))?.username ?? "",
|
|
7475
|
+
publicKey: publicKeyB64,
|
|
7476
|
+
credentialId: assertion.rawId
|
|
7477
|
+
});
|
|
7478
|
+
db.close();
|
|
7479
|
+
} catch {}
|
|
7480
|
+
return {
|
|
7481
|
+
seed,
|
|
7482
|
+
publicKeyB64,
|
|
7483
|
+
credentialIdB64
|
|
7484
|
+
};
|
|
7342
7485
|
}
|
|
7343
7486
|
};
|
|
7344
7487
|
|
|
@@ -8495,7 +8638,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8495
8638
|
concurrency: opts?.concurrency ?? 2,
|
|
8496
8639
|
syncTimeout: opts?.syncTimeout ?? 15e3,
|
|
8497
8640
|
prefetchFiles: opts?.prefetchFiles ?? true,
|
|
8498
|
-
throttleMs: opts?.throttleMs ??
|
|
8641
|
+
throttleMs: opts?.throttleMs ?? 200,
|
|
8499
8642
|
maxRetries: opts?.maxRetries ?? 2
|
|
8500
8643
|
};
|
|
8501
8644
|
let serverOrigin = "default";
|
|
@@ -9865,6 +10008,8 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9865
10008
|
this.yjsChannels = /* @__PURE__ */ new Map();
|
|
9866
10009
|
this.fileChannels = /* @__PURE__ */ new Map();
|
|
9867
10010
|
this.e2eeChannels = /* @__PURE__ */ new Map();
|
|
10011
|
+
this._resolvedE2ee = null;
|
|
10012
|
+
this._resolveE2eePromise = null;
|
|
9868
10013
|
this.peers = /* @__PURE__ */ new Map();
|
|
9869
10014
|
this.localPeerId = null;
|
|
9870
10015
|
this.isConnected = false;
|
|
@@ -10131,9 +10276,27 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
10131
10276
|
this.attachDataHandlers(peerId, pc);
|
|
10132
10277
|
return pc;
|
|
10133
10278
|
}
|
|
10279
|
+
/** Resolve the E2EE identity, supporting both pre-resolved objects and lazy factories. */
|
|
10280
|
+
async resolveE2ee() {
|
|
10281
|
+
if (this._resolvedE2ee) return this._resolvedE2ee;
|
|
10282
|
+
if (!this.config.e2ee) return null;
|
|
10283
|
+
if (typeof this.config.e2ee === "function") {
|
|
10284
|
+
if (!this._resolveE2eePromise) this._resolveE2eePromise = this.config.e2ee().then((id) => {
|
|
10285
|
+
this._resolvedE2ee = id;
|
|
10286
|
+
return id;
|
|
10287
|
+
});
|
|
10288
|
+
return this._resolveE2eePromise;
|
|
10289
|
+
}
|
|
10290
|
+
this._resolvedE2ee = this.config.e2ee;
|
|
10291
|
+
return this._resolvedE2ee;
|
|
10292
|
+
}
|
|
10134
10293
|
attachDataHandlers(peerId, pc) {
|
|
10135
|
-
if (this.config.e2ee) {
|
|
10136
|
-
|
|
10294
|
+
if (this.config.e2ee) this.resolveE2ee().then((identity) => {
|
|
10295
|
+
if (!identity) {
|
|
10296
|
+
this.startDataSync(peerId, pc);
|
|
10297
|
+
return;
|
|
10298
|
+
}
|
|
10299
|
+
const e2ee = new E2EEChannel(identity, this.config.docId);
|
|
10137
10300
|
this.e2eeChannels.set(peerId, e2ee);
|
|
10138
10301
|
pc.router.setEncryptor(e2ee);
|
|
10139
10302
|
pc.router.on("channelMessage", async ({ name, data }) => {
|
|
@@ -10160,7 +10323,14 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
10160
10323
|
error: err
|
|
10161
10324
|
});
|
|
10162
10325
|
});
|
|
10163
|
-
}
|
|
10326
|
+
}).catch((err) => {
|
|
10327
|
+
this.emit("e2eeFailed", {
|
|
10328
|
+
peerId,
|
|
10329
|
+
error: err
|
|
10330
|
+
});
|
|
10331
|
+
this.startDataSync(peerId, pc);
|
|
10332
|
+
});
|
|
10333
|
+
else this.startDataSync(peerId, pc);
|
|
10164
10334
|
}
|
|
10165
10335
|
startDataSync(peerId, pc) {
|
|
10166
10336
|
if (this.config.document && this.config.enableDocSync) {
|