Skip to main content

Secure Tar

Secure Tarfile library

SecureTar is a streaming wrapper around Python's tarfile module. It creates and reads tar archives that contain inner tar files, which can be encrypted. It is the archive format used for Home Assistant backups.

Archive layout

A SecureTar archive is a plain, uncompressed outer tar in PAX format. Its members are either inner tar files, optionally gzip compressed and optionally encrypted, or plain metadata files such as backup.json that are added to the outer tar directly.

Encrypted inner tar files start with a SecureTar header followed by the ciphertext. Two header versions can be written:

Version Cipher Key derivation
2 AES-128-CBC 100 rounds of SHA-256 (legacy)
3 XChaCha20-Poly1305 secretstream Argon2id, per-file BLAKE2b subkeys

Version 3 is the default. It authenticates the data, detects truncation, and allows the password to be checked without decrypting the whole file. Version 1 (AES-128-CBC without a header) can still be read.

Creating an archive

from pathlib import Path

from securetar import SecureTarArchive, atomic_contents_add

with SecureTarArchive(Path("backup.tar"), "w", password="hunter2") as archive:
    with archive.create_tar("./homeassistant.tar.gz", gzip=True) as inner_tar:
        atomic_contents_add(
            inner_tar,
            Path("/config"),
            file_filter=lambda path: path.name == "home-assistant.log",
            arcname=".",
        )

    with archive.create_tar("./share.tar.gz", gzip=True) as inner_tar:
        atomic_contents_add(
            inner_tar,
            Path("/share"),
            file_filter=lambda _: False,
            arcname=".",
        )

create_tar returns a context manager that yields a regular tarfile.TarFile. Everything written to it is streamed into the outer tar; no temporary files are needed. The inner tar is encrypted when the archive was opened with a password or a root_key_context. Leave both out to create a plain archive.

atomic_contents_add recursively adds a directory. The file_filter callback receives the archive-relative PurePath of each item and returns True to exclude it. Errors while adding a file raise AddFileError, which carries the offending path.

Pass create_version=2 to SecureTarArchive to write the legacy AES format.

Reading an archive

Iterate the outer tar to find the inner tar files, then use extract_tar to get a decrypted stream, or wrap a member in SecureTarFile to read the inner tar directly:

from pathlib import Path

from securetar import SecureTarArchive, SecureTarFile

with SecureTarArchive(Path("backup.tar"), "r", password="hunter2") as archive:
    for member in archive.tar:
        if not member.name.endswith(".tar.gz"):
            continue

        # Decrypted bytes of the inner tar
        with archive.extract_tar(member) as decrypted:
            data = decrypted.read(1024 * 1024)

        # Or open the inner tar and extract its contents
        fileobj = archive.tar.extractfile(member)
        with SecureTarFile(fileobj=fileobj, password="hunter2") as inner_tar:
            inner_tar.extractall(Path("/restore"), filter="data")

Encrypted inner tar files are opened in stream mode because the ciphertext is not seekable. Random access to members, such as getmember followed by extractfile, is therefore not possible; read members sequentially.

A wrong password raises InvalidPasswordError for version 3 files. For version 1 and 2 files, which carry no key check, a wrong password raises SecureTarReadError once the decrypted data turns out not to be a tar.

Validating

SecureTarArchive.validate_password(member) checks the password against an encrypted inner tar without reading all of it. SecureTarArchive.validate(member) additionally decrypts the whole member, which for version 3 verifies integrity and detects truncation. Both consume the member's stream. SecureTarFile offers the same two methods for a single inner tar.

Re-encrypting existing tar files

import_tar encrypts an existing plaintext tar and adds it to the archive without writing to disk. The source TarInfo must have size set.

with (
    SecureTarArchive(Path("plain.tar"), "r") as source,
    SecureTarArchive(Path("encrypted.tar"), "w", password="hunter2") as target,
):
    for member in source.tar:
        target.import_tar(source.tar.extractfile(member), member)

get_archive_max_ciphertext_size(plaintext_size, version, number_of_inner_tar_files) returns an upper bound for the encrypted archive size, useful when the size has to be announced before the data is streamed.

Sharing key derivation between archives

Deriving the version 3 root key with Argon2id is deliberately slow. A SecureTarRootKeyContext performs that derivation once and can be passed to several SecureTarArchive instances instead of a password.

Passing the same derived_key_id to create_tar or import_tar reuses the per-file key and nonce, so importing the same plaintext tar into two archives produces byte-identical ciphertext. Reusing a key and nonce for different plaintext breaks the encryption, so an ID must identify exactly one plaintext. Tar member names are not guaranteed to be unique, so the example below keys on the member's position as well. Use this only when identical output is required, for example to upload one backup to several locations; otherwise leave derived_key_id unset so every inner tar gets a fresh key.

from securetar import SecureTarArchive, SecureTarRootKeyContext

root_key_context = SecureTarRootKeyContext("hunter2")

with SecureTarArchive(Path("plain.tar"), "r") as source:
    for name in ("backup1.tar", "backup2.tar"):
        with SecureTarArchive(Path(name), "w", root_key_context=root_key_context) as target:
            for index, member in enumerate(source.tar):
                target.import_tar(
                    source.tar.extractfile(member),
                    member,
                    derived_key_id=(index, member.name),
                )

Metadata

Release files for securetar 2026.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for securetar 2026.10.0
File Size Uploaded
securetar-2026.10.0.tar.gz 32.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for securetar 2026.10.0
File Interpreter ABI Platform
securetar-2026.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 53.9 kB

Release files / securetar-2026.10.0.tar.gz

Download URL securetar-2026.10.0.tar.gz
Size 32.6 kB
Tags Source
SHA-256 checksum
How to use checksums
580e07109bd5c759fbb25b7dca02bd875db2978c33eb27a8538930d6a802a5d3
BLAKE2b-256 checksum
How to use checksums
4dd3350f1bc86ebaf45e644452fb1c8bc44855ac84b8f2f8e5338ff41b6dbf21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release files / securetar-2026.10.0-py3-none-any.whl

Download URL securetar-2026.10.0-py3-none-any.whl
Size 21.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0cc81a15b09fab5c1b878ca5ea330033bde444c80919b564185535ff006d17fa
BLAKE2b-256 checksum
How to use checksums
8f3667135f8ae21a15c6d4bc40b2a8c89372829a9bc0a789c974c01ca5318f3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page