SecureStream 256 Pro — v2.3 • Offline AES-256 Encryption

v2.3.0 • 100% offline · AES-256-GCM · Argon2id

SecureStream 256 Pro

SecureStream 256 Pro is a portable, fully offline file-encryption tool. It protects data with authenticated AES-256-GCM encryption with keys derived via Argon2id, handles very large files through streaming, lets you hide the original name and extension, and runs entirely locally. It provides safe I/O with atomic writes, optional backups, a clear progress indicator with cancel support, and SHA-256 hashes to verify downloaded releases. The app uses only standard, audited cryptographic building blocks (AES-GCM, Argon2id, HKDF) — no custom primitives.

📦 Full documentation (User Manual, Datasheet, EULA) is included in the ZIP.

Key capabilities

ENCV2 + SGCM2 + SGCM2F

Header + streamed AEAD frames with a footer MAC and BLAKE2s digest for whole-file integrity.

Per-chunk AEAD

Unique nonce per chunk (prefix + counter), AAD binds header context and sizes; HKDF domain separation per file.

Metadata hiding

Original name/extension stored only inside ciphertext (EMETA2); minimal header.

Durable, secure I/O

Private .tmp_secure, atomic fsync + replace, Unicode-safe paths; resilient to crashes and partial writes.

Screenshot — SecureStream 256 Pro

Quick start

  1. Select files/folders.
  2. Choose password (Argon2id) or a 32-byte key file (.key).
  3. Optional: Hide metadata, Backup, filters.
  4. Adjust Argon2id and chunk size if needed (stored in file; decryption uses value from file).
  5. Encrypt / Decrypt and monitor progress.

Performance & limits

  • Balanced default: 8 MiB chunks. Larger chunks = higher throughput (SSD/NVMe); smaller chunks = lower RAM usage.
  • Configured plaintext cap: 2 TiB; chunk size range: 1–256 MiB (stored in file).

Verification & requirements

  • Windows 10/11 x64 (min.); ≥8 GB RAM recommended for m=128 MiB
  • Release integrity (SHA-256)
    # PowerShell (Windows) Get-FileHash -Algorithm SHA256 "C:\path\securestream256pro_en_v2.3.0.zip"
    SHA-256 (EN v2.3.0): a1c6e8244b3c702c3b777ebc8e01d169337706c00ab24794a962e7cf28a1d096 securestream256pro_en_v2.3.0.zip
    SHA-256 (PL v2.3.0): be9aa575933c436a8f34ffc2d74aacb12a88c0eee89d3fe190d82c3b31219325 securestream256pro_pl_v2.3.0.zip

Technical basis: AES-256-GCM (AEAD per chunk), Argon2id KDF, domain-separated HKDF labels (AEAD/KCV/FILEKEY/FILEMAC), KCV, metadata hiding, atomic write with fsync & replace, Unicode-safe I/O, fully offline. No custom cryptographic primitives are implemented.

Release notes

v2.3.0 — Stabilization release

  • Smaller EXE: ~48 MB → 27 MB (-44%).
  • Faster startup: slimmed-down Qt package (one-file mode).
  • Runtime: Python 3.13.9 (from 3.10).
  • Reproducible build: dedicated venv + hard-pinned dependencies.
  • Functional changes: none.

v2.2.2 — Source hygiene & precise exclusions

  • .key policy: “foreign” key files are handled like regular files. After a successful encryption, their plaintext is removed from the source folder. The currently used key remains globally excluded.
  • Read-only resilience: before deleting sources, the tool clears the read-only attribute, ensuring reliable cleanup on Windows/SMB without leaving tombstones.
  • Log exclusion: the active log securestream256pro.log and its rotations (.log.1.log.3) are skipped during scanning to avoid accidental encryption.
  • Consistent cleanup flow: after encrypt/decrypt, inputs are moved to .tmp_secure and permanently removed (tomb → chmod → unlink), eliminating plaintext leftovers.

