@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
|
@@ -7071,15 +7071,60 @@ const ristretto255_oprf = createORPF({
|
|
|
7071
7071
|
/**
|
|
7072
7072
|
* CryptoIdentityKeystore
|
|
7073
7073
|
*
|
|
7074
|
-
* Per-
|
|
7075
|
-
*
|
|
7076
|
-
*
|
|
7077
|
-
*
|
|
7078
|
-
*
|
|
7079
|
-
*
|
|
7080
|
-
*
|
|
7081
|
-
*
|
|
7082
|
-
|
|
7074
|
+
* Per-user Ed25519 keypair derived deterministically from a synced WebAuthn
|
|
7075
|
+
* passkey's PRF extension output. The same passkey on any device produces the
|
|
7076
|
+
* same identity — no private key storage needed.
|
|
7077
|
+
*
|
|
7078
|
+
* Derivation chain:
|
|
7079
|
+
* Synced Passkey → PRF(constant salt) → HKDF-SHA256 → Ed25519 seed → keypair
|
|
7080
|
+
*
|
|
7081
|
+
* IndexedDB is used only as a lightweight cache for the public key and
|
|
7082
|
+
* credential ID. Loss of IndexedDB is non-catastrophic — a passkey assertion
|
|
7083
|
+
* re-derives everything.
|
|
7084
|
+
*
|
|
7085
|
+
* Dependencies: @noble/ed25519, @noble/hashes (for HKDF), @noble/curves (for X25519)
|
|
7086
|
+
*/
|
|
7087
|
+
/**
|
|
7088
|
+
* Fixed PRF eval salt. Must be constant across all devices so the same synced
|
|
7089
|
+
* passkey produces the same PRF output everywhere.
|
|
7090
|
+
*
|
|
7091
|
+
* Value: SHA-256("abracadabra-prf-salt-v1"), precomputed.
|
|
7092
|
+
*/
|
|
7093
|
+
const PRF_SALT = new Uint8Array([
|
|
7094
|
+
183,
|
|
7095
|
+
121,
|
|
7096
|
+
65,
|
|
7097
|
+
162,
|
|
7098
|
+
231,
|
|
7099
|
+
133,
|
|
7100
|
+
29,
|
|
7101
|
+
250,
|
|
7102
|
+
94,
|
|
7103
|
+
193,
|
|
7104
|
+
171,
|
|
7105
|
+
182,
|
|
7106
|
+
80,
|
|
7107
|
+
26,
|
|
7108
|
+
92,
|
|
7109
|
+
133,
|
|
7110
|
+
117,
|
|
7111
|
+
253,
|
|
7112
|
+
62,
|
|
7113
|
+
63,
|
|
7114
|
+
146,
|
|
7115
|
+
201,
|
|
7116
|
+
225,
|
|
7117
|
+
167,
|
|
7118
|
+
78,
|
|
7119
|
+
186,
|
|
7120
|
+
113,
|
|
7121
|
+
252,
|
|
7122
|
+
242,
|
|
7123
|
+
177,
|
|
7124
|
+
18,
|
|
7125
|
+
207
|
|
7126
|
+
]);
|
|
7127
|
+
const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
|
|
7083
7128
|
function toBase64url(bytes) {
|
|
7084
7129
|
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
7085
7130
|
}
|
|
@@ -7091,61 +7136,88 @@ function fromBase64url(b64) {
|
|
|
7091
7136
|
}
|
|
7092
7137
|
const DB_NAME = "abracadabra:identity";
|
|
7093
7138
|
const STORE_NAME = "identity";
|
|
7094
|
-
const RECORD_KEY = "current";
|
|
7095
|
-
const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
|
|
7096
7139
|
function openDb$4() {
|
|
7097
7140
|
return new Promise((resolve, reject) => {
|
|
7098
|
-
const req = indexedDB.open(DB_NAME,
|
|
7141
|
+
const req = indexedDB.open(DB_NAME, 2);
|
|
7099
7142
|
req.onupgradeneeded = () => {
|
|
7100
|
-
req.result
|
|
7143
|
+
const db = req.result;
|
|
7144
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
|
|
7101
7145
|
};
|
|
7102
7146
|
req.onsuccess = () => resolve(req.result);
|
|
7103
7147
|
req.onerror = () => reject(req.error);
|
|
7104
7148
|
});
|
|
7105
7149
|
}
|
|
7106
|
-
async function
|
|
7150
|
+
async function dbGetAll(db) {
|
|
7151
|
+
return new Promise((resolve, reject) => {
|
|
7152
|
+
const store = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME);
|
|
7153
|
+
const results = [];
|
|
7154
|
+
const req = store.openCursor();
|
|
7155
|
+
req.onsuccess = () => {
|
|
7156
|
+
const cursor = req.result;
|
|
7157
|
+
if (cursor) {
|
|
7158
|
+
results.push({
|
|
7159
|
+
key: cursor.key,
|
|
7160
|
+
value: cursor.value
|
|
7161
|
+
});
|
|
7162
|
+
cursor.continue();
|
|
7163
|
+
} else resolve(results);
|
|
7164
|
+
};
|
|
7165
|
+
req.onerror = () => reject(req.error);
|
|
7166
|
+
});
|
|
7167
|
+
}
|
|
7168
|
+
async function dbGet(db, key) {
|
|
7107
7169
|
return new Promise((resolve, reject) => {
|
|
7108
|
-
const req = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(
|
|
7170
|
+
const req = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(key);
|
|
7109
7171
|
req.onsuccess = () => resolve(req.result);
|
|
7110
7172
|
req.onerror = () => reject(req.error);
|
|
7111
7173
|
});
|
|
7112
7174
|
}
|
|
7113
|
-
async function dbPut(db, value) {
|
|
7175
|
+
async function dbPut(db, key, value) {
|
|
7114
7176
|
return new Promise((resolve, reject) => {
|
|
7115
|
-
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(value,
|
|
7177
|
+
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(value, key);
|
|
7116
7178
|
req.onsuccess = () => resolve();
|
|
7117
7179
|
req.onerror = () => reject(req.error);
|
|
7118
7180
|
});
|
|
7119
7181
|
}
|
|
7120
|
-
async function
|
|
7182
|
+
async function dbClear(db) {
|
|
7121
7183
|
return new Promise((resolve, reject) => {
|
|
7122
|
-
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).
|
|
7184
|
+
const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).clear();
|
|
7123
7185
|
req.onsuccess = () => resolve();
|
|
7124
7186
|
req.onerror = () => reject(req.error);
|
|
7125
7187
|
});
|
|
7126
7188
|
}
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
return
|
|
7189
|
+
/** Derive a 32-byte Ed25519 seed from raw PRF output. */
|
|
7190
|
+
function deriveEd25519Seed(prfOutput) {
|
|
7191
|
+
return hkdf(sha256, new Uint8Array(prfOutput), PRF_SALT, HKDF_INFO$2, 32);
|
|
7192
|
+
}
|
|
7193
|
+
function extractPrfOutput(credential) {
|
|
7194
|
+
const prfOutput = credential.getClientExtensionResults()?.prf?.results?.first;
|
|
7195
|
+
if (!prfOutput) throw new Error("WebAuthn PRF extension not available on this authenticator. A PRF-capable platform authenticator (e.g. iCloud Keychain) is required.");
|
|
7196
|
+
return prfOutput;
|
|
7130
7197
|
}
|
|
7131
7198
|
var CryptoIdentityKeystore = class {
|
|
7132
7199
|
/**
|
|
7133
|
-
*
|
|
7134
|
-
*
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7200
|
+
* Check whether the platform supports WebAuthn with PRF extension.
|
|
7201
|
+
* Call this before offering the "Secure with Passkey" option.
|
|
7202
|
+
*/
|
|
7203
|
+
static async isPrfAvailable() {
|
|
7204
|
+
if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
|
|
7205
|
+
try {
|
|
7206
|
+
if (!await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) return false;
|
|
7207
|
+
return true;
|
|
7208
|
+
} catch {
|
|
7209
|
+
return false;
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
/**
|
|
7213
|
+
* Create a synced discoverable passkey and derive the Ed25519 identity from
|
|
7214
|
+
* its PRF output. The passkey is stored in the platform credential manager
|
|
7215
|
+
* (e.g. iCloud Keychain) and syncs across devices automatically.
|
|
7140
7216
|
*
|
|
7141
|
-
*
|
|
7142
|
-
*
|
|
7143
|
-
* @param rpName - Human-readable relying party name.
|
|
7217
|
+
* Returns the base64url-encoded public key, X25519 public key, and credential ID.
|
|
7218
|
+
* The caller must register the public key with the server.
|
|
7144
7219
|
*/
|
|
7145
7220
|
async register(username, rpId, rpName) {
|
|
7146
|
-
const privateKey = ed.utils.randomPrivateKey();
|
|
7147
|
-
const publicKey = await ed.getPublicKeyAsync(privateKey);
|
|
7148
|
-
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
7149
7221
|
const credential = await navigator.credentials.create({ publicKey: {
|
|
7150
7222
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
7151
7223
|
rp: {
|
|
@@ -7164,151 +7236,222 @@ var CryptoIdentityKeystore = class {
|
|
|
7164
7236
|
alg: -257,
|
|
7165
7237
|
type: "public-key"
|
|
7166
7238
|
}],
|
|
7167
|
-
authenticatorSelection: {
|
|
7168
|
-
|
|
7239
|
+
authenticatorSelection: {
|
|
7240
|
+
residentKey: "required",
|
|
7241
|
+
requireResidentKey: true,
|
|
7242
|
+
userVerification: "required"
|
|
7243
|
+
},
|
|
7244
|
+
extensions: { prf: { eval: { first: PRF_SALT.buffer } } }
|
|
7169
7245
|
} });
|
|
7170
|
-
if (!credential) throw new Error("WebAuthn credential creation
|
|
7171
|
-
const
|
|
7172
|
-
|
|
7173
|
-
const
|
|
7174
|
-
const
|
|
7175
|
-
const
|
|
7176
|
-
name: "AES-GCM",
|
|
7177
|
-
iv
|
|
7178
|
-
}, aesKey, privateKey);
|
|
7246
|
+
if (!credential) throw new Error("WebAuthn credential creation cancelled");
|
|
7247
|
+
const seed = deriveEd25519Seed(extractPrfOutput(credential));
|
|
7248
|
+
const publicKey = await ed.getPublicKeyAsync(seed);
|
|
7249
|
+
const publicKeyB64 = toBase64url(publicKey);
|
|
7250
|
+
const x25519PubB64 = toBase64url(ed25519.utils.toMontgomery(publicKey));
|
|
7251
|
+
const credentialIdB64 = toBase64url(new Uint8Array(credential.rawId));
|
|
7179
7252
|
const db = await openDb$4();
|
|
7180
|
-
await dbPut(db, {
|
|
7253
|
+
await dbPut(db, credentialIdB64, {
|
|
7181
7254
|
username,
|
|
7182
|
-
publicKey:
|
|
7183
|
-
encryptedPrivateKey,
|
|
7184
|
-
iv,
|
|
7185
|
-
salt,
|
|
7255
|
+
publicKey: publicKeyB64,
|
|
7186
7256
|
credentialId: credential.rawId
|
|
7187
7257
|
});
|
|
7188
7258
|
db.close();
|
|
7189
|
-
|
|
7259
|
+
seed.fill(0);
|
|
7190
7260
|
return {
|
|
7191
|
-
publicKey:
|
|
7192
|
-
x25519PublicKey:
|
|
7261
|
+
publicKey: publicKeyB64,
|
|
7262
|
+
x25519PublicKey: x25519PubB64,
|
|
7263
|
+
credentialId: credentialIdB64
|
|
7193
7264
|
};
|
|
7194
7265
|
}
|
|
7195
7266
|
/**
|
|
7196
|
-
* Sign a base64url-encoded challenge using the
|
|
7197
|
-
*
|
|
7198
|
-
* This triggers a WebAuthn assertion (biometric / PIN prompt) to unlock the
|
|
7199
|
-
* private key via PRF → HKDF → AES-GCM decryption. The private key is
|
|
7200
|
-
* wiped from memory after signing.
|
|
7267
|
+
* Sign a base64url-encoded challenge using the Ed25519 key derived from
|
|
7268
|
+
* a passkey assertion. Triggers a WebAuthn prompt (biometric / PIN).
|
|
7201
7269
|
*
|
|
7202
7270
|
* @param challengeB64 - base64url-encoded challenge bytes from the server.
|
|
7271
|
+
* @param credentialIdHint - optional credential ID to select a specific passkey.
|
|
7203
7272
|
* @returns base64url-encoded Ed25519 signature (64 bytes).
|
|
7204
7273
|
*/
|
|
7205
|
-
async sign(challengeB64) {
|
|
7274
|
+
async sign(challengeB64, credentialIdHint) {
|
|
7275
|
+
const { seed } = await this._assertAndDerive(credentialIdHint);
|
|
7276
|
+
try {
|
|
7277
|
+
const challengeBytes = fromBase64url(challengeB64);
|
|
7278
|
+
return toBase64url(await ed.signAsync(challengeBytes, seed));
|
|
7279
|
+
} finally {
|
|
7280
|
+
seed.fill(0);
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
/**
|
|
7284
|
+
* Returns the cached base64url public key, or null if no identity is cached.
|
|
7285
|
+
*
|
|
7286
|
+
* Does NOT trigger a WebAuthn prompt. If the cache is empty (e.g. IndexedDB
|
|
7287
|
+
* cleared), returns null — the identity can be recovered via sign() or
|
|
7288
|
+
* a fresh register() with the same synced passkey.
|
|
7289
|
+
*/
|
|
7290
|
+
async getPublicKey(credentialIdHint) {
|
|
7206
7291
|
const db = await openDb$4();
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
if (!assertion) throw new Error("WebAuthn assertion failed");
|
|
7220
|
-
const prfOutput = assertion.getClientExtensionResults()?.prf?.results?.first;
|
|
7221
|
-
if (!prfOutput) throw new Error("PRF output not available from authenticator");
|
|
7222
|
-
const aesKey = await deriveAesKey(prfOutput, stored.salt);
|
|
7223
|
-
const privateKeyBytes = await crypto.subtle.decrypt({
|
|
7224
|
-
name: "AES-GCM",
|
|
7225
|
-
iv: stored.iv
|
|
7226
|
-
}, aesKey, stored.encryptedPrivateKey);
|
|
7227
|
-
const privateKey = new Uint8Array(privateKeyBytes);
|
|
7228
|
-
const challengeBytes = fromBase64url(challengeB64);
|
|
7229
|
-
const signature = await ed.signAsync(challengeBytes, privateKey);
|
|
7230
|
-
privateKey.fill(0);
|
|
7231
|
-
return toBase64url(signature);
|
|
7232
|
-
}
|
|
7233
|
-
/** Returns the stored base64url public key, or null if no identity exists. */
|
|
7234
|
-
async getPublicKey() {
|
|
7292
|
+
try {
|
|
7293
|
+
if (credentialIdHint) return (await dbGet(db, credentialIdHint))?.publicKey ?? null;
|
|
7294
|
+
const all = await dbGetAll(db);
|
|
7295
|
+
return all.length > 0 ? all[0].value.publicKey : null;
|
|
7296
|
+
} finally {
|
|
7297
|
+
db.close();
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
/**
|
|
7301
|
+
* Returns the locally-cached username label, or null if no identity is cached.
|
|
7302
|
+
*/
|
|
7303
|
+
async getUsername(credentialIdHint) {
|
|
7235
7304
|
const db = await openDb$4();
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7305
|
+
try {
|
|
7306
|
+
if (credentialIdHint) return (await dbGet(db, credentialIdHint))?.username ?? null;
|
|
7307
|
+
const all = await dbGetAll(db);
|
|
7308
|
+
return all.length > 0 ? all[0].value.username : null;
|
|
7309
|
+
} finally {
|
|
7310
|
+
db.close();
|
|
7311
|
+
}
|
|
7239
7312
|
}
|
|
7240
7313
|
/**
|
|
7241
|
-
*
|
|
7242
|
-
*
|
|
7243
|
-
* This is NOT the auth identifier (the public key is). It can be used as a
|
|
7244
|
-
* hint when calling POST /auth/register, or displayed before the user sets
|
|
7245
|
-
* a real display name via PATCH /users/me.
|
|
7314
|
+
* Updates the cached username for a given credential (or the first cached identity).
|
|
7315
|
+
* Call this after the user sets/changes their display name so it persists across devices.
|
|
7246
7316
|
*/
|
|
7247
|
-
async
|
|
7317
|
+
async setUsername(username, credentialIdHint) {
|
|
7248
7318
|
const db = await openDb$4();
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7319
|
+
try {
|
|
7320
|
+
if (credentialIdHint) {
|
|
7321
|
+
const stored = await dbGet(db, credentialIdHint);
|
|
7322
|
+
if (stored) await dbPut(db, credentialIdHint, {
|
|
7323
|
+
...stored,
|
|
7324
|
+
username
|
|
7325
|
+
});
|
|
7326
|
+
} else {
|
|
7327
|
+
const all = await dbGetAll(db);
|
|
7328
|
+
if (all.length > 0) await dbPut(db, all[0].key, {
|
|
7329
|
+
...all[0].value,
|
|
7330
|
+
username
|
|
7331
|
+
});
|
|
7332
|
+
}
|
|
7333
|
+
} finally {
|
|
7334
|
+
db.close();
|
|
7335
|
+
}
|
|
7252
7336
|
}
|
|
7253
|
-
/** Returns true if an identity is
|
|
7337
|
+
/** Returns true if an identity is cached in IndexedDB. */
|
|
7254
7338
|
async hasIdentity() {
|
|
7255
7339
|
const db = await openDb$4();
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7340
|
+
try {
|
|
7341
|
+
return (await dbGetAll(db)).length > 0;
|
|
7342
|
+
} finally {
|
|
7343
|
+
db.close();
|
|
7344
|
+
}
|
|
7259
7345
|
}
|
|
7260
|
-
/** Remove
|
|
7346
|
+
/** Remove cached identity record(s) from IndexedDB. The passkey itself
|
|
7347
|
+
* remains in the platform credential store. */
|
|
7261
7348
|
async clear() {
|
|
7262
7349
|
const db = await openDb$4();
|
|
7263
|
-
await
|
|
7350
|
+
await dbClear(db);
|
|
7264
7351
|
db.close();
|
|
7265
7352
|
}
|
|
7266
7353
|
/**
|
|
7267
|
-
* Returns the X25519 public key derived from the
|
|
7268
|
-
* Does NOT require WebAuthn — computed from the
|
|
7269
|
-
*
|
|
7270
|
-
* since nobleEd25519Curves.utils.toMontgomery only needs the public key.
|
|
7271
|
-
* Returns null if no identity is stored.
|
|
7354
|
+
* Returns the X25519 public key derived from the cached Ed25519 public key.
|
|
7355
|
+
* Does NOT require WebAuthn — computed from the cached public key only.
|
|
7356
|
+
* Returns null if no identity is cached.
|
|
7272
7357
|
*/
|
|
7273
7358
|
async getX25519PublicKey() {
|
|
7274
7359
|
const db = await openDb$4();
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7360
|
+
try {
|
|
7361
|
+
const all = await dbGetAll(db);
|
|
7362
|
+
if (all.length === 0) return null;
|
|
7363
|
+
const edPub = fromBase64url(all[0].value.publicKey);
|
|
7364
|
+
return ed25519.utils.toMontgomery(edPub);
|
|
7365
|
+
} finally {
|
|
7366
|
+
db.close();
|
|
7367
|
+
}
|
|
7280
7368
|
}
|
|
7281
7369
|
/**
|
|
7282
|
-
* Returns the X25519 private key derived from the
|
|
7283
|
-
* Requires WebAuthn assertion to
|
|
7370
|
+
* Returns the X25519 private key derived from the Ed25519 seed.
|
|
7371
|
+
* Requires a WebAuthn assertion to get the PRF output.
|
|
7284
7372
|
* The caller MUST wipe the returned Uint8Array after use.
|
|
7285
7373
|
*/
|
|
7286
|
-
async getX25519PrivateKey() {
|
|
7374
|
+
async getX25519PrivateKey(credentialIdHint) {
|
|
7375
|
+
const { seed } = await this._assertAndDerive(credentialIdHint);
|
|
7376
|
+
try {
|
|
7377
|
+
return ed25519.utils.toMontgomerySecret(seed);
|
|
7378
|
+
} finally {
|
|
7379
|
+
seed.fill(0);
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
/**
|
|
7383
|
+
* Trigger a WebAuthn assertion to derive (or re-derive) the identity and
|
|
7384
|
+
* update the IndexedDB cache. Returns the public key and credential ID.
|
|
7385
|
+
*
|
|
7386
|
+
* Use this when the cache is empty but you need the public key before
|
|
7387
|
+
* signing (e.g. to send it to the server for the challenge request).
|
|
7388
|
+
*/
|
|
7389
|
+
async deriveIdentity(credentialIdHint) {
|
|
7390
|
+
const { seed, publicKeyB64, credentialIdB64 } = await this._assertAndDerive(credentialIdHint);
|
|
7391
|
+
seed.fill(0);
|
|
7392
|
+
return {
|
|
7393
|
+
publicKey: publicKeyB64,
|
|
7394
|
+
credentialId: credentialIdB64
|
|
7395
|
+
};
|
|
7396
|
+
}
|
|
7397
|
+
/**
|
|
7398
|
+
* List all cached credential IDs. Useful for account switching UI.
|
|
7399
|
+
*/
|
|
7400
|
+
async listCachedIdentities() {
|
|
7287
7401
|
const db = await openDb$4();
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7402
|
+
try {
|
|
7403
|
+
return (await dbGetAll(db)).map(({ key, value }) => ({
|
|
7404
|
+
credentialId: key,
|
|
7405
|
+
publicKey: value.publicKey,
|
|
7406
|
+
username: value.username
|
|
7407
|
+
}));
|
|
7408
|
+
} finally {
|
|
7409
|
+
db.close();
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Perform a WebAuthn assertion with PRF, derive the Ed25519 seed, and
|
|
7414
|
+
* update the IndexedDB cache. Returns the seed (caller MUST wipe it).
|
|
7415
|
+
*/
|
|
7416
|
+
async _assertAndDerive(credentialIdHint) {
|
|
7417
|
+
const allowCredentials = [];
|
|
7418
|
+
if (credentialIdHint) allowCredentials.push({
|
|
7419
|
+
id: fromBase64url(credentialIdHint),
|
|
7420
|
+
type: "public-key"
|
|
7421
|
+
});
|
|
7422
|
+
else try {
|
|
7423
|
+
const db = await openDb$4();
|
|
7424
|
+
const all = await dbGetAll(db);
|
|
7425
|
+
db.close();
|
|
7426
|
+
for (const { value } of all) allowCredentials.push({
|
|
7427
|
+
id: value.credentialId,
|
|
7428
|
+
type: "public-key"
|
|
7429
|
+
});
|
|
7430
|
+
} catch {}
|
|
7291
7431
|
const assertion = await navigator.credentials.get({ publicKey: {
|
|
7292
7432
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
7293
|
-
allowCredentials:
|
|
7294
|
-
id: stored.credentialId,
|
|
7295
|
-
type: "public-key"
|
|
7296
|
-
}],
|
|
7433
|
+
...allowCredentials.length > 0 ? { allowCredentials } : {},
|
|
7297
7434
|
userVerification: "required",
|
|
7298
|
-
extensions: { prf: { eval: { first:
|
|
7435
|
+
extensions: { prf: { eval: { first: PRF_SALT.buffer } } }
|
|
7299
7436
|
} });
|
|
7300
|
-
if (!assertion) throw new Error("WebAuthn assertion
|
|
7301
|
-
const
|
|
7302
|
-
|
|
7303
|
-
const
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7437
|
+
if (!assertion) throw new Error("WebAuthn assertion cancelled");
|
|
7438
|
+
const seed = deriveEd25519Seed(extractPrfOutput(assertion));
|
|
7439
|
+
const publicKeyB64 = toBase64url(await ed.getPublicKeyAsync(seed));
|
|
7440
|
+
const credentialIdB64 = toBase64url(new Uint8Array(assertion.rawId));
|
|
7441
|
+
try {
|
|
7442
|
+
const db = await openDb$4();
|
|
7443
|
+
await dbPut(db, credentialIdB64, {
|
|
7444
|
+
username: (await dbGet(db, credentialIdB64))?.username ?? "",
|
|
7445
|
+
publicKey: publicKeyB64,
|
|
7446
|
+
credentialId: assertion.rawId
|
|
7447
|
+
});
|
|
7448
|
+
db.close();
|
|
7449
|
+
} catch {}
|
|
7450
|
+
return {
|
|
7451
|
+
seed,
|
|
7452
|
+
publicKeyB64,
|
|
7453
|
+
credentialIdB64
|
|
7454
|
+
};
|
|
7312
7455
|
}
|
|
7313
7456
|
};
|
|
7314
7457
|
|
|
@@ -8443,7 +8586,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8443
8586
|
concurrency: opts?.concurrency ?? 2,
|
|
8444
8587
|
syncTimeout: opts?.syncTimeout ?? 15e3,
|
|
8445
8588
|
prefetchFiles: opts?.prefetchFiles ?? true,
|
|
8446
|
-
throttleMs: opts?.throttleMs ??
|
|
8589
|
+
throttleMs: opts?.throttleMs ?? 200,
|
|
8447
8590
|
maxRetries: opts?.maxRetries ?? 2
|
|
8448
8591
|
};
|
|
8449
8592
|
let serverOrigin = "default";
|
|
@@ -9813,6 +9956,8 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9813
9956
|
this.yjsChannels = /* @__PURE__ */ new Map();
|
|
9814
9957
|
this.fileChannels = /* @__PURE__ */ new Map();
|
|
9815
9958
|
this.e2eeChannels = /* @__PURE__ */ new Map();
|
|
9959
|
+
this._resolvedE2ee = null;
|
|
9960
|
+
this._resolveE2eePromise = null;
|
|
9816
9961
|
this.peers = /* @__PURE__ */ new Map();
|
|
9817
9962
|
this.localPeerId = null;
|
|
9818
9963
|
this.isConnected = false;
|
|
@@ -10079,9 +10224,27 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
10079
10224
|
this.attachDataHandlers(peerId, pc);
|
|
10080
10225
|
return pc;
|
|
10081
10226
|
}
|
|
10227
|
+
/** Resolve the E2EE identity, supporting both pre-resolved objects and lazy factories. */
|
|
10228
|
+
async resolveE2ee() {
|
|
10229
|
+
if (this._resolvedE2ee) return this._resolvedE2ee;
|
|
10230
|
+
if (!this.config.e2ee) return null;
|
|
10231
|
+
if (typeof this.config.e2ee === "function") {
|
|
10232
|
+
if (!this._resolveE2eePromise) this._resolveE2eePromise = this.config.e2ee().then((id) => {
|
|
10233
|
+
this._resolvedE2ee = id;
|
|
10234
|
+
return id;
|
|
10235
|
+
});
|
|
10236
|
+
return this._resolveE2eePromise;
|
|
10237
|
+
}
|
|
10238
|
+
this._resolvedE2ee = this.config.e2ee;
|
|
10239
|
+
return this._resolvedE2ee;
|
|
10240
|
+
}
|
|
10082
10241
|
attachDataHandlers(peerId, pc) {
|
|
10083
|
-
if (this.config.e2ee) {
|
|
10084
|
-
|
|
10242
|
+
if (this.config.e2ee) this.resolveE2ee().then((identity) => {
|
|
10243
|
+
if (!identity) {
|
|
10244
|
+
this.startDataSync(peerId, pc);
|
|
10245
|
+
return;
|
|
10246
|
+
}
|
|
10247
|
+
const e2ee = new E2EEChannel(identity, this.config.docId);
|
|
10085
10248
|
this.e2eeChannels.set(peerId, e2ee);
|
|
10086
10249
|
pc.router.setEncryptor(e2ee);
|
|
10087
10250
|
pc.router.on("channelMessage", async ({ name, data }) => {
|
|
@@ -10108,7 +10271,14 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
10108
10271
|
error: err
|
|
10109
10272
|
});
|
|
10110
10273
|
});
|
|
10111
|
-
}
|
|
10274
|
+
}).catch((err) => {
|
|
10275
|
+
this.emit("e2eeFailed", {
|
|
10276
|
+
peerId,
|
|
10277
|
+
error: err
|
|
10278
|
+
});
|
|
10279
|
+
this.startDataSync(peerId, pc);
|
|
10280
|
+
});
|
|
10281
|
+
else this.startDataSync(peerId, pc);
|
|
10112
10282
|
}
|
|
10113
10283
|
startDataSync(peerId, pc) {
|
|
10114
10284
|
if (this.config.document && this.config.enableDocSync) {
|