1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +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:
Stefan Agner
2026-03-25 19:10:58 +01:00
committed by GitHub
parent a73157e739
commit 4d7bd49d2c
6 changed files with 47 additions and 15 deletions

View File

@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
"home-assistant_v2.db-wal",
]
SECURETAR_CREATE_VERSION = 2
SECURETAR_CREATE_VERSION = 3

View File

@@ -3542,7 +3542,7 @@ async def test_initiate_backup_per_agent_encryption(
await hass.async_block_till_done()
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()

View File

@@ -5,10 +5,13 @@ from __future__ import annotations
import asyncio
from collections.abc import AsyncIterator
import dataclasses
import hashlib
import os
from pathlib import Path
import tarfile
from unittest.mock import Mock, patch
import nacl.bindings.crypto_secretstream as nss
import pytest
import securetar
@@ -25,6 +28,32 @@ from homeassistant.core import HomeAssistant
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(
("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"),
],
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"),
],
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]:
return send_backup()
# Patch os.urandom to return values matching the nonce used in the encrypted
# test backup. The backup has three inner tar files, but we need an extra nonce
# for a future planned supervisor.tar.
with patch("os.urandom") as mock_randbytes:
mock_randbytes.side_effect = (
bytes.fromhex("bd34ea6fc93b0614ce7af2b44b4f3957"),
bytes.fromhex("1296d6f7554e2cb629a3dc4082bae36c"),
bytes.fromhex("8b7a58e48faf2efb23845eb3164382e0"),
bytes.fromhex("00000000000000000000000000000000"),
)
# Patch os.urandom for deterministic key derivation salts, and patch
# crypto_secretstream init_push to use os.urandom for the stream header
# instead of libsodium's internal CSPRNG.
with (
patch("os.urandom", side_effect=_make_deterministic_urandom()),
patch(
"nacl.bindings.crypto_secretstream"
".crypto_secretstream_xchacha20poly1305_init_push",
side_effect=_deterministic_init_push,
),
):
encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2")
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(
"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(
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),