v2.2.1 — Stability & filtering refinements

  • File-filter logic: improved precision in should_process_for_encrypt() and should_process_for_decrypt(); refined handling of runtime paths and exclusion rules to prevent accidental self-processing of the active executable.
  • POSIX/Windows parity: unified path-resolution behaviour for consistent filtering across systems.
  • Security hardening: subtle runtime guard against encryption attempts inside temporary PyInstaller runtime folders (_MEIPASS).

v2.2.0 — Maintenance & migration

  • GUI framework: migrated from PyQt6 to PySide6 (Qt6, LGPL). Functionality and UI remain unchanged.
  • Cleanup & hardening: internal tidying, clearer error reporting, and more robust cleanup of temporary files; minor polish of progress/status messaging.
  • Packaging: still a single portable EXE; size may differ slightly depending on Qt components.

v2.1.0 — Service update

  • Clean cancel with Backup (Decrypt): cancelling no longer leaves temporary files or partial backups.
  • Unified semantics: Backup is created only after a fully successful operation for both Encrypt and Decrypt.
  • Minor code tidy-ups: a few small issues flagged by linters were removed; the test suite remained green.

v2.0.0 — New v2 series

  • New v2 file model: streamed AES-256-GCM with a unique nonce per chunk; the ENCV2 header is bound as AAD; subkeys are separated via HKDF and a per-file AEAD is derived using HKDF with BLAKE2s(header || nonce_prefix).
  • Footer (SGCM2F): includes total_chunks, total_plain, a BLAKE2s(32) digest over all ciphertext, and a file-level HMAC-SHA256 (truncated to 32 B) over common AAD plus totals and digest; verified before the atomic replace to disk.
  • Configured limits: plaintext cap 2 TiB; chunk size 1–256 MiB (stored in file); precise byte-level progress for large files.

Test suite

Show test summary

