mirror of
https://github.com/home-assistant/core.git
synced 2026-04-17 23:53:49 +01:00
Make SecureTar v3 the default for backup creation (#166272)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
|
|||||||
"home-assistant_v2.db-wal",
|
"home-assistant_v2.db-wal",
|
||||||
]
|
]
|
||||||
|
|
||||||
SECURETAR_CREATE_VERSION = 2
|
SECURETAR_CREATE_VERSION = 3
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -3542,7 +3542,7 @@ async def test_initiate_backup_per_agent_encryption(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_secure_tar_archive.mock_calls[0] == call(
|
assert mock_secure_tar_archive.mock_calls[0] == call(
|
||||||
ANY, ANY, "w", bufsize=4194304, create_version=2, password=inner_tar_password
|
ANY, ANY, "w", bufsize=4194304, create_version=3, password=inner_tar_password
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await ws_client.receive_json()
|
result = await ws_client.receive_json()
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tarfile
|
import tarfile
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import nacl.bindings.crypto_secretstream as nss
|
||||||
import pytest
|
import pytest
|
||||||
import securetar
|
import securetar
|
||||||
|
|
||||||
@@ -25,6 +28,32 @@ from homeassistant.core import HomeAssistant
|
|||||||
from tests.common import get_fixture_path
|
from tests.common import get_fixture_path
|
||||||
|
|
||||||
|
|
||||||
|
def _deterministic_init_push(
|
||||||
|
state: nss.crypto_secretstream_xchacha20poly1305_state, key: bytes
|
||||||
|
) -> bytes:
|
||||||
|
"""Replace init_push with init_pull + deterministic header from os.urandom.
|
||||||
|
|
||||||
|
libsodium's init_push generates random bytes internally, bypassing os.urandom.
|
||||||
|
This replacement generates the header via os.urandom (which can be patched for
|
||||||
|
deterministic tests) and uses init_pull to correctly initialize the state.
|
||||||
|
"""
|
||||||
|
header = os.urandom(nss.crypto_secretstream_xchacha20poly1305_HEADERBYTES)
|
||||||
|
nss.crypto_secretstream_xchacha20poly1305_init_pull(state, header, key)
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def _make_deterministic_urandom() -> callable:
|
||||||
|
"""Create a deterministic os.urandom replacement."""
|
||||||
|
call_idx = 0
|
||||||
|
|
||||||
|
def deterministic_urandom(n: int) -> bytes:
|
||||||
|
nonlocal call_idx
|
||||||
|
call_idx += 1
|
||||||
|
return hashlib.sha256(f"deterministic-{call_idx}".encode()).digest()[:n]
|
||||||
|
|
||||||
|
return deterministic_urandom
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("backup_json_content", "expected_backup"),
|
("backup_json_content", "expected_backup"),
|
||||||
[
|
[
|
||||||
@@ -432,14 +461,14 @@ async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) ->
|
|||||||
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
|
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
|
||||||
],
|
],
|
||||||
40960, # 4 x 10240 byte of padding
|
40960, # 4 x 10240 byte of padding
|
||||||
"test_backups/c0cb53bd.tar",
|
"test_backups/c0cb53bd.tar.encrypted_v3",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
||||||
],
|
],
|
||||||
30720, # 3 x 10240 byte of padding
|
30720, # 3 x 10240 byte of padding
|
||||||
"test_backups/c0cb53bd.tar.encrypted_skip_core2",
|
"test_backups/c0cb53bd.tar.encrypted_v3_skip_core2",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -477,16 +506,17 @@ async def test_encrypted_backup_streamer(
|
|||||||
async def open_backup() -> AsyncIterator[bytes]:
|
async def open_backup() -> AsyncIterator[bytes]:
|
||||||
return send_backup()
|
return send_backup()
|
||||||
|
|
||||||
# Patch os.urandom to return values matching the nonce used in the encrypted
|
# Patch os.urandom for deterministic key derivation salts, and patch
|
||||||
# test backup. The backup has three inner tar files, but we need an extra nonce
|
# crypto_secretstream init_push to use os.urandom for the stream header
|
||||||
# for a future planned supervisor.tar.
|
# instead of libsodium's internal CSPRNG.
|
||||||
with patch("os.urandom") as mock_randbytes:
|
with (
|
||||||
mock_randbytes.side_effect = (
|
patch("os.urandom", side_effect=_make_deterministic_urandom()),
|
||||||
bytes.fromhex("bd34ea6fc93b0614ce7af2b44b4f3957"),
|
patch(
|
||||||
bytes.fromhex("1296d6f7554e2cb629a3dc4082bae36c"),
|
"nacl.bindings.crypto_secretstream"
|
||||||
bytes.fromhex("8b7a58e48faf2efb23845eb3164382e0"),
|
".crypto_secretstream_xchacha20poly1305_init_push",
|
||||||
bytes.fromhex("00000000000000000000000000000000"),
|
side_effect=_deterministic_init_push,
|
||||||
)
|
),
|
||||||
|
):
|
||||||
encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2")
|
encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2")
|
||||||
|
|
||||||
assert encryptor.backup() == dataclasses.replace(
|
assert encryptor.backup() == dataclasses.replace(
|
||||||
@@ -587,7 +617,9 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No
|
|||||||
decrypted_backup_path = get_fixture_path(
|
decrypted_backup_path = get_fixture_path(
|
||||||
"test_backups/c0cb53bd.tar.decrypted", DOMAIN
|
"test_backups/c0cb53bd.tar.decrypted", DOMAIN
|
||||||
)
|
)
|
||||||
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
|
encrypted_backup_path = get_fixture_path(
|
||||||
|
"test_backups/c0cb53bd.tar.encrypted_v3", DOMAIN
|
||||||
|
)
|
||||||
backup = AgentBackup(
|
backup = AgentBackup(
|
||||||
addons=[
|
addons=[
|
||||||
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
||||||
|
|||||||
Reference in New Issue
Block a user