@@ -295,13 +295,26 @@ def validate_password_stream(
295295 raise BackupEmpty
296296
297297
298+ def _get_expected_archives (backup : AgentBackup ) -> set [str ]:
299+ """Get the expected archives in the backup."""
300+ expected_archives = set ()
301+ if backup .homeassistant_included :
302+ expected_archives .add ("homeassistant" )
303+ for addon in backup .addons :
304+ expected_archives .add (addon .slug )
305+ for folder in backup .folders :
306+ expected_archives .add (folder .value )
307+ return expected_archives
308+
309+
298310def decrypt_backup (
311+ backup : AgentBackup ,
299312 input_stream : IO [bytes ],
300313 output_stream : IO [bytes ],
301314 password : str | None ,
302315 on_done : Callable [[Exception | None ], None ],
303316 minimum_size : int ,
304- nonces : list [ bytes ] ,
317+ nonces : NonceGenerator ,
305318) -> None :
306319 """Decrypt a backup."""
307320 error : Exception | None = None
@@ -315,7 +328,7 @@ def decrypt_backup(
315328 fileobj = output_stream , mode = "w|" , bufsize = BUF_SIZE
316329 ) as output_tar ,
317330 ):
318- _decrypt_backup (input_tar , output_tar , password )
331+ _decrypt_backup (backup , input_tar , output_tar , password )
319332 except (DecryptError , SecureTarError , tarfile .TarError ) as err :
320333 LOGGER .warning ("Error decrypting backup: %s" , err )
321334 error = err
@@ -333,15 +346,18 @@ def decrypt_backup(
333346
334347
335348def _decrypt_backup (
349+ backup : AgentBackup ,
336350 input_tar : tarfile .TarFile ,
337351 output_tar : tarfile .TarFile ,
338352 password : str | None ,
339353) -> None :
340354 """Decrypt a backup."""
355+ expected_archives = _get_expected_archives (backup )
341356 for obj in input_tar :
342357 # We compare with PurePath to avoid issues with different path separators,
343358 # for example when backup.json is added as "./backup.json"
344- if PurePath (obj .name ) == PurePath ("backup.json" ):
359+ object_path = PurePath (obj .name )
360+ if object_path == PurePath ("backup.json" ):
345361 # Rewrite the backup.json file to indicate that the backup is decrypted
346362 if not (reader := input_tar .extractfile (obj )):
347363 raise DecryptError
@@ -352,7 +368,13 @@ def _decrypt_backup(
352368 metadata_obj .size = len (updated_metadata_b )
353369 output_tar .addfile (metadata_obj , BytesIO (updated_metadata_b ))
354370 continue
355- if not obj .name .endswith ((".tar" , ".tgz" , ".tar.gz" )):
371+ prefix , _ , suffix = object_path .name .partition ("." )
372+ if suffix not in ("tar" , "tgz" , "tar.gz" ):
373+ LOGGER .debug ("Unknown file %s will not be decrypted" , obj .name )
374+ output_tar .addfile (obj , input_tar .extractfile (obj ))
375+ continue
376+ if prefix not in expected_archives :
377+ LOGGER .debug ("Unknown inner tar file %s will not be decrypted" , obj .name )
356378 output_tar .addfile (obj , input_tar .extractfile (obj ))
357379 continue
358380 istf = SecureTarFile (
@@ -371,12 +393,13 @@ def _decrypt_backup(
371393
372394
373395def encrypt_backup (
396+ backup : AgentBackup ,
374397 input_stream : IO [bytes ],
375398 output_stream : IO [bytes ],
376399 password : str | None ,
377400 on_done : Callable [[Exception | None ], None ],
378401 minimum_size : int ,
379- nonces : list [ bytes ] ,
402+ nonces : NonceGenerator ,
380403) -> None :
381404 """Encrypt a backup."""
382405 error : Exception | None = None
@@ -390,7 +413,7 @@ def encrypt_backup(
390413 fileobj = output_stream , mode = "w|" , bufsize = BUF_SIZE
391414 ) as output_tar ,
392415 ):
393- _encrypt_backup (input_tar , output_tar , password , nonces )
416+ _encrypt_backup (backup , input_tar , output_tar , password , nonces )
394417 except (EncryptError , SecureTarError , tarfile .TarError ) as err :
395418 LOGGER .warning ("Error encrypting backup: %s" , err )
396419 error = err
@@ -408,17 +431,20 @@ def encrypt_backup(
408431
409432
410433def _encrypt_backup (
434+ backup : AgentBackup ,
411435 input_tar : tarfile .TarFile ,
412436 output_tar : tarfile .TarFile ,
413437 password : str | None ,
414- nonces : list [ bytes ] ,
438+ nonces : NonceGenerator ,
415439) -> None :
416440 """Encrypt a backup."""
417441 inner_tar_idx = 0
442+ expected_archives = _get_expected_archives (backup )
418443 for obj in input_tar :
419444 # We compare with PurePath to avoid issues with different path separators,
420445 # for example when backup.json is added as "./backup.json"
421- if PurePath (obj .name ) == PurePath ("backup.json" ):
446+ object_path = PurePath (obj .name )
447+ if object_path == PurePath ("backup.json" ):
422448 # Rewrite the backup.json file to indicate that the backup is encrypted
423449 if not (reader := input_tar .extractfile (obj )):
424450 raise EncryptError
@@ -429,16 +455,21 @@ def _encrypt_backup(
429455 metadata_obj .size = len (updated_metadata_b )
430456 output_tar .addfile (metadata_obj , BytesIO (updated_metadata_b ))
431457 continue
432- if not obj .name .endswith ((".tar" , ".tgz" , ".tar.gz" )):
458+ prefix , _ , suffix = object_path .name .partition ("." )
459+ if suffix not in ("tar" , "tgz" , "tar.gz" ):
460+ LOGGER .debug ("Unknown file %s will not be encrypted" , obj .name )
433461 output_tar .addfile (obj , input_tar .extractfile (obj ))
434462 continue
463+ if prefix not in expected_archives :
464+ LOGGER .debug ("Unknown inner tar file %s will not be encrypted" , obj .name )
465+ continue
435466 istf = SecureTarFile (
436467 None , # Not used
437468 gzip = False ,
438469 key = password_to_key (password ) if password is not None else None ,
439470 mode = "r" ,
440471 fileobj = input_tar .extractfile (obj ),
441- nonce = nonces [ inner_tar_idx ] ,
472+ nonce = nonces . get ( inner_tar_idx ) ,
442473 )
443474 inner_tar_idx += 1
444475 with istf .encrypt (obj ) as encrypted :
@@ -456,17 +487,33 @@ class _CipherWorkerStatus:
456487 writer : AsyncIteratorWriter
457488
458489
490+ class NonceGenerator :
491+ """Generate nonces for encryption."""
492+
493+ def __init__ (self ) -> None :
494+ """Initialize the generator."""
495+ self ._nonces : dict [int , bytes ] = {}
496+
497+ def get (self , index : int ) -> bytes :
498+ """Get a nonce for the given index."""
499+ if index not in self ._nonces :
500+ # Generate a new nonce for the given index
501+ self ._nonces [index ] = os .urandom (16 )
502+ return self ._nonces [index ]
503+
504+
459505class _CipherBackupStreamer :
460506 """Encrypt or decrypt a backup."""
461507
462508 _cipher_func : Callable [
463509 [
510+ AgentBackup ,
464511 IO [bytes ],
465512 IO [bytes ],
466513 str | None ,
467514 Callable [[Exception | None ], None ],
468515 int ,
469- list [ bytes ] ,
516+ NonceGenerator ,
470517 ],
471518 None ,
472519 ]
@@ -484,7 +531,7 @@ def __init__(
484531 self ._hass = hass
485532 self ._open_stream = open_stream
486533 self ._password = password
487- self ._nonces : list [ bytes ] = []
534+ self ._nonces = NonceGenerator ()
488535
489536 def size (self ) -> int :
490537 """Return the maximum size of the decrypted or encrypted backup."""
@@ -508,7 +555,15 @@ def on_done(error: Exception | None) -> None:
508555 writer = AsyncIteratorWriter (self ._hass )
509556 worker = threading .Thread (
510557 target = self ._cipher_func ,
511- args = [reader , writer , self ._password , on_done , self .size (), self ._nonces ],
558+ args = [
559+ self ._backup ,
560+ reader ,
561+ writer ,
562+ self ._password ,
563+ on_done ,
564+ self .size (),
565+ self ._nonces ,
566+ ],
512567 )
513568 worker_status = _CipherWorkerStatus (
514569 done = asyncio .Event (), reader = reader , thread = worker , writer = writer
@@ -538,17 +593,6 @@ def backup(self) -> AgentBackup:
538593class EncryptedBackupStreamer (_CipherBackupStreamer ):
539594 """Encrypt a backup."""
540595
541- def __init__ (
542- self ,
543- hass : HomeAssistant ,
544- backup : AgentBackup ,
545- open_stream : Callable [[], Coroutine [Any , Any , AsyncIterator [bytes ]]],
546- password : str | None ,
547- ) -> None :
548- """Initialize."""
549- super ().__init__ (hass , backup , open_stream , password )
550- self ._nonces = [os .urandom (16 ) for _ in range (self ._num_tar_files ())]
551-
552596 _cipher_func = staticmethod (encrypt_backup )
553597
554598 def backup (self ) -> AgentBackup :
0 commit comments