PASSED tests/test_argon_params.py::test_argon_params_bounds_ok
PASSED tests/test_argon_params.py::test_argon_params_out_of_range
PASSED tests/test_argon_params_extremes.py::test_argon_minimum_params_roundtrip
PASSED tests/test_argon_params_extremes.py::test_argon_params_rejected[0-262144-6]
PASSED tests/test_argon_params_extremes.py::test_argon_params_rejected[5-32767-6]
PASSED tests/test_argon_params_extremes.py::test_argon_params_rejected[5-1048577-6]
PASSED tests/test_argon_params_extremes.py::test_argon_params_rejected[5-262144-0]
PASSED tests/test_argon_params_extremes.py::test_argon_params_rejected[11-262144-6]
PASSED tests/test_argon_params_extremes.py::test_argon_params_rejected[5-262144-17]
PASSED tests/test_backup_filters.py::test_collect_encrypt_filters
PASSED tests/test_backup_filters.py::test_collect_decrypt_filters
PASSED tests/test_backup_filters.py::test_backup_excluded
PASSED tests/test_backup_mode.py::test_backup_created_on_encrypt_and_excluded_from_scan
PASSED tests/test_backup_semantics.py::test_decrypt_wrong_password_does_not_create_backup
PASSED tests/test_backup_semantics.py::test_decrypt_cancel_does_not_create_backup
PASSED tests/test_backup_tmp_cleanup_on_cancel.py::test_backup_tmp_cleanup_on_cancel
PASSED tests/test_cancel.py::test_encrypt_cancel_early
PASSED tests/test_chunk_boundaries.py::test_chunk_boundaries_roundtrip[0]
PASSED tests/test_chunk_boundaries.py::test_chunk_boundaries_roundtrip[1024]
PASSED tests/test_chunk_boundaries.py::test_chunk_boundaries_roundtrip[1025]
PASSED tests/test_cli_like.py::test_cli_like_encrypt_decrypt_folder
PASSED tests/test_conflicting_filenames.py::test_decrypt_creates_unique_name_when_conflict
PASSED tests/test_conflicting_filenames_variants.py::test_conflict_single_creates_suffix_one
PASSED tests/test_conflicting_filenames_variants.py::test_conflict_twice_creates_suffix_two
PASSED tests/test_conflicting_filenames_variants.py::test_conflict_many_increments_until_free[3]
PASSED tests/test_conflicting_filenames_variants.py::test_conflict_many_increments_until_free[4]
PASSED tests/test_conflicting_filenames_variants.py::test_conflict_many_increments_until_free[5]
PASSED tests/test_copy_file_stream.py::test_copy_file_stream_disk_full
PASSED tests/test_crypto_header.py::test_argon2id_derivation_stable
PASSED tests/test_crypto_header.py::test_hkdf_filekey_is_32bytes
PASSED tests/test_crypto_header.py::test_subkeys_len
PASSED tests/test_crypto_header.py::test_kcv_len_and_change_on_ctx
PASSED tests/test_decrypt_finalization_semantics.py::test_decrypt_finalization_semantics_backup_like_worker
PASSED tests/test_directory_roundtrip.py::test_directory_roundtrip_plain_and_meta
PASSED tests/test_e2e_key_plaintext_and_ro_cleanup.py::test_e2e_encrypt_decrypt_obcy_key_i_ro_cleanup
PASSED tests/test_e2e_key_plaintext_and_ro_cleanup.py::test_e2e_encrypt_decrypt_backup_mode
PASSED tests/test_encrypt_cleanup.py::test_encrypt_cleanup_tmp_on_replace_error
PASSED tests/test_encrypt_decrypt_roundtrip.py::test_roundtrip_password[False]
PASSED tests/test_encrypt_decrypt_roundtrip.py::test_roundtrip_password[True]
PASSED tests/test_encrypt_decrypt_roundtrip.py::test_roundtrip_filekey
PASSED tests/test_fault_injection.py::test_truncated_footer_fails_and_leaves_no_output
PASSED tests/test_fault_injection.py::test_encrypt_failure_midstream_keeps_original_and_cleans_tmp
PASSED tests/test_fault_injection.py::test_os_replace_failure_leaves_tmp_clean_and_preserves_source
PASSED tests/test_footer_and_mac.py::test_footer_invalid_digest_raises_invalidtag
PASSED tests/test_footer_and_mac.py::test_footer_invalid_mac_raises_invalidtag
PASSED tests/test_framing.py::test_password_hide_meta_roundtrip_ok
PASSED tests/test_framing.py::test_filekey_no_meta_roundtrip_ok
PASSED tests/test_framing.py::test_corruption_detected_per_frame
PASSED tests/test_header_kcv.py::test_header_kcv_mismatch_raises_kcverror
PASSED tests/test_heavy_io.py::test_large_file_roundtrip_heavy
PASSED tests/test_heavy_io.py::test_concurrent_roundtrip_many_files
PASSED tests/test_heavy_io.py::test_ciphertext_corruption_detection_heavy
PASSED tests/test_heavy_io.py::test_truncated_ciphertext_is_rejected
PASSED tests/test_heavy_io.py::test_meta_enabled_with_longish_name_roundtrip
PASSED tests/test_import_smoke.py::test_import_program_main_smoke
PASSED tests/test_inner_meta_limits.py::test_encrypt_rejects_inner_meta_too_large
PASSED tests/test_inner_meta_limits.py::test_decrypt_raises_when_inner_meta_too_large
PASSED tests/test_kcv_logging.py::test_decrypt_wrong_password_low_level_no_logging
PASSED tests/test_kcv_logging.py::test_decrypt_wrong_password_high_level_logs_once
PASSED tests/test_key_premissions.py::test_key_permissions_after_decrypt
PASSED tests/test_logging_meta.py::test_too_large_error_is_logged
PASSED tests/test_logs_redaction.py::test_logs_redaction_basic
PASSED tests/test_long_filename.py::test_encrypt_decrypt_with_long_filename
PASSED tests/test_longrun_smoke.py::test_longrun_encrypt_decrypt_100_rounds
PASSED tests/test_meta_too_large.py::test_inner_meta_too_large
PASSED tests/test_negative_corruption.py::test_wrong_password_raises_kcv
PASSED tests/test_negative_corruption.py::test_corrupted_gcm_tag_raises
PASSED tests/test_negative_corruption.py::test_truncated_file_raises
PASSED tests/test_nonce_prefix_uniqueness.py::test_nonce_prefix_uniqueness_across_files
PASSED tests/test_paths.py::test_fit_base_preserves_extension_and_truncates
PASSED tests/test_paths.py::test_fit_base_handles_no_extension
PASSED tests/test_paths.py::test_next_nonconflicting_adds_counter_and_respects_budget
PASSED tests/test_paths.py::test_many_conflicts_sequence
PASSED tests/test_paths.py::test_ensure_within_prevents_traversal
PASSED tests/test_paths.py::test_sanitize_header_name_ext_reserved_on_windows
PASSED tests/test_paths_security.py::test_ensure_within_prevents_escape
PASSED tests/test_paths_security.py::test_ensure_tmp_secure_dir_ok
PASSED tests/test_per_file_domain.py::test_per_file_aead_derivation_and_randomization
PASSED tests/test_per_file_domain.py::test_same_content_different_files_differ
PASSED tests/test_perf_smoke.py::test_big_file_smoke
PASSED tests/test_practical_binary_and_wrong_password.py::test_binary_png_roundtrip
PASSED tests/test_practical_binary_and_wrong_password.py::test_decrypt_with_wrong_password_fails
PASSED tests/test_self_exclude_filters.py::test_encrypt_excludes_sys_executable
PASSED tests/test_self_exclude_filters.py::test_encrypt_excludes_argv0
PASSED tests/test_self_exclude_filters.py::test_encrypt_excludes_meipass_file
PASSED tests/test_self_exclude_filters.py::test_encrypt_excludes_loaded_key_path
PASSED tests/test_self_exclude_filters.py::test_encrypt_excludes_dot_enc
PASSED tests/test_self_exclude_filters.py::test_encrypt_ext_filter_allows_only_listed
PASSED tests/test_self_exclude_filters.py::test_decrypt_requires_dot_enc
PASSED tests/test_self_exclude_filters.py::test_decrypt_allows_basic_enc_without_filters
PASSED tests/test_self_exclude_filters.py::test_decrypt_respects_ext_filter_with_meta
PASSED tests/test_self_exclude_filters.py::test_decrypt_best_effort_when_no_meta
PASSED tests/test_self_exclude_filters.py::test_decrypt_excludes_meipass_file
PASSED tests/test_stream_aad.py::test_nonce_unique_per_chunk
PASSED tests/test_stream_aad.py::test_aad_change_chunk_bytes_breaks_decrypt
PASSED tests/test_stream_tamper_mutations.py::test_stream_mutations_fail[swap_two]
PASSED tests/test_stream_tamper_mutations.py::test_stream_mutations_fail[duplicate_first]
PASSED tests/test_stream_tamper_mutations.py::test_stream_mutations_fail[flip_bit]
PASSED tests/test_try_unpack_inner.py::test_try_unpack_inner_meta_is_total
PASSED tests/test_windows_hardening_extras.py::test_roundtrip_small_and_unicode[False]
PASSED tests/test_windows_hardening_extras.py::test_roundtrip_small_and_unicode[True]
PASSED tests/test_windows_hardening_extras.py::test_tmp_secure_hidden_attribute
PASSED tests/test_windows_hardening_extras.py::test_tmp_secure_acl_minimal
PASSED tests/test_windows_hardening_extras.py::test_cancel_midway_encryption_cleans_tmp
PASSED tests/test_windows_hardening_extras.py::test_footer_corruption_detected
PASSED tests/test_windows_hardening_extras.py::test_progress_monotonic
SKIPPED [1] test_conflicting_filenames_variants.py:221: API does not support out_dir – test not applicable to this variant.
SKIPPED [1] test_paths_security.py:27: POSIX-only permissions check.
SKIPPED [1] test_paths_security.py:41: Symlinks/Junctions: skipped on Windows.
SKIPPED [1] test_paths_security.py:57: Hardlink test: skipped on Windows.
        

