| """Lightweight local encryption helpers for credentials.""" |
|
|
| from __future__ import annotations |
|
|
| import base64 |
| import json |
| import os |
|
|
| from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
|
|
| from .config import CRED_DIR |
|
|
| SENSITIVE_KEYS = {"access", "refresh", "key", "token"} |
| KEY_FILE = CRED_DIR / ".kek" |
|
|
|
|
| def _get_kek() -> bytes: |
| CRED_DIR.mkdir(parents=True, exist_ok=True) |
| if KEY_FILE.exists(): |
| with open(KEY_FILE, "rb") as f: |
| return f.read() |
| key = AESGCM.generate_key(bit_length=256) |
| with open(KEY_FILE, "wb") as f: |
| f.write(key) |
| os.chmod(KEY_FILE, 0o600) |
| return key |
|
|
|
|
| def _aes_gcm_encrypt(plaintext: str) -> dict[str, str]: |
| key = _get_kek() |
| aesgcm = AESGCM(key) |
| iv = os.urandom(12) |
| ct = aesgcm.encrypt(iv, plaintext.encode("utf-8"), None) |
| return { |
| "iv": base64.b64encode(iv).decode(), |
| "ct": base64.b64encode(ct).decode(), |
| } |
|
|
|
|
| def _aes_gcm_decrypt(blob: dict[str, str]) -> str: |
| key = _get_kek() |
| aesgcm = AESGCM(key) |
| iv = base64.b64decode(blob["iv"]) |
| ct = base64.b64decode(blob["ct"]) |
| return aesgcm.decrypt(iv, ct, None).decode("utf-8") |
|
|
|
|
| def encrypt_value(value: str) -> dict[str, str]: |
| return _aes_gcm_encrypt(value) |
|
|
|
|
| def decrypt_value(blob: dict[str, str]) -> str: |
| return _aes_gcm_decrypt(blob) |
|
|
|
|
| def encrypt_record(record: dict) -> dict: |
| result: dict = {} |
| for k, v in record.items(): |
| if isinstance(v, str) and k in SENSITIVE_KEYS: |
| result[k] = encrypt_value(v) |
| else: |
| result[k] = v |
| return result |
|
|
|
|
| def decrypt_record(record: dict) -> dict: |
| result: dict = {} |
| for k, v in record.items(): |
| if isinstance(v, dict) and "iv" in v and "ct" in v: |
| result[k] = decrypt_value(v) |
| else: |
| result[k] = v |
| return result |
|
|
|
|
| def load_auth_data() -> dict: |
| from .config import AUTH_FILE |
|
|
| if not AUTH_FILE.exists(): |
| return {} |
| with open(AUTH_FILE, "r", encoding="utf-8") as f: |
| raw = json.load(f) |
| if not isinstance(raw, dict): |
| return {} |
| return {k: decrypt_record(v) if isinstance(v, dict) else v for k, v in raw.items()} |
|
|
|
|
| def save_auth_data(data: dict) -> None: |
| from .config import AUTH_FILE |
|
|
| CRED_DIR.mkdir(parents=True, exist_ok=True) |
| encrypted = { |
| k: encrypt_record(v) if isinstance(v, dict) else v for k, v in data.items() |
| } |
| with open(AUTH_FILE, "w", encoding="utf-8") as f: |
| json.dump(encrypted, f, indent=2) |
| os.chmod(AUTH_FILE, 0o600) |
|
|