forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathencryption.py
More file actions
142 lines (108 loc) · 4.26 KB
/
Copy pathencryption.py
File metadata and controls
142 lines (108 loc) · 4.26 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
import base64
import os
import struct
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
import logging
logger = logging.getLogger(__name__)
# Load the master secret from environment variables. This must be a securely managed 32-byte key.
ENCRYPTION_SECRET = os.getenv('ENCRYPTION_SECRET', '').encode('utf-8')
if not ENCRYPTION_SECRET or len(ENCRYPTION_SECRET) < 32:
raise ValueError(
"ENCRYPTION_SECRET environment variable not set or is too short. " "It must be a securely managed 32-byte key."
)
def derive_key(uid: str) -> bytes:
"""
Derives a user-specific 32-byte key from the master secret and user ID (salt).
"""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=uid.encode('utf-8'),
info=b'user-data-encryption',
)
return hkdf.derive(ENCRYPTION_SECRET)
def encrypt(data: str, uid: str) -> str:
"""
Encrypts a string using a user-specific key.
Returns a base64 encoded string containing nonce + ciphertext + tag.
"""
if not data:
return data
key = derive_key(uid)
aesgcm = AESGCM(key)
nonce = os.urandom(12) # GCM standard nonce size
# Data must be bytes
plaintext_bytes = data.encode('utf-8')
ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
# Combine nonce and ciphertext for storage
encrypted_payload = nonce + ciphertext
return base64.b64encode(encrypted_payload).decode('utf-8')
def decrypt(encrypted_data: str, uid: str) -> str:
"""
Decrypts a base64 encoded string using a user-specific key.
"""
if not encrypted_data:
return encrypted_data
try:
key = derive_key(uid)
aesgcm = AESGCM(key)
encrypted_payload = base64.b64decode(encrypted_data.encode('utf-8'))
# Extract nonce and ciphertext
nonce = encrypted_payload[:12]
ciphertext = encrypted_payload[12:]
decrypted_bytes = aesgcm.decrypt(nonce, ciphertext, None)
return decrypted_bytes.decode('utf-8')
except Exception as e:
# If decryption fails (e.g., wrong key, corrupted data), return the original encrypted data
# to avoid data loss and to make debugging easier. In a production system, you might want
# to log this error.
logger.error(f"Decryption failed for user {uid}: {e}")
return encrypted_data
def encrypt_audio_chunk(data: bytes, uid: str) -> bytes:
"""
Encrypt audio chunk and return length-prefixed binary format.
Format: [4 bytes length][12 bytes nonce][ciphertext + tag]
This format allows concatenating multiple encrypted chunks without decryption.
"""
key = derive_key(uid)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
# Encrypt (includes authentication tag)
ciphertext = aesgcm.encrypt(nonce, data, None)
# Combine nonce + ciphertext
encrypted_payload = nonce + ciphertext
# Add length prefix (4 bytes, big-endian)
length = len(encrypted_payload)
return struct.pack('>I', length) + encrypted_payload
def decrypt_audio_chunk(encrypted_data: bytes, uid: str, offset: int = 0):
"""
Decrypt a single length-prefixed chunk.
Returns: (decrypted_data, bytes_consumed)
"""
# Read length prefix
length = struct.unpack('>I', encrypted_data[offset : offset + 4])[0]
offset += 4
# Extract encrypted payload
encrypted_payload = encrypted_data[offset : offset + length]
# Extract nonce and ciphertext
nonce = encrypted_payload[:12]
ciphertext = encrypted_payload[12:]
# Decrypt
key = derive_key(uid)
aesgcm = AESGCM(key)
decrypted = aesgcm.decrypt(nonce, ciphertext, None)
return decrypted, 4 + length
def decrypt_audio_file(encrypted_data: bytes, uid: str) -> bytes:
"""
Decrypt an entire merged audio file (multiple concatenated chunks).
Each chunk is length-prefixed, allowing simple concatenation during merge.
"""
decrypted_audio = bytearray()
offset = 0
while offset < len(encrypted_data):
chunk_data, bytes_consumed = decrypt_audio_chunk(encrypted_data, uid, offset)
decrypted_audio.extend(chunk_data)
offset += bytes_consumed
return bytes(decrypted_audio)