Legacy note: the v1.x line has reached end of support.

Security & Code Audit v2.3.0

Argon2id parameter validation

def _validate_argon_params(t: int, m: int, p: int) -> None:
    """
    Validates Argon2id parameters (time, memory in KiB, parallelism).

    Args:
        t: Time cost (1..MAX_ARGON_T).
        m: Memory cost in KiB (32768..MAX_ARGON_M_KIB).
        p: Parallelism (1..MAX_ARGON_P).

    Raises:
        ValueError: If any parameter is outside the allowed range.
    """
    if not 1 <= t <= MAX_ARGON_T:
        raise ValueError(f"argon2.t out of range (1..{MAX_ARGON_T})")
    if not 32 * 1024 <= m <= MAX_ARGON_M_KIB:
        raise ValueError(f"argon2.m (KiB) out of range (32768..{MAX_ARGON_M_KIB})")
    if not 1 <= p <= MAX_ARGON_P:
        raise ValueError(f"argon2.p out of range (1..{MAX_ARGON_P})")

KDF / HKDF (master, subkeys, file-MAC)

def derive_key_argon2id(password: str, salt: bytes, t: int, m_kib: int, p: int) -> bytes:
    """
    Derives a 32-byte key from a password using Argon2id.

    Args:
        password: Password as UTF-8 text.
        salt: 16-byte salt used in the KDF.
        t: Time cost (number of Argon2id iterations).
        m_kib: Memory cost in KiB.
        p: Parallelism (number of lanes).

    Returns:
        A 32-byte derived key.

    Raises:
        RuntimeError: If the 'argon2-cffi' library is missing.
    """
    if ARGON2_HASH_RAW is None or ARGON2_TYPE is None:
        raise RuntimeError("Missing 'argon2-cffi'. Install: pip install argon2-cffi")
    return ARGON2_HASH_RAW(
        secret=password.encode(UTF8),
        salt=salt,
        time_cost=t,
        memory_cost=m_kib,
        parallelism=p,
        hash_len=32,
        type=ARGON2_TYPE.ID,
    )


