66"""
77
88import hashlib
9+ import hmac
910import json
1011import secrets
1112import time
@@ -74,10 +75,18 @@ def create(cls, name: Optional[str] = None) -> "Identity":
7475 )
7576
7677 def sign (self , data : bytes ) -> bytes :
77- """Sign data with this identity's private key."""
78- # Simplified signature for demo - in production use Ed25519
79- signature_input = self ._private_key .encode () + data
80- return hashlib .sha256 (signature_input ).digest ()
78+ """Sign data with this identity's private key using HMAC-SHA256."""
79+ # Use HMAC for secure message authentication in fallback mode
80+ return hmac .new (
81+ bytes .fromhex (self ._private_key ),
82+ data ,
83+ hashlib .sha256
84+ ).digest ()
85+
86+ def verify_signature (self , data : bytes , signature : bytes ) -> bool :
87+ """Verify a signature created by this identity."""
88+ expected = self .sign (data )
89+ return hmac .compare_digest (expected , signature )
8190
8291 def export (self ) -> str :
8392 """Export identity as JSON."""
@@ -116,6 +125,7 @@ class Content:
116125 body : str
117126 created_at : int
118127 signature : str
128+ _author_private_key : str = field (default = "" , repr = False )
119129
120130 @classmethod
121131 def create (cls , body : str , identity : Identity ) -> "Content" :
@@ -137,13 +147,32 @@ def create(cls, body: str, identity: Identity) -> "Content":
137147 body = body ,
138148 created_at = created_at ,
139149 signature = signature .hex (),
150+ _author_private_key = identity ._private_key ,
140151 )
141152
142153 def verify (self ) -> bool :
143- """Verify the content's signature."""
144- # Simplified verification for demo
145- # In production, verify Ed25519 signature
146- return len (self .signature ) == 64 # SHA256 hex length
154+ """Verify the content's signature using HMAC verification."""
155+ if not self ._author_private_key :
156+ # Cannot verify without private key (signature was created elsewhere)
157+ # In production, use public-key cryptography (Ed25519)
158+ return len (self .signature ) == 64 # Basic length check
159+
160+ # Reconstruct signature payload
161+ signature_payload = f"{ self .cid } :{ self .author } :{ self .body } :{ self .created_at } " .encode ()
162+
163+ # Compute expected signature using HMAC
164+ expected_signature = hmac .new (
165+ bytes .fromhex (self ._author_private_key ),
166+ signature_payload ,
167+ hashlib .sha256
168+ ).digest ()
169+
170+ # Compare signatures in constant time
171+ try :
172+ actual_signature = bytes .fromhex (self .signature )
173+ return hmac .compare_digest (expected_signature , actual_signature )
174+ except ValueError :
175+ return False
147176
148177 def export (self ) -> str :
149178 """Export content as JSON."""
@@ -163,6 +192,8 @@ class EncryptedMessage:
163192 sender_public_key : str
164193 recipient_public_key : str
165194 ciphertext : str
195+ nonce : str # Added nonce for proper encryption
196+ mac : str # Added MAC for authentication
166197 timestamp : int
167198 message_id : str
168199
@@ -172,56 +203,64 @@ class Messaging:
172203 End-to-end encrypted messaging for RootlessNet.
173204
174205 Provides secure message encryption and decryption using
175- X25519 key exchange and XChaCha20-Poly1305 .
206+ authenticated encryption with HMAC for message integrity .
176207 """
177208
178209 @staticmethod
179- def _derive_shared_secret (sender_public_key : str , recipient_public_key : str ) -> bytes :
180- """Derive a symmetric shared secret from both public keys."""
181- # Use both public keys to derive shared secret (available on both sides)
182- # Sort keys to ensure same derivation order on both ends
210+ def _derive_keys (sender_public_key : str , recipient_public_key : str ) -> tuple [bytes , bytes ]:
211+ """Derive encryption and authentication keys from public keys."""
212+ # Derive base secret
183213 keys_combined = sender_public_key + recipient_public_key
184- shared_secret = hashlib .sha256 (keys_combined .encode ()).digest ()
185- return shared_secret
214+ base_secret = hashlib .sha256 (keys_combined .encode ()).digest ()
215+
216+ # Derive separate encryption and MAC keys using HKDF-like expansion
217+ enc_key = hashlib .sha256 (base_secret + b"encryption" ).digest ()
218+ mac_key = hashlib .sha256 (base_secret + b"authentication" ).digest ()
219+
220+ return enc_key , mac_key
186221
187222 @staticmethod
188223 def encrypt (
189224 message : str ,
190225 sender : Identity ,
191226 recipient_public_key : str ,
192227 ) -> str :
193- """Encrypt a message for a recipient."""
228+ """Encrypt a message for a recipient with authentication ."""
194229 if RUST_BACKEND_AVAILABLE and _rust_core :
195230 return _rust_core .encrypt_message (message , sender , recipient_public_key )
196231
197- # Pure Python fallback (simplified)
198232 timestamp = int (time .time ())
199233
200- # Derive shared secret using both public keys
201- key_bytes = Messaging . _derive_shared_secret ( sender . public_key , recipient_public_key )
234+ # Generate random nonce
235+ nonce = secrets . token_bytes ( 16 )
202236
203- # XOR encryption for demo (use real encryption in production)
237+ # Derive encryption and MAC keys
238+ enc_key , mac_key = Messaging ._derive_keys (sender .public_key , recipient_public_key )
239+
240+ # XOR encryption with nonce-derived keystream (CTR-like mode)
204241 message_bytes = message .encode ()
205- # Extend key to message length
206- extended_key = (key_bytes * ((len (message_bytes ) // 32 ) + 1 ))[:len (message_bytes )]
207- encrypted = bytes (m ^ k for m , k in zip (message_bytes , extended_key ))
242+ keystream = b""
243+ counter = 0
244+ while len (keystream ) < len (message_bytes ):
245+ block = hashlib .sha256 (enc_key + nonce + counter .to_bytes (4 , 'big' )).digest ()
246+ keystream += block
247+ counter += 1
208248
209- message_id = hashlib . sha256 ( f" { message } : { timestamp } " . encode ()). hexdigest ()[: 16 ]
249+ ciphertext = bytes ( m ^ k for m , k in zip ( message_bytes , keystream [: len ( message_bytes )]))
210250
211- msg = EncryptedMessage (
212- sender_public_key = sender .public_key ,
213- recipient_public_key = recipient_public_key ,
214- ciphertext = encrypted .hex (),
215- timestamp = timestamp ,
216- message_id = message_id ,
217- )
251+ # Compute MAC over ciphertext for authentication
252+ mac = hmac .new (mac_key , nonce + ciphertext , hashlib .sha256 ).digest ()
253+
254+ message_id = hashlib .sha256 (f"{ message } :{ timestamp } " .encode ()).hexdigest ()[:16 ]
218255
219256 return json .dumps ({
220- "sender_public_key" : msg .sender_public_key ,
221- "recipient_public_key" : msg .recipient_public_key ,
222- "ciphertext" : msg .ciphertext ,
223- "timestamp" : msg .timestamp ,
224- "message_id" : msg .message_id ,
257+ "sender_public_key" : sender .public_key ,
258+ "recipient_public_key" : recipient_public_key ,
259+ "ciphertext" : ciphertext .hex (),
260+ "nonce" : nonce .hex (),
261+ "mac" : mac .hex (),
262+ "timestamp" : timestamp ,
263+ "message_id" : message_id ,
225264 })
226265
227266 @staticmethod
@@ -230,20 +269,37 @@ def decrypt(
230269 recipient : Identity ,
231270 sender_public_key : str ,
232271 ) -> str :
233- """Decrypt a message from a sender."""
272+ """Decrypt a message from a sender with authentication verification ."""
234273 if RUST_BACKEND_AVAILABLE and _rust_core :
235274 return _rust_core .decrypt_message (encrypted_message , recipient , sender_public_key )
236275
237- # Pure Python fallback (simplified)
238276 msg_data = json .loads (encrypted_message )
239277
240- # Derive shared secret using both public keys (same as encryption)
241- key_bytes = Messaging ._derive_shared_secret (sender_public_key , recipient .public_key )
278+ # Verify sender matches
279+ if msg_data ["sender_public_key" ] != sender_public_key :
280+ raise ValueError ("Sender public key mismatch" )
281+
282+ # Derive keys
283+ enc_key , mac_key = Messaging ._derive_keys (sender_public_key , recipient .public_key )
242284
243- # XOR decryption for demo
285+ # Parse message components
244286 ciphertext = bytes .fromhex (msg_data ["ciphertext" ])
245- # Extend key to ciphertext length
246- extended_key = (key_bytes * ((len (ciphertext ) // 32 ) + 1 ))[:len (ciphertext )]
247- decrypted = bytes (c ^ k for c , k in zip (ciphertext , extended_key ))
287+ nonce = bytes .fromhex (msg_data ["nonce" ])
288+ received_mac = bytes .fromhex (msg_data ["mac" ])
289+
290+ # Verify MAC (authenticate before decrypting)
291+ expected_mac = hmac .new (mac_key , nonce + ciphertext , hashlib .sha256 ).digest ()
292+ if not hmac .compare_digest (expected_mac , received_mac ):
293+ raise ValueError ("Message authentication failed - message may have been tampered" )
294+
295+ # Decrypt using CTR-like mode
296+ keystream = b""
297+ counter = 0
298+ while len (keystream ) < len (ciphertext ):
299+ block = hashlib .sha256 (enc_key + nonce + counter .to_bytes (4 , 'big' )).digest ()
300+ keystream += block
301+ counter += 1
302+
303+ plaintext = bytes (c ^ k for c , k in zip (ciphertext , keystream [:len (ciphertext )]))
248304
249- return decrypted .decode ()
305+ return plaintext .decode ()
0 commit comments