-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcrypto_manager.py
More file actions
335 lines (271 loc) · 10.6 KB
/
Copy pathcrypto_manager.py
File metadata and controls
335 lines (271 loc) · 10.6 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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
"""
crypto_manager.py - Cifratura credenziali per PCM
Algoritmo: Fernet (AES-128-CBC + HMAC-SHA256)
KDF: PBKDF2-HMAC-SHA256, 480.000 iterazioni (OWASP 2023)
Sale: 32 byte casuali, salvato in pcm_settings.json (non segreto)
Chiave: derivata dalla password master, tenuta in memoria (_KEY),
mai scritta su disco.
Campi cifrati in connections.json: "user" e "password".
I valori cifrati hanno il prefisso "ENC:" per distinguerli dal testo chiaro.
Flusso primo avvio:
1. config_manager rileva che connections.json non esiste ancora
2. Chiede se cifrare (dialog in PCM.py)
3. Se sì: genera sale, deriva chiave, salva sale in settings,
setta _KEY in memoria
4. Ogni save_profiles() cifra automaticamente user/password
Flusso avvii successivi (cifratura attiva):
1. PCM.py rileva "crypto.enabled": True in settings
2. Mostra dialog di sblocco password
3. Chiama unlock(password) → deriva _KEY
4. load_profiles() decifra automaticamente
API pubblica:
is_enabled() → bool
is_unlocked() → bool
setup(password) → configura cifratura (primo avvio o cambio password)
unlock(password) → bool (verifica e sblocca)
lock() → cancella chiave dalla memoria
change_password(old, new) → bool
disable(password) → bool (rimuove cifratura, torna in chiaro)
encrypt_field(val) → str (ENC:... o val se non cifrato)
decrypt_field(val) → str (testo chiaro)
encrypt_profile(p) → dict (copia del profilo con campi cifrati)
decrypt_profile(p) → dict (copia del profilo con campi in chiaro)
"""
import os
import base64
import secrets
import threading
# Importazione lazy di cryptography per dare errore chiaro se mancante
def _get_fernet():
try:
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
return Fernet, InvalidToken, PBKDF2HMAC, hashes
except ImportError:
raise ImportError(
"Il modulo 'cryptography' non è installato.\n"
"Esegui: pip install cryptography\n"
"oppure: bash setup.sh"
)
# ---------------------------------------------------------------------------
# Stato in memoria (mai scritto su disco)
# ---------------------------------------------------------------------------
_KEY: bytes | None = None # chiave Fernet derivata dalla password
_lock = threading.Lock() # protegge l'accesso a _KEY
_ENC_PREFIX = "ENC:" # prefisso per valori cifrati
_FIELDS_TO_ENCRYPT = ("user", "password") # campi da cifrare nei profili
# ---------------------------------------------------------------------------
# Accesso settings (import circolare evitato con import lazy)
# ---------------------------------------------------------------------------
def _load_settings() -> dict:
import config_manager
return config_manager.load_settings()
def _save_settings(s: dict):
import config_manager
config_manager.save_settings(s)
# ---------------------------------------------------------------------------
# API pubblica — stato
# ---------------------------------------------------------------------------
def is_enabled() -> bool:
"""Restituisce True se la cifratura è attiva (salt presente in settings)."""
s = _load_settings()
return bool(s.get("crypto", {}).get("enabled", False))
def is_unlocked() -> bool:
"""Restituisce True se la chiave è in memoria (app sbloccata)."""
with _lock:
return _KEY is not None
# ---------------------------------------------------------------------------
# Derivazione chiave
# ---------------------------------------------------------------------------
def _derive_key(password: str, salt: bytes) -> bytes:
"""Deriva una chiave Fernet a 32 byte da password + salt via PBKDF2."""
Fernet, InvalidToken, PBKDF2HMAC, hashes = _get_fernet()
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=480_000,
)
raw = kdf.derive(password.encode("utf-8"))
return base64.urlsafe_b64encode(raw) # formato atteso da Fernet
def _get_salt() -> bytes:
"""Legge il salt da settings (deve esistere se cifratura abilitata)."""
s = _load_settings()
salt_b64 = s.get("crypto", {}).get("salt", "")
if not salt_b64:
raise ValueError("Salt non trovato in settings — cifratura non configurata")
return base64.b64decode(salt_b64)
# ---------------------------------------------------------------------------
# API pubblica — setup / unlock / lock
# ---------------------------------------------------------------------------
def setup(password: str):
"""
Configura la cifratura per la prima volta (o dopo disable()).
Genera un nuovo salt, deriva la chiave, salva la configurazione.
NON cifra i profili esistenti — lo fa config_manager dopo.
"""
global _KEY
Fernet, _, __, ___ = _get_fernet()
salt = secrets.token_bytes(32)
key = _derive_key(password, salt)
f = Fernet(key)
# Canary casuale: previene attacchi a dizionario offline su plaintext noto
canary = secrets.token_bytes(32)
verify_data = b"pcm-verify:" + canary
s = _load_settings()
s["crypto"] = {
"enabled": True,
"salt": base64.b64encode(salt).decode("ascii"),
"canary": base64.b64encode(canary).decode("ascii"),
"verify": f.encrypt(verify_data).decode("ascii"),
}
_save_settings(s)
with _lock:
_KEY = key
def unlock(password: str) -> bool:
"""
Tenta di sbloccare con la password fornita.
Restituisce True se la password è corretta, False altrimenti.
"""
global _KEY
try:
Fernet, InvalidToken, _, __ = _get_fernet()
salt = _get_salt()
key = _derive_key(password, salt)
# Verifica la password decifrando il token di verifica
s = _load_settings()
verify_token = s.get("crypto", {}).get("verify", "")
if not verify_token:
return False
f = Fernet(key)
decrypted = f.decrypt(verify_token.encode("ascii"))
# Supporta sia il formato vecchio (b"pcm-verify") che il nuovo (b"pcm-verify:" + canary)
canary_b64 = s.get("crypto", {}).get("canary", "")
if canary_b64:
canary = base64.b64decode(canary_b64)
if decrypted != b"pcm-verify:" + canary:
return False
else:
# Compatibilità con installazioni precedenti senza canary
if decrypted != b"pcm-verify":
return False
with _lock:
_KEY = key
return True
except Exception:
return False
def lock():
"""Rimuove la chiave dalla memoria (blocca l'app)."""
global _KEY
with _lock:
_KEY = None
def change_password(old_password: str, new_password: str) -> bool:
"""
Cambia la password master:
1. Verifica la vecchia password
2. Decifra tutti i profili
3. Genera nuovo salt, deriva nuova chiave
4. Ricifra tutti i profili
Restituisce True se riuscito.
"""
global _KEY
import config_manager
if not unlock(old_password):
return False
# Decifra tutti i profili con la vecchia chiave
profili = config_manager.load_profiles() # già decifrati da load_profiles
# Configura nuova cifratura
setup(new_password) # genera nuovo salt, nuova chiave, aggiorna settings
# Ricifra e salva
config_manager.save_profiles(profili) # save_profiles cifra con la nuova chiave
return True
def disable(password: str) -> bool:
"""
Disabilita la cifratura:
1. Verifica la password
2. Decifra tutti i profili
3. Salva in chiaro
4. Rimuove configurazione crypto da settings
Restituisce True se riuscito.
"""
global _KEY
import config_manager
if not unlock(password):
return False
# Decifra tutti i profili
profili = config_manager.load_profiles() # load_profiles decifra
# Rimuovi cifratura da settings
s = _load_settings()
s.pop("crypto", None)
_save_settings(s)
with _lock:
_KEY = None
# Salva profili in chiaro (save_profiles ora vede is_enabled()==False)
config_manager.save_profiles(profili)
return True
# ---------------------------------------------------------------------------
# API pubblica — cifratura/decifratura campi singoli
# ---------------------------------------------------------------------------
def encrypt_field(value: str) -> str:
"""
Cifra un singolo valore stringa.
Restituisce "ENC:<base64>" oppure il valore originale se la chiave
non è disponibile o il valore è già cifrato.
"""
if not value or value.startswith(_ENC_PREFIX):
return value
with _lock:
key = _KEY
if key is None:
return value
Fernet, _, __, ___ = _get_fernet()
f = Fernet(key)
token = f.encrypt(value.encode("utf-8")).decode("ascii")
return _ENC_PREFIX + token
def decrypt_field(value: str) -> str:
"""
Decifra un singolo valore stringa.
Restituisce il testo in chiaro, oppure il valore originale se non cifrato
o se la chiave non è disponibile.
"""
if not value or not value.startswith(_ENC_PREFIX):
return value
with _lock:
key = _KEY
if key is None:
return ""
try:
Fernet, InvalidToken, _, __ = _get_fernet()
f = Fernet(key)
token = value[len(_ENC_PREFIX):].encode("ascii")
return f.decrypt(token).decode("utf-8")
except Exception:
return ""
# ---------------------------------------------------------------------------
# API pubblica — cifratura/decifratura profili interi
# ---------------------------------------------------------------------------
def encrypt_profile(profilo: dict) -> dict:
"""
Restituisce una copia del profilo con i campi sensibili cifrati.
Opera solo se la cifratura è abilitata e sbloccata.
"""
if not is_enabled() or not is_unlocked():
return profilo
result = dict(profilo)
for campo in _FIELDS_TO_ENCRYPT:
if campo in result:
result[campo] = encrypt_field(str(result[campo]))
return result
def decrypt_profile(profilo: dict) -> dict:
"""
Restituisce una copia del profilo con i campi sensibili in chiaro.
Opera solo se la cifratura è abilitata (sbloccata o meno).
"""
if not is_enabled():
return profilo
result = dict(profilo)
for campo in _FIELDS_TO_ENCRYPT:
if campo in result:
result[campo] = decrypt_field(str(result[campo]))
return result