-
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) · 3.5 KB
/
encryption.py
File metadata and controls
130 lines (97 loc) · 3.5 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
"""
AES encryption for sensitive database values.
"""
import os
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import logging
logger = logging.getLogger(__name__)
# Encryption key derivation salt (stored in database config on first run)
_encryption_key = None
def _derive_key(master_secret: str, salt: bytes) -> bytes:
"""Derive encryption key from master secret using PBKDF2."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
backend=default_backend()
)
return kdf.derive(master_secret.encode())
def initialize_encryption(master_secret: str, salt: bytes | None = None) -> bytes:
"""
Initialize encryption system with master secret.
Args:
master_secret: Master password/secret for key derivation
salt: Optional salt (will be generated if not provided)
Returns:
Salt used for key derivation (must be stored)
"""
global _encryption_key
if salt is None:
salt = os.urandom(16)
_encryption_key = _derive_key(master_secret, salt)
logger.info("Encryption system initialized")
return salt
def encrypt(plaintext: str) -> str:
"""
Encrypt plaintext string using AES-256-GCM.
Returns base64-encoded: nonce(12) + ciphertext + tag(16)
"""
if _encryption_key is None:
raise RuntimeError("Encryption not initialized. Call initialize_encryption() first.")
if not plaintext:
return ""
# Generate random nonce (12 bytes for GCM)
nonce = os.urandom(12)
# Create cipher
cipher = Cipher(
algorithms.AES(_encryption_key),
modes.GCM(nonce),
backend=default_backend()
)
encryptor = cipher.encryptor()
# Encrypt
ciphertext = encryptor.update(plaintext.encode()) + encryptor.finalize()
# Combine nonce + ciphertext + tag
encrypted_data = nonce + ciphertext + encryptor.tag
# Base64 encode for storage
return base64.b64encode(encrypted_data).decode('utf-8')
def decrypt(encrypted: str) -> str:
"""
Decrypt base64-encoded encrypted string.
Args:
encrypted: Base64-encoded nonce + ciphertext + tag
Returns:
Decrypted plaintext
"""
if _encryption_key is None:
raise RuntimeError("Encryption not initialized. Call initialize_encryption() first.")
if not encrypted:
return ""
try:
# Decode from base64
encrypted_data = base64.b64decode(encrypted)
# Extract components
nonce = encrypted_data[:12]
tag = encrypted_data[-16:]
ciphertext = encrypted_data[12:-16]
# Create cipher
cipher = Cipher(
algorithms.AES(_encryption_key),
modes.GCM(nonce, tag),
backend=default_backend()
)
decryptor = cipher.decryptor()
# Decrypt
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext.decode('utf-8')
except Exception as e:
logger.error(f"Decryption failed: {e}")
raise ValueError("Failed to decrypt data. Key may be incorrect.")
def is_initialized() -> bool:
"""Check if encryption system is initialized."""
return _encryption_key is not None