1414import base64
1515import binascii
1616import hashlib
17+ import hmac
1718import ipaddress
1819import json
1920import math
4546FATAL_PULL_STATUSES = frozenset ({401 , 402 , 403 , 429 })
4647MAX_SYNC_TOKEN_BYTES = 8192
4748MAX_SYNC_POLICY_BYTES = 64
49+ SYNC_E2EE_PROTOCOL = "v1"
50+ # Wire framing is intentionally binary. The relay stays a blind byte store and can never
51+ # mistake an encrypted bundle for its old JSON payload format.
52+ SYNC_E2EE_MAGIC = b"engraphis-sync-e2ee-v1\x00 "
53+ SYNC_E2EE_KEY_BYTES = 32
54+ SYNC_E2EE_NONCE_BYTES = 12
55+ SYNC_E2EE_TAG_BYTES = 16
56+ SYNC_E2EE_KEY_ENV = "ENGRAPHIS_SYNC_E2EE_KEY"
4857
4958
5059class RelayError (RuntimeError ):
@@ -63,6 +72,48 @@ class RelayUnreachable(RelayError):
6372 """
6473
6574
75+ def decode_sync_e2ee_key (value : object ) -> bytes :
76+ """Decode the user-held Cloud Sync key without ever accepting a weak variant.
77+
78+ It is deliberately a URL-safe, unpadded base64 value for exactly 32 random bytes.
79+ The Cloud service never receives this value: operators provision the same value to
80+ each authorized device through their own trusted channel.
81+ """
82+ raw = str (value or "" ).strip ()
83+ if re .fullmatch (r"[A-Za-z0-9_-]{43}" , raw ) is None :
84+ raise RelayError (
85+ "Cloud Sync needs a 32-byte end-to-end encryption key in "
86+ + SYNC_E2EE_KEY_ENV ,
87+ status = 409 ,
88+ )
89+ try :
90+ key = base64 .b64decode (raw + "=" , altchars = b"-_" , validate = True )
91+ except (ValueError , binascii .Error ):
92+ raise RelayError ("Cloud Sync end-to-end encryption key is malformed" , status = 409 ) from None
93+ if len (key ) != SYNC_E2EE_KEY_BYTES :
94+ raise RelayError ("Cloud Sync end-to-end encryption key is malformed" , status = 409 )
95+ return key
96+
97+
98+ def configured_sync_e2ee_key (value : object = None ) -> bytes :
99+ """Return an explicit key or fail closed before a Cloud upload can begin."""
100+ configured = os .environ .get (SYNC_E2EE_KEY_ENV ) if value is None else value
101+ return decode_sync_e2ee_key (configured )
102+
103+
104+ def _new_e2ee_cipher (key : bytes ):
105+ """Construct the optional cryptography backend lazily, preserving a NumPy-only core."""
106+ try :
107+ from cryptography .hazmat .primitives .ciphers .aead import ChaCha20Poly1305
108+ from cryptography .exceptions import InvalidTag
109+ except ImportError :
110+ raise RelayError (
111+ "Cloud Sync encryption requires the cryptography package (Python 3.10+)" ,
112+ status = 409 ,
113+ ) from None
114+ return ChaCha20Poly1305 (key ), InvalidTag
115+
116+
66117class _NoRedirectHandler (urllib .request .HTTPRedirectHandler ):
67118 """Never forward a relay bearer credential to a redirect target."""
68119
@@ -308,6 +359,83 @@ def _safe_bundle_name(name: object) -> str:
308359 return value
309360
310361
362+ class EncryptedRelayTransport :
363+ """Client-side AEAD wrapper for the managed Cloud Sync byte relay.
364+
365+ The wrapped relay receives only an opaque deterministic bundle name and a framed
366+ ChaCha20-Poly1305 ciphertext. The key stays on authorized devices; authentication
367+ data binds each ciphertext to both its Cloud workspace and stored name, so moving,
368+ renaming, modifying, or downgrading a bundle fails closed before sync parses it.
369+ """
370+
371+ def __init__ (self , relay , key : bytes ) -> None :
372+ workspace_id = str (getattr (relay , "workspace_id" , "" ) or "" )
373+ if not workspace_id :
374+ raise ValueError ("encrypted relay transport requires a workspace-bound relay" )
375+ if not isinstance (key , (bytes , bytearray )) or len (key ) != SYNC_E2EE_KEY_BYTES :
376+ raise ValueError ("Cloud Sync encryption key must contain exactly 32 bytes" )
377+ self .relay = relay
378+ self .workspace_id = workspace_id
379+ self ._key = bytes (key )
380+ self ._cipher , self ._invalid_tag = _new_e2ee_cipher (self ._key )
381+
382+ def _opaque_name (self , name : object ) -> str :
383+ safe = _safe_bundle_name (name )
384+ if not safe :
385+ raise RelayError ("relay bundle name is invalid" )
386+ digest = hmac .new (
387+ self ._key ,
388+ b"engraphis-cloud-sync-e2ee-name-v1\x00 "
389+ + self .workspace_id .encode ("utf-8" )
390+ + b"\x00 "
391+ + safe .encode ("utf-8" ),
392+ hashlib .sha256 ,
393+ ).hexdigest ()
394+ return "e2ee-" + digest + ".json"
395+
396+ def _aad (self , stored_name : str ) -> bytes :
397+ return (
398+ b"engraphis-cloud-sync-e2ee-v1\x00 "
399+ + self .workspace_id .encode ("utf-8" )
400+ + b"\x00 "
401+ + stored_name .encode ("ascii" )
402+ )
403+
404+ def push (self , name : str , data : bytes ) -> None :
405+ if not isinstance (data , (bytes , bytearray )):
406+ raise RelayError ("relay bundle data must be bytes" )
407+ # The relay cap applies to ciphertext too. Refuse before allocating a large
408+ # encrypted copy rather than relying on the wrapped transport to reject it later.
409+ overhead = len (SYNC_E2EE_MAGIC ) + SYNC_E2EE_NONCE_BYTES + SYNC_E2EE_TAG_BYTES
410+ if len (data ) > MAX_RELAY_BUNDLE_BYTES - overhead :
411+ raise RelayError ("relay bundle exceeded the encrypted upload safety limit" )
412+ stored_name = self ._opaque_name (name )
413+ nonce = os .urandom (SYNC_E2EE_NONCE_BYTES )
414+ ciphertext = self ._cipher .encrypt (nonce , bytes (data ), self ._aad (stored_name ))
415+ self .relay .push (stored_name , SYNC_E2EE_MAGIC + nonce + ciphertext )
416+
417+ def pull (self ) -> Iterable [Tuple [str , bytes ]]:
418+ for name , data in self .relay .pull ():
419+ safe = _safe_bundle_name (name )
420+ if not safe or not isinstance (data , (bytes , bytearray )):
421+ raise RelayError ("relay returned an invalid encrypted bundle" )
422+ raw = bytes (data )
423+ if not raw .startswith (SYNC_E2EE_MAGIC ):
424+ raise RelayError ("relay bundle requires end-to-end encryption" )
425+ payload = raw [len (SYNC_E2EE_MAGIC ):]
426+ if len (payload ) < SYNC_E2EE_NONCE_BYTES + SYNC_E2EE_TAG_BYTES :
427+ raise RelayError ("bundle could not be authenticated" )
428+ nonce , ciphertext = payload [:SYNC_E2EE_NONCE_BYTES ], payload [SYNC_E2EE_NONCE_BYTES :]
429+ try :
430+ plaintext = self ._cipher .decrypt (nonce , ciphertext , self ._aad (safe ))
431+ except self ._invalid_tag :
432+ raise RelayError ("bundle could not be authenticated" ) from None
433+ yield safe , plaintext
434+
435+ def list_names (self ) -> List [str ]:
436+ return self .relay .list_names ()
437+
438+
311439class RelayTransport :
312440 """A ``SyncTransport`` backed by the customer sync relay.
313441
0 commit comments