diff options
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/users.ts | 33 |
1 files changed, 31 insertions, 2 deletions
diff --git a/routes/users.ts b/routes/users.ts index e301d708..20afc5d8 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -1074,6 +1074,10 @@ export default { const { pubkey } = req.params; const { user } = req; const app = await g(`app:${pubkey}`); + // Missing records must 404, not crash into a 500: clients (Damus one-click + // setup) probe this endpoint and treat 404 as "not configured yet, create + // one" — anything else aborts their whole connection flow. + if (!app) return res.code(404).send({ error: "connection not found" }); if (app.uid !== user.id) fail("unauthorized"); const lud16 = `${user.username}@${host}`; @@ -1097,7 +1101,11 @@ export default { async apps(req, res) { const { user } = req; const pubkeys = await db.sMembers(`${user.id}:apps`); - const apps = await Promise.all(pubkeys.map((p) => g(`app:${p}`))); + // Drop set members whose app record was deleted (e.g. an invalidation + // sweep) so one stale pubkey can't 500 the whole listing. + const apps = (await Promise.all(pubkeys.map((p) => g(`app:${p}`)))).filter( + Boolean, + ); const lud16 = `${user.username}@${host}`; @@ -1148,6 +1156,24 @@ export default { let app = await g(`app:${pubkey}`); if (app && uid !== app.uid) fail("Unauthorized"); + // Rotation is mandatory after a credential disclosure: the NWC pubkey is + // derived from the bearer secret, so re-registering a retired or + // quarantined pubkey would re-authorize the exact secret that was + // disclosed (we store client-supplied secrets, so they're part of any DB + // leak). Reject it and make the client mint a fresh secret — a new secret + // yields a new pubkey and never trips this check. + const credentialCutoff = Number(await g("nwc:credential-cutoff")) || 0; + const retired = + (app && + credentialCutoff > 0 && + (!Number(app.created) || Number(app.created) < credentialCutoff)) || + (await db.sIsMember("nwc:quarantined", pubkey)); + if (retired) + return res.code(403).send({ + error: + "This connection's key was retired for security reasons; connect again with a newly generated secret", + }); + const validRenewals = new Set([ "daily", "weekly", @@ -1189,7 +1215,10 @@ export default { secret, }; - if (!app?.created) app.created = Date.now(); + // Refresh the creation epoch on every owner post: this record passed the + // retirement check above, so the owner updating it (or minting it fresh) + // asserts it's live and keeps it clear of the credential cutoff. + app.created = Date.now(); await s(`app:${pubkey}`, app); await db.sAdd(`${uid}:apps`, pubkey); |
