| """ |
| Compute credential store (PR 2). |
| |
| Sources that talk to a remote endpoint (an OpenAI-compatible API, a hosted |
| provider) need a credential. We never keep that in the frontend or in the |
| ``ComputeSource`` row — the source stores only a ``credential_ref`` (an opaque |
| key), and the actual secret lives here, server-side. |
| |
| Encryption is opt-in and mirrors ``app.cloud_tokens``: if ``cryptography`` is |
| installed and ``HOMEPILOT_COMPUTE_SECRET_KEY`` (a Fernet key) is set, values are |
| encrypted at rest ("f:"); otherwise they are stored server-side as-is ("p:"). |
| Either way the secret is out of the browser — the primary goal. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import sqlite3 |
| import time |
| import uuid |
| from typing import Optional |
|
|
| from ..storage import _get_db_path |
|
|
| _TABLE = "compute_secrets" |
| _fernet_cache: object = None |
|
|
|
|
| def _fernet(): |
| global _fernet_cache |
| if _fernet_cache is not None: |
| return _fernet_cache or None |
| key = os.getenv("HOMEPILOT_COMPUTE_SECRET_KEY", "").strip() |
| if not key: |
| _fernet_cache = False |
| return None |
| try: |
| from cryptography.fernet import Fernet |
| _fernet_cache = Fernet(key.encode()) |
| return _fernet_cache |
| except Exception: |
| _fernet_cache = False |
| return None |
|
|
|
|
| def _encode(secret: str) -> str: |
| f = _fernet() |
| if f: |
| try: |
| return "f:" + f.encrypt(secret.encode()).decode() |
| except Exception: |
| pass |
| return "p:" + secret |
|
|
|
|
| def _decode(stored: str) -> Optional[str]: |
| if stored.startswith("f:"): |
| f = _fernet() |
| if not f: |
| return None |
| try: |
| return f.decrypt(stored[2:].encode()).decode() |
| except Exception: |
| return None |
| if stored.startswith("p:"): |
| return stored[2:] |
| return stored |
|
|
|
|
| def ensure_table() -> None: |
| con = sqlite3.connect(_get_db_path()) |
| try: |
| con.execute( |
| f""" |
| CREATE TABLE IF NOT EXISTS {_TABLE} ( |
| ref TEXT PRIMARY KEY, |
| secret_enc TEXT NOT NULL, |
| updated_at REAL NOT NULL |
| ) |
| """ |
| ) |
| con.commit() |
| finally: |
| con.close() |
|
|
|
|
| def put_secret(secret: str, *, ref: Optional[str] = None) -> str: |
| """Store (upsert) a secret and return its ref. Generates a ref if omitted.""" |
| ref = ref or f"cred_{uuid.uuid4().hex[:16]}" |
| if not secret: |
| return ref |
| ensure_table() |
| con = sqlite3.connect(_get_db_path()) |
| try: |
| con.execute( |
| f"INSERT INTO {_TABLE}(ref, secret_enc, updated_at) VALUES (?,?,?) " |
| f"ON CONFLICT(ref) DO UPDATE SET secret_enc=excluded.secret_enc, " |
| f"updated_at=excluded.updated_at", |
| (ref, _encode(secret), time.time()), |
| ) |
| con.commit() |
| finally: |
| con.close() |
| return ref |
|
|
|
|
| def get_secret(ref: Optional[str]) -> Optional[str]: |
| """Return the secret for ``ref`` (decrypted), or None. Never raises.""" |
| if not ref: |
| return None |
| try: |
| ensure_table() |
| con = sqlite3.connect(_get_db_path()) |
| try: |
| row = con.execute( |
| f"SELECT secret_enc FROM {_TABLE} WHERE ref = ?", (ref,) |
| ).fetchone() |
| finally: |
| con.close() |
| except Exception: |
| return None |
| if not row or not row[0]: |
| return None |
| return _decode(row[0]) |
|
|
|
|
| def delete_secret(ref: Optional[str]) -> None: |
| if not ref: |
| return |
| try: |
| ensure_table() |
| con = sqlite3.connect(_get_db_path()) |
| try: |
| con.execute(f"DELETE FROM {_TABLE} WHERE ref = ?", (ref,)) |
| con.commit() |
| finally: |
| con.close() |
| except Exception: |
| pass |
|
|