diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json index b1b6e6f70c6..ffaed260c88 100644 --- a/homeassistant/components/backup/manifest.json +++ b/homeassistant/components/backup/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "calculated", "quality_scale": "internal", - "requirements": ["cronsim==2.6", "securetar==2025.1.2"] + "requirements": ["cronsim==2.6", "securetar==2025.1.3"] } diff --git a/homeassistant/components/backup/util.py b/homeassistant/components/backup/util.py index 55f3c3c05c7..ac1525b7d69 100644 --- a/homeassistant/components/backup/util.py +++ b/homeassistant/components/backup/util.py @@ -13,13 +13,7 @@ import tarfile from typing import IO, Self, cast import aiohttp -from securetar import ( - PLAINTEXT_SIZE_HEADER, - VERSION_HEADER, - SecureTarError, - SecureTarFile, - SecureTarReadError, -) +from securetar import SecureTarError, SecureTarFile, SecureTarReadError from homeassistant.backup_restore import password_to_key from homeassistant.core import HomeAssistant @@ -205,8 +199,6 @@ def validate_password_stream( for obj in input_tar: if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): continue - if obj.pax_headers.get(VERSION_HEADER) != "2.0": - raise UnsupportedSecureTarVersion istf = SecureTarFile( None, # Not used gzip=False, @@ -215,6 +207,8 @@ def validate_password_stream( fileobj=input_tar.extractfile(obj), ) with istf.decrypt(obj) as decrypted: + if istf.securetar_header.plaintext_size is None: + raise UnsupportedSecureTarVersion try: decrypted.read(1) # Read a single byte to trigger the decryption except SecureTarReadError as err: @@ -270,10 +264,6 @@ def _decrypt_backup( if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): output_tar.addfile(obj, input_tar.extractfile(obj)) continue - if obj.pax_headers.get(VERSION_HEADER) != "2.0": - raise UnsupportedSecureTarVersion - decrypted_obj = copy.deepcopy(obj) - decrypted_obj.size = int(obj.pax_headers[PLAINTEXT_SIZE_HEADER]) istf = SecureTarFile( None, # Not used gzip=False, @@ -282,6 +272,10 @@ def _decrypt_backup( fileobj=input_tar.extractfile(obj), ) with istf.decrypt(obj) as decrypted: + if (plaintext_size := istf.securetar_header.plaintext_size) is None: + raise UnsupportedSecureTarVersion + decrypted_obj = copy.deepcopy(obj) + decrypted_obj.size = plaintext_size output_tar.addfile(decrypted_obj, decrypted) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b994679780..6f7fe970cc9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ PyTurboJPEG==1.7.5 pyudev==0.24.1 PyYAML==6.0.2 requests==2.32.3 -securetar==2025.1.2 +securetar==2025.1.3 SQLAlchemy==2.0.36 standard-aifc==3.13.0;python_version>='3.13' standard-telnetlib==3.13.0;python_version>='3.13' diff --git a/pyproject.toml b/pyproject.toml index 0bb5ad7ea8d..406fbe6cf25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ "python-slugify==8.0.4", "PyYAML==6.0.2", "requests==2.32.3", - "securetar==2025.1.2", + "securetar==2025.1.3", "SQLAlchemy==2.0.36", "standard-aifc==3.13.0;python_version>='3.13'", "standard-telnetlib==3.13.0;python_version>='3.13'", diff --git a/requirements.txt b/requirements.txt index 7f12eb14274..52e1b412803 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ psutil-home-assistant==0.0.1 python-slugify==8.0.4 PyYAML==6.0.2 requests==2.32.3 -securetar==2025.1.2 +securetar==2025.1.3 SQLAlchemy==2.0.36 standard-aifc==3.13.0;python_version>='3.13' standard-telnetlib==3.13.0;python_version>='3.13' diff --git a/requirements_all.txt b/requirements_all.txt index 78525134731..d6d9b583d94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2665,7 +2665,7 @@ screenlogicpy==0.10.0 scsgate==0.1.0 # homeassistant.components.backup -securetar==2025.1.2 +securetar==2025.1.3 # homeassistant.components.sendgrid sendgrid==6.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2413d5ed95..4a8dfe35756 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2147,7 +2147,7 @@ sanix==1.0.6 screenlogicpy==0.10.0 # homeassistant.components.backup -securetar==2025.1.2 +securetar==2025.1.3 # homeassistant.components.emulated_kasa # homeassistant.components.sense diff --git a/tests/components/backup/fixtures/test_backups/ed1608a9.tar b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar similarity index 66% rename from tests/components/backup/fixtures/test_backups/ed1608a9.tar rename to tests/components/backup/fixtures/test_backups/c0cb53bd.tar index fc928b16d1b..f3b2845d5eb 100644 Binary files a/tests/components/backup/fixtures/test_backups/ed1608a9.tar and b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar differ diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index ac4e77fca41..41e6c574227 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -186,7 +186,7 @@ 'type': 'result', }) # --- -# name: test_can_decrypt_on_download[backup.local-ed1608a9-hunter2] +# name: test_can_decrypt_on_download[backup.local-c0cb53bd-hunter2] dict({ 'id': 1, 'result': None, @@ -194,7 +194,7 @@ 'type': 'result', }) # --- -# name: test_can_decrypt_on_download[backup.local-ed1608a9-wrong_password] +# name: test_can_decrypt_on_download[backup.local-c0cb53bd-wrong_password] dict({ 'error': dict({ 'code': 'password_incorrect', @@ -216,7 +216,7 @@ 'type': 'result', }) # --- -# name: test_can_decrypt_on_download[no_such_agent-ed1608a9-hunter2] +# name: test_can_decrypt_on_download[no_such_agent-c0cb53bd-hunter2] dict({ 'error': dict({ 'code': 'home_assistant_error', diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index 693434631b9..b7b86cc1d45 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -106,14 +106,14 @@ async def test_downloading_remote_encrypted_backup( hass_client: ClientSessionGenerator, ) -> None: """Test downloading a local backup file.""" - backup_path = get_fixture_path("test_backups/ed1608a9.tar", DOMAIN) + backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) await setup_backup_integration(hass) hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest( "test", [ AgentBackup( addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], - backup_id="ed1608a9", + backup_id="c0cb53bd", database_included=True, date="1970-01-01T00:00:00Z", extra_metadata={}, @@ -141,7 +141,7 @@ async def _test_downloading_encrypted_backup( """Test downloading an encrypted backup file.""" # Try downloading without supplying a password client = await hass_client() - resp = await client.get(f"/api/backup/download/ed1608a9?agent_id={agent_id}") + resp = await client.get(f"/api/backup/download/c0cb53bd?agent_id={agent_id}") assert resp.status == 200 backup = await resp.read() # We expect a valid outer tar file, but the inner tar file is encrypted and @@ -158,7 +158,7 @@ async def _test_downloading_encrypted_backup( # Download with the wrong password resp = await client.get( - f"/api/backup/download/ed1608a9?agent_id={agent_id}&password=wrong" + f"/api/backup/download/c0cb53bd?agent_id={agent_id}&password=wrong" ) assert resp.status == 200 backup = await resp.read() @@ -171,7 +171,7 @@ async def _test_downloading_encrypted_backup( # Finally download with the correct password resp = await client.get( - f"/api/backup/download/ed1608a9?agent_id={agent_id}&password=hunter2" + f"/api/backup/download/c0cb53bd?agent_id={agent_id}&password=hunter2" ) assert resp.status == 200 backup = await resp.read() diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 2aa6eca3b95..fe6c0c1f679 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -2560,13 +2560,13 @@ async def test_subscribe_event( ("agent_id", "backup_id", "password"), [ # Invalid agent or backup - ("no_such_agent", "ed1608a9", "hunter2"), + ("no_such_agent", "c0cb53bd", "hunter2"), ("backup.local", "no_such_backup", "hunter2"), # Legacy backup, which can't be streamed ("backup.local", "2bcb3113", "hunter2"), # New backup, which can be streamed, try with correct and wrong password - ("backup.local", "ed1608a9", "hunter2"), - ("backup.local", "ed1608a9", "wrong_password"), + ("backup.local", "c0cb53bd", "hunter2"), + ("backup.local", "c0cb53bd", "wrong_password"), ], ) @pytest.mark.usefixtures("mock_backups")