From 365fef6659292565e7e033861518b6b5832b7da6 Mon Sep 17 00:00:00 2001 From: dbl-0 Date: Tue, 15 Sep 2026 23:18:56 -0600 Subject: [PATCH] Write the backup store with private=True so the backup password is not world-readable (#182259) Co-authored-by: Claude Opus 5 (1M context) --- homeassistant/components/backup/store.py | 7 +- .../backup/snapshots/test_store.ambr | 152 ++++++++++++++++-- .../backup/snapshots/test_websocket.ambr | 32 ++-- tests/components/backup/test_store.py | 51 ++++++ 4 files changed, 211 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 6215c88574ff..b8c350f5aa1b 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: STORE_DELAY_SAVE = 30 STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 7 +STORAGE_VERSION_MINOR = 8 class StoredBackupData(TypedDict): @@ -39,6 +39,8 @@ class _BackupStore(Store[StoredBackupData]): STORAGE_KEY, max_readable_version=self._MAX_READABLE_VERSION, minor_version=STORAGE_VERSION_MINOR, + # The store persists the automatic backup password in plaintext. + private=True, ) @override @@ -85,6 +87,9 @@ class _BackupStore(Store[StoredBackupData]): for backup in data["backups"]: backup["failed_addons"] = [] backup["failed_folders"] = [] + # Version 1.8 does not change the data. The minor version was bumped + # so that stores written before the store was made private are + # rewritten once, with mode 0600, on the first load. # Note: We allow reading data with major version 2 in which the unused key # data["config"]["schedule"]["state"] will be removed. The bump to 2 is diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index b82bb7c650fa..d2f1b45934be 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -44,7 +44,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -93,7 +93,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -142,7 +142,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -191,7 +191,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -250,7 +250,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -309,7 +309,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -362,7 +362,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -415,7 +415,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -468,7 +468,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -521,7 +521,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -577,7 +577,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -633,7 +633,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -695,7 +695,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -757,7 +757,131 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data7] + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), + 'failed_agent_ids': list([ + 'test.remote', + ]), + 'failed_folders': list([ + 'ssl', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + }), + }), + 'automatic_backups_configured': True, + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': 'hunter2', + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 8, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data7].1 + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), + 'failed_agent_ids': list([ + 'test.remote', + ]), + 'failed_folders': list([ + 'ssl', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + }), + }), + 'automatic_backups_configured': True, + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': 'hunter2', + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 8, 'version': 1, }) # --- diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 2b1d7399a7fd..2fd9453aef29 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -1330,7 +1330,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1446,7 +1446,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1562,7 +1562,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1692,7 +1692,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1969,7 +1969,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2083,7 +2083,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2197,7 +2197,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2313,7 +2313,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2431,7 +2431,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2547,7 +2547,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2667,7 +2667,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2791,7 +2791,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2907,7 +2907,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -3023,7 +3023,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -3139,7 +3139,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -3255,7 +3255,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index a016ab36f3d7..63e7cbb55045 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -302,6 +302,57 @@ def mock_delay_save() -> Generator[None]: "minor_version": 7, "version": 1, }, + { + "data": { + "backups": [ + { + "backup_id": "abc123", + "failed_addons": [ + { + "name": "Test add-on", + "slug": "test_addon", + "version": "1.0.0", + } + ], + "failed_agent_ids": ["test.remote"], + "failed_folders": ["ssl"], + } + ], + "config": { + "agents": { + "test.remote": { + "protected": True, + "retention": {"copies": None, "days": None}, + } + }, + "automatic_backups_configured": True, + "create_backup": { + "agent_ids": [], + "include_addons": None, + "include_all_addons": False, + "include_database": True, + "include_folders": None, + "name": None, + "password": "hunter2", + }, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "retention": { + "copies": None, + "days": None, + }, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "minor_version": 8, + "version": 1, + }, ], ) async def test_store_migration(