def derive_subkeys(master_key: bytes, salt: bytes) -> tuple[bytes, bytes]:
    """
    Derives child keys from a master key (HKDF-SHA256).

    Args:
        master_key (bytes): Secret input material (master key).
        salt (bytes): Salt/context for the KDF.

    Returns:
        tuple[bytes, bytes]: (k_aead, k_kcv), where:
            - k_aead: Key for AEAD (encryption + authentication).
            - k_kcv: Control key (Key Check Value) for verification.

    Raises:
        TypeError, ValueError: Type/length errors raised by HKDF in the
            'cryptography' library (e.g., non-bytes, invalid lengths).

    Notes:
        Key separation principle — distinct HKDF info labels for different purposes.
    """
    k_aead = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=HKDF_INFO_AEAD).derive(master_key)
    k_kcv  = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=HKDF_INFO_KCV ).derive(master_key)
    return k_aead, k_kcv


def derive_filemac_key(master_key: bytes, salt: bytes) -> bytes:
    """
    Derives a 32-byte per-file MAC key (HKDF-SHA256).

    Args:
        master_key (bytes): Base key material (derived from password/secret).
        salt (bytes): Contextual salt/file identifier.

    Returns:
        bytes: 32-byte MAC key (per file).

    Raises:
        TypeError, ValueError: Type/length errors raised by HKDF in the
            'cryptography' library (e.g., non-bytes).
    """
    return HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=HKDF_INFO_FILEMAC).derive(master_key)

Per-file AEAD (HKDF + BLAKE2s)

