ENCV2 + SGCM2 + SGCM2F
Header + streamed AEAD frames with a footer MAC and BLAKE2s digest for whole-file integrity.
v2.3.0 • 100% offline · AES-256-GCM · Argon2id
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.
Header + streamed AEAD frames with a footer MAC and BLAKE2s digest for whole-file integrity.
Unique nonce per chunk (prefix + counter), AAD binds header context and sizes; HKDF domain separation per file.
Original name/extension stored only inside ciphertext (EMETA2); minimal header.
Private .tmp_secure, atomic fsync + replace, Unicode-safe paths; resilient to crashes and partial writes.
a1c6e8244b3c702c3b777ebc8e01d169337706c00ab24794a962e7cf28a1d096 securestream256pro_en_v2.3.0.zip 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.
venv + hard-pinned dependencies.securestream256pro.log and its rotations
(.log.1–.log.3) are skipped during scanning to avoid accidental encryption..tmp_secure and permanently removed (tomb → chmod → unlink), eliminating plaintext leftovers.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._MEIPASS).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.
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.
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})")
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)
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)
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
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
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_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.")
.tmp_secure). Backups are not touched.