API keys revealed once: store fingerprint (last-4) + hash, purge plaintext

This commit is contained in:
inference-bot committed 2026-09-24 08:00:02 -06:00
1 parent 6564508e1d
commit c213a489e9
1 file changed
+27 -5
+27 -5
View File
@@ -282,6 +282,23 @@ def get_db() -> sqlite3.Connection:
# used to throttle re-invites of members who never activated. # used to throttle re-invites of members who never activated.
if "welcome_sent_at" not in cols: if "welcome_sent_at" not in cols:
conn.execute("ALTER TABLE members ADD COLUMN welcome_sent_at TEXT") conn.execute("ALTER TABLE members ADD COLUMN welcome_sent_at TEXT")
# Member-managed API keys: add a fingerprint (last-4 of the key, shown in
# lists) and purge the stored plaintext. Member API keys are revealed ONCE
# (at creation) and never again — the industry standard. Revocation uses the
# LiteLLM hash, not the plaintext, so the portal has no reason to keep it.
# (The `members.key_token` chat key is a different table and IS retained in
# plaintext — the injector needs it on every request.)
ak_cols = [r[1] for r in conn.execute("PRAGMA table_info(member_api_keys)").fetchall()]
if "fingerprint" not in ak_cols:
conn.execute("ALTER TABLE member_api_keys ADD COLUMN fingerprint TEXT")
# Backfill fingerprints for keys that already have plaintext stored,
# then purge the plaintext. (fingerprint = last-4 of the key.)
conn.execute(
"UPDATE member_api_keys SET fingerprint = substr(sk_token, -4) "
"WHERE fingerprint IS NULL AND sk_token != ''"
)
conn.execute("UPDATE member_api_keys SET sk_token = '' WHERE sk_token != ''")
return conn return conn
@@ -899,12 +916,17 @@ async def broker_create_api_key(email: str, name: str) -> dict:
sk_token = data.get("key", "") sk_token = data.get("key", "")
h = data.get("token") or data.get("token_id") or "" h = data.get("token") or data.get("token_id") or ""
# Reveal the key ONCE (in the response). Persist only a fingerprint (last-4)
# and the LiteLLM hash — the plaintext is NOT stored. Revocation uses the
# hash, so the full key never needs to be retained.
fingerprint = sk_token[-4:] if sk_token else ""
conn = get_db() conn = get_db()
conn.execute( conn.execute(
"INSERT INTO member_api_keys (email, name, sk_token, hash) VALUES (?, ?, ?, ?) " "INSERT INTO member_api_keys (email, name, sk_token, hash, fingerprint) VALUES (?, ?, ?, ?, ?) "
"ON CONFLICT(email, name) DO UPDATE SET sk_token=excluded.sk_token, hash=excluded.hash, " "ON CONFLICT(email, name) DO UPDATE SET sk_token='', hash=excluded.hash, fingerprint=excluded.fingerprint, "
"created_at=datetime('now')", "created_at=datetime('now')",
(email, name, sk_token, h), (email, name, "", h, fingerprint),
) )
conn.commit() conn.commit()
row = conn.execute( row = conn.execute(
@@ -918,12 +940,12 @@ async def broker_create_api_key(email: str, name: str) -> dict:
async def broker_list_api_keys(email: str) -> list[dict]: async def broker_list_api_keys(email: str) -> list[dict]:
conn = get_db() conn = get_db()
rows = conn.execute( rows = conn.execute(
"SELECT name, sk_token, created_at FROM member_api_keys WHERE email = ? ORDER BY created_at DESC", "SELECT name, fingerprint, created_at FROM member_api_keys WHERE email = ? ORDER BY created_at DESC",
(email,), (email,),
).fetchall() ).fetchall()
conn.close() conn.close()
return [ return [
{"name": r[0], "sk_token": r[1], "created_at": r[2]} {"name": r[0], "fingerprint": r[1] or "", "created_at": r[2]}
for r in rows for r in rows
] ]