def derive_aead_per_file(k_aead_base: bytes, header_bytes: bytes, nonce_prefix: bytes) -> bytes:
    """
    Derives a 32-byte AEAD key specific to a file from a base key and context.

    Mechanism:
        1) BLAKE2s(32) over `header_bytes || nonce_prefix` → salt for HKDF.
        2) HKDF-SHA256 (`length=32`, `info=b"SecureStream256Pro AEAD per-file"`)
           → the resulting AEAD key.

    Args:
        k_aead_base (bytes): Base AEAD material (32 bytes).
        header_bytes (bytes): Full ENCV2 header used as stream **AAD**.
        nonce_prefix (bytes): Nonce prefix (expected exactly 8 bytes).

    Returns:
        bytes: 32-byte AEAD key for file contents.

    Raises:
        TypeError, ValueError: Type/size errors raised by 'cryptography'
            (BLAKE2s/HKDF) for non-bytes, etc.

    Notes:
        The KCV is computed from `header_ctx` (see `build_header_ctx`), not from
        `header_bytes`. The function assumes an 8-byte `nonce_prefix`; its length
        is validated during stream parsing.
    """
    h = hashes.Hash(hashes.BLAKE2s(32))
    h.update(header_bytes)
    h.update(nonce_prefix)
    ctx_salt = h.finalize()

    return HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=ctx_salt,
        info=b"SecureStream256Pro AEAD per-file",
    ).derive(k_aead_base)

AAD and nonce per chunk

def _aad_common(header_bytes: bytes, chunk_bytes: int, nonce_prefix: bytes) -> bytes:
    """
    Builds common AAD for all file chunks.

    Args:
        header_bytes (bytes): The full ENCV2 header written to the file (built by
            `build_header`); the exact same byte sequence that precedes the stream.
        chunk_bytes (int): Chunk size in bytes.
        nonce_prefix (bytes): 8-byte nonce prefix.

    Returns:
        bytes: AAD buffer in the format:
            header_bytes || STREAM_MAGIC || >I(chunk_bytes) || nonce_prefix.
    """
    return b"".join([header_bytes, STREAM_MAGIC, struct.pack(">I", chunk_bytes), nonce_prefix])


def _nonce_for_chunk(nonce_prefix: bytes, idx: int) -> bytes:
    """
    Builds a 12-byte nonce: 8-byte prefix + 4-byte index (u32, big-endian).

    Args:
        nonce_prefix (bytes): Nonce prefix (exactly 8 bytes).
        idx (int): Chunk index in the range 0..2^32-1.

    Returns:
        bytes: 12-byte nonce.

    Raises:
        ValueError: If the index is out of range, the prefix has a wrong size,
            or the resulting nonce is not 12 bytes long.
    """
    if idx < 0 or idx > 0xFFFFFFFF:
        raise ValueError("Chunk index out of range.")
    if len(nonce_prefix) != NONCE_PREFIX_LEN:
        raise ValueError(f"Incorrect nonce prefix size (expected {NONCE_PREFIX_LEN} bytes).")

    nonce = nonce_prefix + struct.pack(">I", idx)
    if len(nonce) != NONCE_LEN:
        raise ValueError("Incorrect nonce size (not 12 bytes).")
    return nonce

AES-GCM: encrypt/decrypt chunks

def _encrypt_one_chunk(key: bytes, aad: bytes, nonce: bytes, pt: memoryview) -> tuple[bytes, bytes]:
    """
    Encrypts a single chunk in AEAD mode (e.g., AES-256-GCM).

    Args:
        key (bytes): Symmetric key.
        aad (bytes): Additional authenticated data (AAD).
        nonce (bytes): Nonce/IV (12 bytes).
        pt (memoryview): Plaintext of the chunk.

    Returns:
        tuple[bytes, bytes]: (ciphertext, tag).

    Raises:
        TypeError, ValueError: Parameter errors raised by 'cryptography'
            (e.g., incorrect type or length of key/nonce/AAD).
    """
    encryptor = Cipher(algorithms.AES(key), modes.GCM(nonce)).encryptor()
    encryptor.authenticate_additional_data(aad)
    ct = encryptor.update(pt)
    tail = encryptor.finalize()
    if tail:
        ct += tail
    return ct, encryptor.tag


