API keys revealed once: store fingerprint (last-4) + hash, purge plaintext
This commit is contained in:
1 parent
6564508e1d
commit
c213a489e9
1 file changed
+27
-5
+27
-5
@@ -282,6 +282,23 @@ def get_db() -> sqlite3.Connection:
|
||||
# used to throttle re-invites of members who never activated.
|
||||
if "welcome_sent_at" not in cols:
|
||||
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
|
||||
|
||||
|
||||
@@ -899,12 +916,17 @@ async def broker_create_api_key(email: str, name: str) -> dict:
|
||||
sk_token = data.get("key", "")
|
||||
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.execute(
|
||||
"INSERT INTO member_api_keys (email, name, sk_token, hash) VALUES (?, ?, ?, ?) "
|
||||
"ON CONFLICT(email, name) DO UPDATE SET sk_token=excluded.sk_token, hash=excluded.hash, "
|
||||
"INSERT INTO member_api_keys (email, name, sk_token, hash, fingerprint) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(email, name) DO UPDATE SET sk_token='', hash=excluded.hash, fingerprint=excluded.fingerprint, "
|
||||
"created_at=datetime('now')",
|
||||
(email, name, sk_token, h),
|
||||
(email, name, "", h, fingerprint),
|
||||
)
|
||||
conn.commit()
|
||||
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]:
|
||||
conn = get_db()
|
||||
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,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
Reference in new issue
Block a user