# -*- coding: utf-8 -*- """Credentials repository. 包含加密路径(secret_storage='local_encrypted')与现存非加密路径 (none/env/windows_credential)的统一访问。 """ from __future__ import annotations import json from typing import Any, Optional from db.accounts_repo import _unix_to_iso_local, insert_credential as _insert_credential_raw from service.crypto import CredentialDecryptError, decrypt_secret from service.secret_store import delete_secret, mask_secret, read_secret CREDENTIAL_TYPES = frozenset( { "password", "api_key", "api_token", "bearer_token", "oauth_client", "refresh_token", "browser_profile", "note", } ) ALLOWED_SECRET_STORAGE = frozenset({"none", "env", "windows_credential", "local_encrypted"}) class CredentialNotEncryptedError(Exception): """Credential has no decryptable plaintext via this API (e.g. storage=none).""" code = "ERR_CREDENTIAL_NOT_ENCRYPTED" def _validate_credential_type(credential_type: str) -> None: if credential_type not in CREDENTIAL_TYPES: raise ValueError(f"invalid credential_type: {credential_type!r}") def _validate_storage(secret_storage: str) -> None: if secret_storage not in ALLOWED_SECRET_STORAGE: raise ValueError(f"invalid secret_storage: {secret_storage!r}") def insert_credential_encrypted( conn, *, account_id: Optional[int], platform_key: str, credential_label: str, credential_type: str, plaintext: str, expires_at: Optional[int], extra_json: Optional[str], now: int, ) -> int: _validate_credential_type(credential_type) if plaintext is None or str(plaintext).strip() == "": raise ValueError("plaintext required for encrypted credential insert") from service.crypto import encrypt_secret ciphertext = encrypt_secret(str(plaintext)) mask = mask_secret(str(plaintext)) return _insert_credential_raw( conn, account_id, platform_key, credential_label, credential_type, "local_encrypted", None, mask, expires_at, extra_json, now, secret_ciphertext=ciphertext, ) def insert_credential_external( conn, *, account_id: Optional[int], platform_key: str, credential_label: str, credential_type: str, secret_storage: str, secret_ref: Optional[str], secret_mask: Optional[str], expires_at: Optional[int], extra_json: Optional[str], now: int, ) -> int: _validate_credential_type(credential_type) _validate_storage(secret_storage) if secret_storage == "local_encrypted": raise ValueError("use insert_credential_encrypted for local_encrypted") return _insert_credential_raw( conn, account_id, platform_key, credential_label, credential_type, secret_storage, secret_ref, secret_mask, expires_at, extra_json, now, secret_ciphertext=None, ) def read_credential_plaintext(conn, credential_id: int) -> str: cur = conn.cursor() cur.execute( """ SELECT secret_storage, secret_ref, secret_ciphertext FROM credentials WHERE id = ? """, (int(credential_id),), ) row = cur.fetchone() if not row: raise ValueError(f"credential not found: id={credential_id}") storage = (row[0] or "").strip().lower() secret_ref = row[1] or "" secret_ciphertext = row[2] if storage == "local_encrypted": if not secret_ciphertext: raise CredentialDecryptError("missing secret_ciphertext") return decrypt_secret(str(secret_ciphertext)) if storage == "none": raise CredentialNotEncryptedError("credential storage is none") if storage == "env": return read_secret("env", secret_ref) if storage == "windows_credential": return read_secret("windows_credential", secret_ref) raise ValueError(f"unsupported secret_storage: {storage!r}") def list_credentials_by_account(conn, account_id: int) -> list[dict[str, Any]]: cur = conn.cursor() cur.execute( """ SELECT id, account_id, platform_key, credential_label, credential_type, secret_storage, secret_ref, secret_mask, expires_at, status, last_used_at, extra_json, created_at, updated_at FROM credentials WHERE account_id = ? ORDER BY updated_at DESC, id DESC """, (int(account_id),), ) rows = cur.fetchall() out: list[dict[str, Any]] = [] for r in rows: ex = r[11] parsed_ex: Any try: parsed_ex = json.loads(ex) if isinstance(ex, str) else (ex or {}) except json.JSONDecodeError: parsed_ex = {} out.append( { "id": r[0], "account_id": r[1], "platform_key": r[2], "credential_label": r[3], "credential_type": r[4], "secret_storage": r[5], "secret_ref": r[6] or "", "secret_mask": r[7] or "", "expires_at": _unix_to_iso_local(r[8]), "status": r[9], "last_used_at": _unix_to_iso_local(r[10]), "extra_json": parsed_ex, "created_at": _unix_to_iso_local(r[12]), "updated_at": _unix_to_iso_local(r[13]), } ) return out def delete_credential(conn, credential_id: int) -> None: cur = conn.cursor() cur.execute( "SELECT secret_storage, secret_ref FROM credentials WHERE id = ?", (int(credential_id),), ) row = cur.fetchone() if not row: return storage = (row[0] or "").strip().lower() secret_ref = row[1] or "" if storage == "windows_credential" and secret_ref: delete_secret("windows_credential", secret_ref) cur.execute("DELETE FROM credentials WHERE id = ?", (int(credential_id),))