@@ -295,13 +295,26 @@ def validate_password_stream(
295
295
raise BackupEmpty
296
296
297
297
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
+
298
310
def decrypt_backup (
311
+ backup : AgentBackup ,
299
312
input_stream : IO [bytes ],
300
313
output_stream : IO [bytes ],
301
314
password : str | None ,
302
315
on_done : Callable [[Exception | None ], None ],
303
316
minimum_size : int ,
304
- nonces : list [ bytes ] ,
317
+ nonces : NonceGenerator ,
305
318
) -> None :
306
319
"""Decrypt a backup."""
307
320
error : Exception | None = None
@@ -315,7 +328,7 @@ def decrypt_backup(
315
328
fileobj = output_stream , mode = "w|" , bufsize = BUF_SIZE
316
329
) as output_tar ,
317
330
):
318
- _decrypt_backup (input_tar , output_tar , password )
331
+ _decrypt_backup (backup , input_tar , output_tar , password )
319
332
except (DecryptError , SecureTarError , tarfile .TarError ) as err :
320
333
LOGGER .warning ("Error decrypting backup: %s" , err )
321
334
error = err
@@ -333,15 +346,18 @@ def decrypt_backup(
333
346
334
347
335
348
def _decrypt_backup (
349
+ backup : AgentBackup ,
336
350
input_tar : tarfile .TarFile ,
337
351
output_tar : tarfile .TarFile ,
338
352
password : str | None ,
339
353
) -> None :
340
354
"""Decrypt a backup."""
355
+ expected_archives = _get_expected_archives (backup )
341
356
for obj in input_tar :
342
357
# We compare with PurePath to avoid issues with different path separators,
343
358
# 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" ):
345
361
# Rewrite the backup.json file to indicate that the backup is decrypted
346
362
if not (reader := input_tar .extractfile (obj )):
347
363
raise DecryptError
@@ -352,7 +368,13 @@ def _decrypt_backup(
352
368
metadata_obj .size = len (updated_metadata_b )
353
369
output_tar .addfile (metadata_obj , BytesIO (updated_metadata_b ))
354
370
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 )
356
378
output_tar .addfile (obj , input_tar .extractfile (obj ))
357
379
continue
358
380
istf = SecureTarFile (
@@ -371,12 +393,13 @@ def _decrypt_backup(
371
393
372
394
373
395
def encrypt_backup (
396
+ backup : AgentBackup ,
374
397
input_stream : IO [bytes ],
375
398
output_stream : IO [bytes ],
376
399
password : str | None ,
377
400
on_done : Callable [[Exception | None ], None ],
378
401
minimum_size : int ,
379
- nonces : list [ bytes ] ,
402
+ nonces : NonceGenerator ,
380
403
) -> None :
381
404
"""Encrypt a backup."""
382
405
error : Exception | None = None
@@ -390,7 +413,7 @@ def encrypt_backup(
390
413
fileobj = output_stream , mode = "w|" , bufsize = BUF_SIZE
391
414
) as output_tar ,
392
415
):
393
- _encrypt_backup (input_tar , output_tar , password , nonces )
416
+ _encrypt_backup (backup , input_tar , output_tar , password , nonces )
394
417
except (EncryptError , SecureTarError , tarfile .TarError ) as err :
395
418
LOGGER .warning ("Error encrypting backup: %s" , err )
396
419
error = err
@@ -408,17 +431,20 @@ def encrypt_backup(
408
431
409
432
410
433
def _encrypt_backup (
434
+ backup : AgentBackup ,
411
435
input_tar : tarfile .TarFile ,
412
436
output_tar : tarfile .TarFile ,
413
437
password : str | None ,
414
- nonces : list [ bytes ] ,
438
+ nonces : NonceGenerator ,
415
439
) -> None :
416
440
"""Encrypt a backup."""
417
441
inner_tar_idx = 0
442
+ expected_archives = _get_expected_archives (backup )
418
443
for obj in input_tar :
419
444
# We compare with PurePath to avoid issues with different path separators,
420
445
# 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" ):
422
448
# Rewrite the backup.json file to indicate that the backup is encrypted
423
449
if not (reader := input_tar .extractfile (obj )):
424
450
raise EncryptError
@@ -429,16 +455,21 @@ def _encrypt_backup(
429
455
metadata_obj .size = len (updated_metadata_b )
430
456
output_tar .addfile (metadata_obj , BytesIO (updated_metadata_b ))
431
457
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 )
433
461
output_tar .addfile (obj , input_tar .extractfile (obj ))
434
462
continue
463
+ if prefix not in expected_archives :
464
+ LOGGER .debug ("Unknown inner tar file %s will not be encrypted" , obj .name )
465
+ continue
435
466
istf = SecureTarFile (
436
467
None , # Not used
437
468
gzip = False ,
438
469
key = password_to_key (password ) if password is not None else None ,
439
470
mode = "r" ,
440
471
fileobj = input_tar .extractfile (obj ),
441
- nonce = nonces [ inner_tar_idx ] ,
472
+ nonce = nonces . get ( inner_tar_idx ) ,
442
473
)
443
474
inner_tar_idx += 1
444
475
with istf .encrypt (obj ) as encrypted :
@@ -456,17 +487,33 @@ class _CipherWorkerStatus:
456
487
writer : AsyncIteratorWriter
457
488
458
489
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
+
459
505
class _CipherBackupStreamer :
460
506
"""Encrypt or decrypt a backup."""
461
507
462
508
_cipher_func : Callable [
463
509
[
510
+ AgentBackup ,
464
511
IO [bytes ],
465
512
IO [bytes ],
466
513
str | None ,
467
514
Callable [[Exception | None ], None ],
468
515
int ,
469
- list [ bytes ] ,
516
+ NonceGenerator ,
470
517
],
471
518
None ,
472
519
]
@@ -484,7 +531,7 @@ def __init__(
484
531
self ._hass = hass
485
532
self ._open_stream = open_stream
486
533
self ._password = password
487
- self ._nonces : list [ bytes ] = []
534
+ self ._nonces = NonceGenerator ()
488
535
489
536
def size (self ) -> int :
490
537
"""Return the maximum size of the decrypted or encrypted backup."""
@@ -508,7 +555,15 @@ def on_done(error: Exception | None) -> None:
508
555
writer = AsyncIteratorWriter (self ._hass )
509
556
worker = threading .Thread (
510
557
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
+ ],
512
567
)
513
568
worker_status = _CipherWorkerStatus (
514
569
done = asyncio .Event (), reader = reader , thread = worker , writer = writer
@@ -538,17 +593,6 @@ def backup(self) -> AgentBackup:
538
593
class EncryptedBackupStreamer (_CipherBackupStreamer ):
539
594
"""Encrypt a backup."""
540
595
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
-
552
596
_cipher_func = staticmethod (encrypt_backup )
553
597
554
598
def backup (self ) -> AgentBackup :
0 commit comments