forked from ahmedfgad/GeneticAlgorithmPython
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathencryption.py
More file actions
130 lines (97 loc) · 4.19 KB
/
encryption.py
File metadata and controls
130 lines (97 loc) · 4.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""
Fernet encryption helpers with ENV-aware master key resolution.
development
Uses the legacy local key file (DEV_ENCRYPTION_KEY_PATH, default C:/.encryption_key
on Windows). This preserves the existing single-key workflow.
production
Uses ENCRYPTION_KEY from the environment (Railway).
"""
from __future__ import annotations
import base64
import os
from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
import config
class EncryptionError(Exception):
"""Raised when encryption configuration or operations fail."""
def _normalize_fernet_key(raw: str | bytes) -> bytes:
"""Ensure the value is a valid Fernet key (url-safe base64, 32 bytes)."""
if isinstance(raw, str):
raw = raw.strip().encode()
try:
decoded = base64.urlsafe_b64decode(raw)
except Exception as exc:
raise EncryptionError("Invalid Fernet key format.") from exc
if len(decoded) != 32:
raise EncryptionError("Fernet key must decode to exactly 32 bytes.")
return base64.urlsafe_b64encode(decoded)
def _read_key_file(path: str | Path) -> bytes:
"""Read and normalize a key stored in a local file (development legacy)."""
key_path = Path(path)
if not key_path.exists():
raise EncryptionError(
f"Development encryption key file not found: {key_path}. "
"Create it or set DEV_ENCRYPTION_KEY_PATH."
)
content = key_path.read_text(encoding="utf-8").strip()
if not content:
raise EncryptionError(f"Encryption key file is empty: {key_path}")
return _normalize_fernet_key(content)
def get_master_key_bytes() -> bytes:
"""
Resolve the application master Fernet key according to ENV.
- development: read from local file (legacy behavior)
- production: read from ENCRYPTION_KEY environment variable
"""
if config.IS_DEVELOPMENT:
return _read_key_file(config.DEV_ENCRYPTION_KEY_PATH)
if config.IS_PRODUCTION:
env_key = config.ENCRYPTION_KEY
if not env_key or not env_key.strip():
raise EncryptionError(
"ENCRYPTION_KEY environment variable is required in production."
)
return _normalize_fernet_key(env_key.strip())
raise EncryptionError(f"Unsupported ENV: {config.ENV!r}")
def get_master_fernet() -> Fernet:
"""Return a Fernet instance using the ENV-resolved master key."""
return Fernet(get_master_key_bytes())
def generate_user_fernet_key() -> str:
"""Generate a new per-user Fernet key (plaintext, before master encryption)."""
return Fernet.generate_key().decode("utf-8")
def encrypt_for_storage(plaintext: str, fernet: Fernet | None = None) -> str:
"""Encrypt a string (e.g. per-user key) with the master Fernet; returns token str."""
f = fernet or get_master_fernet()
return f.encrypt(plaintext.encode("utf-8")).decode("utf-8")
def decrypt_from_storage(ciphertext: str, fernet: Fernet | None = None) -> str:
"""Decrypt a master-encrypted token back to plaintext."""
f = fernet or get_master_fernet()
try:
return f.decrypt(ciphertext.encode("utf-8")).decode("utf-8")
except InvalidToken as exc:
raise EncryptionError("Failed to decrypt stored value.") from exc
def get_user_fernet(encrypted_user_key: str) -> Fernet:
"""
Build a Fernet instance for a specific user's data.
The user's key is stored encrypted in the database; it is decrypted with
the master key first.
"""
user_key_plain = decrypt_from_storage(encrypted_user_key)
return Fernet(_normalize_fernet_key(user_key_plain))
def ensure_development_key_file() -> Path:
"""
Create a development key file if missing (first-run helper).
Does not run in production. Safe to call on app startup in development.
"""
if not config.IS_DEVELOPMENT:
return Path(config.DEV_ENCRYPTION_KEY_PATH)
key_path = Path(config.DEV_ENCRYPTION_KEY_PATH)
if key_path.exists():
return key_path
key_path.parent.mkdir(parents=True, exist_ok=True)
new_key = Fernet.generate_key().decode("utf-8")
key_path.write_text(new_key, encoding="utf-8")
# Restrict permissions on Unix
if os.name != "nt":
os.chmod(key_path, 0o600)
return key_path