57 lines
1.4 KiB
Python
57 lines
1.4 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from cryptography.fernet import Fernet, InvalidToken
|
||
|
|
|
||
|
|
from paths import DATA_DIR
|
||
|
|
|
||
|
|
KEY_FILE = DATA_DIR / ".secret_key"
|
||
|
|
ENCRYPTED_PREFIX = "enc:"
|
||
|
|
|
||
|
|
|
||
|
|
def _ensure_key() -> bytes:
|
||
|
|
KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
if KEY_FILE.exists():
|
||
|
|
raw = KEY_FILE.read_bytes().strip()
|
||
|
|
if raw:
|
||
|
|
return raw
|
||
|
|
key = Fernet.generate_key()
|
||
|
|
KEY_FILE.write_bytes(key)
|
||
|
|
try:
|
||
|
|
KEY_FILE.chmod(0o600)
|
||
|
|
except OSError:
|
||
|
|
pass
|
||
|
|
return key
|
||
|
|
|
||
|
|
|
||
|
|
def _fernet() -> Fernet:
|
||
|
|
return Fernet(_ensure_key())
|
||
|
|
|
||
|
|
|
||
|
|
def encrypt_secret(plain: str) -> str:
|
||
|
|
if not plain:
|
||
|
|
return ""
|
||
|
|
if plain.startswith(ENCRYPTED_PREFIX):
|
||
|
|
return plain
|
||
|
|
token = _fernet().encrypt(plain.encode("utf-8")).decode("ascii")
|
||
|
|
return f"{ENCRYPTED_PREFIX}{token}"
|
||
|
|
|
||
|
|
|
||
|
|
def decrypt_secret(value: str) -> str:
|
||
|
|
if not value:
|
||
|
|
return ""
|
||
|
|
if not value.startswith(ENCRYPTED_PREFIX):
|
||
|
|
return value
|
||
|
|
token = value[len(ENCRYPTED_PREFIX) :].encode("ascii")
|
||
|
|
try:
|
||
|
|
return _fernet().decrypt(token).decode("utf-8")
|
||
|
|
except InvalidToken as exc:
|
||
|
|
raise ValueError(
|
||
|
|
"Could not decrypt stored password. Secret key may have changed; re-enter password."
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
|
||
|
|
def is_encrypted(value: str) -> bool:
|
||
|
|
return bool(value) and value.startswith(ENCRYPTED_PREFIX)
|