def _decrypt_one_chunk(key: bytes, aad: bytes, nonce: bytes, ct: memoryview, tag: bytes) -> bytes:
    """
    Decrypts an AES-256-GCM chunk with tag verification.

    Args:
        key (bytes): AEAD symmetric key.
        aad (bytes): Additional authenticated data (AAD).
        nonce (bytes): Nonce/IV used for the chunk.
        ct (memoryview): Ciphertext of the chunk (without the tag).
        tag (bytes): Authentication tag.

    Returns:
        bytes: Plaintext of the chunk.

    Raises:
        InvalidTag: If GCM authentication fails.
        TypeError, ValueError: Parameter errors raised by 'cryptography'.
    """
    decryptor = Cipher(algorithms.AES(key), modes.GCM(nonce, tag)).decryptor()
    decryptor.authenticate_additional_data(aad)
    pt = decryptor.update(ct)
    tail = decryptor.finalize()
    if tail:
        pt += tail
    return pt

KCV before starting decryption

mode_byte = (hdr.mode & 0x7F) | (MODE_FLAG_HAS_INNER_META if hdr.has_inner_meta else 0)
header_ctx = build_header_ctx(
    mode_byte,
    hdr.name_len,
    hdr.ext_len,
    t=hdr.t,
    m=hdr.m,
    p=hdr.p,
    salt=hdr.salt,
)
expected_kcv = compute_kcv(k_kcv, header_ctx)

if not secrets.compare_digest(expected_kcv, hdr.kcv):
    raise KCVError("Incorrect password/key (KCV)")

Footer verification (BLAKE2s + HMAC) before write

footer_magic = f.read(len(FOOTER_MAGIC))
if footer_magic != FOOTER_MAGIC:
    raise ValueError("Corrupted stream: missing SGCM2F footer.")

raw_chunks = f.read(4)
raw_size   = f.read(8)
if len(raw_chunks) != 4 or len(raw_size) != 8:
    raise ValueError("Corrupted footer: summary metadata.")
file_chunks = struct.unpack(">I", raw_chunks)[0]
file_size   = struct.unpack(">Q", raw_size)[0]
if file_chunks > MAX_CHUNKS or file_size > MAX_PLAINTEXT:
    raise ValueError("Footer exceeds allowed limits.")

digest = f.read(32)
if len(digest) != 32:
    raise ValueError("Corrupted footer: BLAKE2s digest.")

mac = f.read(FILEMAC_LEN)
if len(mac) != FILEMAC_LEN:
    raise ValueError("Corrupted footer: MAC.")

if file_chunks != chunk_index or file_size != total_plain:
    raise InvalidTag("File summary mismatch.")

calc_digest = blake.finalize()
if not secrets.compare_digest(calc_digest, digest):
    raise InvalidTag("Stream digest mismatch.")

# Verify global file-MAC (HMAC) BEFORE os.replace
h = HMAC(filemac_key, hashes.SHA256())
h.update(aad_common)
h.update(struct.pack(">I", file_chunks))
h.update(struct.pack(">Q", file_size))
h.update(digest)
calc_mac = h.finalize()[:FILEMAC_LEN]
if not secrets.compare_digest(calc_mac, mac):
    raise InvalidTag("Global file-MAC mismatch.")

Frequently Asked Questions (FAQ)

Common questions

  1. First launch shows “Windows protected your PC”. Why?
    This is Windows SmartScreen reacting to a new, unsigned executable. To run, click More info → Run anyway. The app is portable and offline.
  2. Does the app require Internet access?
    No. Everything works locally; no telemetry.
  3. Can I run it from a USB drive?
    Yes. It’s a single portable EXE; no installation, no Python required.
  4. What happens if the disk becomes full while encrypting?
    The operation stops safely. The original file is preserved and temporary files are cleaned up on exit/start. You’ll see a system error like [WinError 112].
  5. How much free space do I need?
    Roughly the size of the largest file as free space (≈1× for Encrypt/Decrypt). With Backup enabled, plan for ≈2× free space during the operation.
  6. Are my passwords/keys stored or sent anywhere?
    No. Used only in RAM during the operation.
  7. Can I encrypt very large files?
    Yes, up to the 2 TiB per file safety limit (streamed).
  8. Can I cancel while it’s running?
    Yes. Cancel stops cleanly; no partial outputs are left.
  9. Which folders are cleaned automatically?
    On startup the app cleans its private temporary workspace (.tmp_secure). Backups are not touched.
  10. Is it free?
    Yes. You may use it under the included EULA. Documentation is in the ZIP.