Streaming File Checksums With SHA-256

Compute file checksums in fixed-size chunks, support alternate hashlib algorithms, and verify expected digests in constant time.

PythonData Integrity

Code

snippet
from __future__ import annotations

import hashlib
import hmac
from pathlib import Path


class ChecksumError(ValueError):
    """Raised when a checksum cannot be computed."""


def stream_checksum(
    path: str | Path,
    *,
    algorithm: str = "sha256",
    chunk_size: int = 1024 * 1024,
) -> str:
    if not isinstance(chunk_size, int) or isinstance(chunk_size, bool):
        raise TypeError("chunk_size must be an integer")
    if chunk_size < 1:
        raise ValueError("chunk_size must be positive")

    try:
        digest = hashlib.new(algorithm)
    except ValueError as error:
        raise ChecksumError(f"unsupported algorithm: {algorithm}") from error

    file_path = Path(path)
    try:
        with file_path.open("rb") as handle:
            while chunk := handle.read(chunk_size):
                digest.update(chunk)
    except OSError as error:
        raise ChecksumError(f"cannot read {file_path}: {error}") from error
    return digest.hexdigest()


def verify_checksum(
    path: str | Path,
    expected: str,
    *,
    algorithm: str = "sha256",
    chunk_size: int = 1024 * 1024,
) -> bool:
    if not isinstance(expected, str) or not expected.strip():
        raise ValueError("expected checksum must be a non-empty string")
    actual = stream_checksum(
        path,
        algorithm=algorithm,
        chunk_size=chunk_size,
    )
    return hmac.compare_digest(actual, expected.strip().lower())


def main() -> None:
    import tempfile

    with tempfile.TemporaryDirectory() as directory:
        path = Path(directory) / "payload.bin"
        path.write_bytes(b"devexamples" * 1000)

        digest = stream_checksum(path, chunk_size=256)
        print(digest)
        print(verify_checksum(path, digest))
        print(verify_checksum(path, "0" * 64))

        try:
            stream_checksum(path, algorithm="not-a-hash")
        except ChecksumError as error:
            print(error)


if __name__ == "__main__":
    main()