93 lines
2.6 KiB
Python
93 lines
2.6 KiB
Python
"""Encrypted secret storage for sensitive Box data (app-level)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
from hashlib import pbkdf2_hmac
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
|
|
STATIC_SALT = b"klangkiste-static-salt-v1"
|
|
KDF_ITERATIONS = 200_000
|
|
|
|
|
|
class SecretStore:
|
|
def __init__(self, base_dir: Path, box_id: str) -> None:
|
|
self._path = base_dir / "secrets.enc"
|
|
self._box_id = box_id
|
|
|
|
def load_secrets(self) -> dict[str, Any]:
|
|
if not self._path.exists():
|
|
return {}
|
|
token = self._path.read_bytes()
|
|
fernet = Fernet(self._derive_key())
|
|
try:
|
|
payload = fernet.decrypt(token)
|
|
except InvalidToken:
|
|
return {}
|
|
return json.loads(payload.decode("utf-8"))
|
|
|
|
def save_secrets(self, data: dict[str, Any]) -> None:
|
|
fernet = Fernet(self._derive_key())
|
|
payload = json.dumps(data, indent=2).encode("utf-8")
|
|
token = fernet.encrypt(payload)
|
|
self._path.write_bytes(token)
|
|
|
|
def ensure_secret_seed(self) -> str:
|
|
data = self.load_secrets()
|
|
seed = data.get("secret_seed")
|
|
if isinstance(seed, str) and seed:
|
|
return seed
|
|
seed_value = self._generate_seed()
|
|
data["secret_seed"] = seed_value
|
|
self.save_secrets(data)
|
|
return seed_value
|
|
|
|
def get_secret_seed(self) -> str | None:
|
|
data = self.load_secrets()
|
|
seed = data.get("secret_seed")
|
|
if isinstance(seed, str) and seed:
|
|
return seed
|
|
return None
|
|
|
|
def save_api_token(self, token: str) -> None:
|
|
data = self.load_secrets()
|
|
data["api_token"] = token
|
|
self.save_secrets(data)
|
|
|
|
def get_api_token(self) -> str | None:
|
|
data = self.load_secrets()
|
|
token = data.get("api_token")
|
|
if isinstance(token, str) and token:
|
|
return token
|
|
return None
|
|
|
|
def clear_wifi_secrets(self) -> None:
|
|
data = self.load_secrets()
|
|
if "wifi_profiles" in data:
|
|
data.pop("wifi_profiles")
|
|
self.save_secrets(data)
|
|
|
|
def clear_all(self) -> None:
|
|
if self._path.exists():
|
|
self._path.unlink()
|
|
|
|
def _derive_key(self) -> bytes:
|
|
raw = pbkdf2_hmac(
|
|
"sha256",
|
|
self._box_id.encode("utf-8"),
|
|
STATIC_SALT,
|
|
KDF_ITERATIONS,
|
|
dklen=32,
|
|
)
|
|
return base64.urlsafe_b64encode(raw)
|
|
|
|
@staticmethod
|
|
def _generate_seed() -> str:
|
|
import secrets
|
|
|
|
return secrets.token_hex(32)
|