@@ -149,6 +149,86 @@ def decrypt_cbc(ciphertext: bytes, token: bytes) -> bytes:
149149 return unpad (decipher .decrypt (ciphertext ), AES .block_size )
150150 return ciphertext
151151
152+ @staticmethod
153+ def _l01_key (local_key : str , timestamp : int ) -> bytes :
154+ """Derive key for L01 protocol."""
155+ hash_input = Utils .encode_timestamp (timestamp ) + Utils .ensure_bytes (local_key ) + SALT
156+ return hashlib .sha256 (hash_input ).digest ()
157+
158+ @staticmethod
159+ def _l01_iv (timestamp : int , nonce : int , sequence : int ) -> bytes :
160+ """Derive IV for L01 protocol."""
161+ digest_input = sequence .to_bytes (4 , "big" ) + nonce .to_bytes (4 , "big" ) + timestamp .to_bytes (4 , "big" )
162+ digest = hashlib .sha256 (digest_input ).digest ()
163+ return digest [:12 ]
164+
165+ @staticmethod
166+ def _l01_aad (timestamp : int , nonce : int , sequence : int , connect_nonce : int , ack_nonce : int ) -> bytes :
167+ """Derive AAD for L01 protocol."""
168+ return (
169+ sequence .to_bytes (4 , "big" )
170+ + connect_nonce .to_bytes (4 , "big" )
171+ + ack_nonce .to_bytes (4 , "big" )
172+ + nonce .to_bytes (4 , "big" )
173+ + timestamp .to_bytes (4 , "big" )
174+ )
175+
176+ @staticmethod
177+ def encrypt_gcm_l01 (
178+ plaintext : bytes ,
179+ local_key : str ,
180+ timestamp : int ,
181+ sequence : int ,
182+ nonce : int ,
183+ connect_nonce : int ,
184+ ack_nonce : int ,
185+ ) -> bytes :
186+ """Encrypt plaintext for L01 protocol using AES-256-GCM."""
187+ if not isinstance (plaintext , bytes ):
188+ raise TypeError ("plaintext requires bytes" )
189+
190+ key = Utils ._l01_key (local_key , timestamp )
191+ iv = Utils ._l01_iv (timestamp , nonce , sequence )
192+ aad = Utils ._l01_aad (timestamp , nonce , sequence , connect_nonce , ack_nonce )
193+
194+ cipher = AES .new (key , AES .MODE_GCM , nonce = iv )
195+ cipher .update (aad )
196+ ciphertext , tag = cipher .encrypt_and_digest (plaintext )
197+
198+ return ciphertext + tag
199+
200+ @staticmethod
201+ def decrypt_gcm_l01 (
202+ payload : bytes ,
203+ local_key : str ,
204+ timestamp : int ,
205+ sequence : int ,
206+ nonce : int ,
207+ connect_nonce : int ,
208+ ack_nonce : int ,
209+ ) -> bytes :
210+ """Decrypt payload for L01 protocol using AES-256-GCM."""
211+ if not isinstance (payload , bytes ):
212+ raise TypeError ("payload requires bytes" )
213+
214+ key = Utils ._l01_key (local_key , timestamp )
215+ iv = Utils ._l01_iv (timestamp , nonce , sequence )
216+ aad = Utils ._l01_aad (timestamp , nonce , sequence , connect_nonce , ack_nonce )
217+
218+ if len (payload ) < 16 :
219+ raise ValueError ("Invalid payload length for GCM decryption" )
220+
221+ tag = payload [- 16 :]
222+ ciphertext = payload [:- 16 ]
223+
224+ cipher = AES .new (key , AES .MODE_GCM , nonce = iv )
225+ cipher .update (aad )
226+
227+ try :
228+ return cipher .decrypt_and_verify (ciphertext , tag )
229+ except ValueError as e :
230+ raise RoborockException ("GCM tag verification failed" ) from e
231+
152232 @staticmethod
153233 def crc (data : bytes ) -> int :
154234 """Gather bytes for checksum calculation."""
0 commit comments