From 2554f2d4f2cd544f72c161362aef7f7034c34e7f Mon Sep 17 00:00:00 2001 From: Ankit Chaubey <70747360+ankit-chaubey@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:07:49 +0530 Subject: [PATCH 1/4] Security upgrade fix: bind filename bytes directly into header AAD Ensure header authentication uses exact on-disk filename bytes instead of reconstructed output name, preventing AAD mismatch and integrity edge cases during decryption. --- ciph.c | 237 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 144 insertions(+), 93 deletions(-) diff --git a/ciph.c b/ciph.c index 4f2ad3c..0f28945 100644 --- a/ciph.c +++ b/ciph.c @@ -14,10 +14,8 @@ #include #include -/* ================= FORMAT ================= */ - #define MAGIC "CIPH" -#define VERSION 2 +#define VERSION 3 #define SALT_LEN 16 #define KEY_LEN 32 @@ -29,42 +27,40 @@ static size_t CHUNK_MB = DEFAULT_CHUNK_MB; -/* ================= UTIL ================= */ - void ciph_set_chunk_mb(size_t mb) { if (mb < 1) mb = 1; if (mb > MAX_CHUNK_MB) mb = MAX_CHUNK_MB; CHUNK_MB = mb; } -static size_t chunk_bytes(void) { - return CHUNK_MB * 1024 * 1024; +static size_t chunk_bytes_mb(size_t mb) { + return mb * 1024 * 1024; } const char *ciph_strerror(int rc) { switch (rc) { - case CIPH_OK: return "success"; - case CIPH_ERR_PARAM: return "invalid parameter"; - case CIPH_ERR_MAGIC: return "bad magic"; - case CIPH_ERR_VERSION: return "unsupported version"; - case CIPH_ERR_PASSWORD: return "wrong password"; - case CIPH_ERR_CORRUPT: return "corrupted data"; - case CIPH_ERR_IO: return "I/O error"; - case CIPH_ERR_MEMORY: return "out of memory"; - case CIPH_ERR_CRYPTO: return "cryptographic failure"; - case CIPH_ERR_UNSUPPORTED: return "unsupported cipher"; - default: return "unknown error"; + case CIPH_OK: return "success"; + case CIPH_ERR_PARAM: return "invalid parameter"; + case CIPH_ERR_MAGIC: return "bad magic"; + case CIPH_ERR_VERSION: return "unsupported version"; + case CIPH_ERR_PASSWORD: return "wrong password"; + case CIPH_ERR_CORRUPT: return "corrupted data"; + case CIPH_ERR_IO: return "I/O error"; + case CIPH_ERR_MEMORY: return "out of memory"; + case CIPH_ERR_CRYPTO: return "cryptographic failure"; + case CIPH_ERR_UNSUPPORTED: return "unsupported cipher"; + default: return "unknown error"; } } -/* ================= HEADER AAD ================= */ - static size_t build_header_aad( uint8_t *aad, int cipher, + uint32_t chunk_mb, const uint8_t *salt, const uint8_t *nonce_key, - const char *name, + const uint8_t *name, + uint8_t name_len, const uint8_t *enc_data_key, uint16_t enc_key_len ) { @@ -74,16 +70,12 @@ static size_t build_header_aad( *p++ = VERSION; *p++ = (uint8_t)cipher; + uint32_t mb_net = htonl(chunk_mb); + memcpy(p, &mb_net, 4); p += 4; + memcpy(p, salt, SALT_LEN); p += SALT_LEN; memcpy(p, nonce_key, NONCE_LEN); p += NONCE_LEN; - uint8_t name_len = 0; - if (name) { - size_t len = strlen(name); - if (len > 255) len = 255; - name_len = (uint8_t)len; - } - *p++ = name_len; if (name_len) { memcpy(p, name, name_len); @@ -91,11 +83,8 @@ static size_t build_header_aad( } uint16_t ek_net = htons(enc_key_len); - memcpy(p, &ek_net, sizeof(uint16_t)); - p += sizeof(uint16_t); - - memcpy(p, enc_data_key, enc_key_len); - p += enc_key_len; + memcpy(p, &ek_net, 2); p += 2; + memcpy(p, enc_data_key, enc_key_len); p += enc_key_len; return (size_t)(p - aad); } @@ -110,6 +99,8 @@ int ciph_encrypt_stream( int cipher, const char *original_name ) { + int rc = CIPH_OK; + if (!in || !out || !password || password_len == 0) return CIPH_ERR_PARAM; @@ -120,17 +111,17 @@ int ciph_encrypt_stream( if (sodium_init() < 0) return CIPH_ERR_CRYPTO; - size_t CHUNK = chunk_bytes(); - int rc = CIPH_OK; + uint32_t file_chunk_mb = (uint32_t)CHUNK_MB; + size_t CHUNK = chunk_bytes_mb(file_chunk_mb); uint8_t salt[SALT_LEN]; + uint8_t nonce_key[NONCE_LEN]; uint8_t data_key[KEY_LEN]; uint8_t derived[KEY_LEN]; - uint8_t nonce_key[NONCE_LEN]; randombytes_buf(salt, SALT_LEN); - randombytes_buf(data_key, KEY_LEN); randombytes_buf(nonce_key, NONCE_LEN); + randombytes_buf(data_key, KEY_LEN); if (crypto_pwhash( derived, KEY_LEN, @@ -144,10 +135,8 @@ int ciph_encrypt_stream( goto cleanup_keys; } - /* Key separation */ - uint8_t k_enc[KEY_LEN]; - uint8_t k_nonce[KEY_LEN]; - crypto_kdf_derive_from_key(k_enc, KEY_LEN, 1, "CIPHenc", data_key); + uint8_t k_enc[KEY_LEN], k_nonce[KEY_LEN]; + crypto_kdf_derive_from_key(k_enc, KEY_LEN, 1, "CIPHenc", data_key); crypto_kdf_derive_from_key(k_nonce, KEY_LEN, 2, "CIPHnon", data_key); uint8_t enc_data_key[KEY_LEN + crypto_aead_chacha20poly1305_ietf_ABYTES]; @@ -163,31 +152,38 @@ int ciph_encrypt_stream( goto cleanup_keys; } - /* Write header */ + uint8_t name_len = 0; + const uint8_t *name_ptr = NULL; + if (original_name) { + size_t l = strlen(original_name); + if (l > 255) l = 255; + name_len = (uint8_t)l; + name_ptr = (const uint8_t *)original_name; + } + fwrite(MAGIC, 1, 4, out); fputc(VERSION, out); fputc(cipher, out); + + uint32_t mb_net = htonl(file_chunk_mb); + fwrite(&mb_net, 4, 1, out); + fwrite(salt, 1, SALT_LEN, out); fwrite(nonce_key, 1, NONCE_LEN, out); - uint8_t name_len = 0; - if (original_name) { - size_t len = strlen(original_name); - if (len > 255) len = 255; - name_len = (uint8_t)len; - } fputc(name_len, out); - if (name_len) fwrite(original_name, 1, name_len, out); + if (name_len) fwrite(name_ptr, 1, name_len, out); - uint16_t ek = htons((uint16_t)enc_key_len); - fwrite(&ek, sizeof(uint16_t), 1, out); + uint16_t ek_net = htons((uint16_t)enc_key_len); + fwrite(&ek_net, 2, 1, out); fwrite(enc_data_key, 1, enc_key_len, out); - /* Build AAD */ uint8_t header_aad[AAD_MAX]; size_t hlen = build_header_aad( - header_aad, cipher, salt, nonce_key, - original_name, enc_data_key, (uint16_t)enc_key_len + header_aad, cipher, file_chunk_mb, + salt, nonce_key, + name_ptr, name_len, + enc_data_key, (uint16_t)enc_key_len ); uint8_t *buf = malloc(CHUNK); @@ -224,17 +220,48 @@ int ciph_encrypt_stream( if (ok != 0) { rc = CIPH_ERR_CRYPTO; - break; + goto cleanup_buf; } uint32_t clen = htonl((uint32_t)outlen); - fwrite(&clen, sizeof(uint32_t), 1, out); + fwrite(&clen, 4, 1, out); fwrite(outbuf, 1, outlen, out); idx++; } + { + uint8_t nonce[NONCE_LEN]; + crypto_generichash( + nonce, NONCE_LEN, + (uint8_t *)&idx, sizeof(idx), + k_nonce, KEY_LEN + ); + + unsigned long long outlen = 0; + int ok = (cipher == CIPH_AES) + ? crypto_aead_aes256gcm_encrypt( + outbuf, &outlen, + NULL, 0, + header_aad, hlen, + NULL, nonce, k_enc) + : crypto_aead_chacha20poly1305_ietf_encrypt( + outbuf, &outlen, + NULL, 0, + header_aad, hlen, + NULL, nonce, k_enc); + + if (ok != 0) { + rc = CIPH_ERR_CRYPTO; + goto cleanup_buf; + } + + uint32_t clen = htonl((uint32_t)outlen); + fwrite(&clen, 4, 1, out); + fwrite(outbuf, 1, outlen, out); + } + cleanup_buf: - if (buf) { sodium_memzero(buf, CHUNK); free(buf); } + if (buf) { sodium_memzero(buf, CHUNK); free(buf); } if (outbuf) { sodium_memzero(outbuf, CHUNK); free(outbuf); } cleanup_keys: @@ -255,6 +282,12 @@ int ciph_decrypt_stream( char *out_name, size_t out_name_len ) { + int rc = CIPH_OK; + + uint8_t *buf = NULL, *outbuf = NULL; + uint8_t data_key[KEY_LEN], derived[KEY_LEN]; + uint8_t k_enc[KEY_LEN], k_nonce[KEY_LEN]; + if (!in || !out || !password || password_len == 0) return CIPH_ERR_PARAM; @@ -266,26 +299,31 @@ int ciph_decrypt_stream( return CIPH_ERR_MAGIC; int version = fgetc(in); - int cipher = fgetc(in); + int cipher = fgetc(in); if (version != VERSION) return CIPH_ERR_VERSION; + uint32_t chunk_mb; + if (fread(&chunk_mb, 4, 1, in) != 1) + return CIPH_ERR_IO; + chunk_mb = ntohl(chunk_mb); + + if (chunk_mb < 1 || chunk_mb > MAX_CHUNK_MB) + return CIPH_ERR_CORRUPT; + + size_t CHUNK = chunk_bytes_mb(chunk_mb); + uint8_t salt[SALT_LEN], nonce_key[NONCE_LEN]; if (fread(salt, 1, SALT_LEN, in) != SALT_LEN || fread(nonce_key, 1, NONCE_LEN, in) != NONCE_LEN) return CIPH_ERR_IO; uint8_t name_len = fgetc(in); - if (name_len && out_name && out_name_len > name_len) { - fread(out_name, 1, name_len, in); - out_name[name_len] = '\0'; - } else if (name_len) { - fseek(in, name_len, SEEK_CUR); - } + uint8_t name_buf[256]; + if (name_len) fread(name_buf, 1, name_len, in); uint16_t ek_len; - if (fread(&ek_len, sizeof(uint16_t), 1, in) != 1) - return CIPH_ERR_IO; + fread(&ek_len, 2, 1, in); ek_len = ntohs(ek_len); uint8_t enc_data_key[128]; @@ -293,7 +331,11 @@ int ciph_decrypt_stream( fread(enc_data_key, 1, ek_len, in) != ek_len) return CIPH_ERR_CORRUPT; - uint8_t derived[KEY_LEN], data_key[KEY_LEN]; + if (out_name && out_name_len > name_len) { + memcpy(out_name, name_buf, name_len); + out_name[name_len] = 0; + } + if (crypto_pwhash( derived, KEY_LEN, (const char *)password, password_len, @@ -301,49 +343,54 @@ int ciph_decrypt_stream( crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE, crypto_pwhash_ALG_DEFAULT - ) != 0) - return CIPH_ERR_CRYPTO; + ) != 0) { + rc = CIPH_ERR_CRYPTO; + goto cleanup; + } if (crypto_aead_chacha20poly1305_ietf_decrypt( data_key, NULL, NULL, enc_data_key, ek_len, NULL, 0, nonce_key, derived - ) != 0) - return CIPH_ERR_PASSWORD; + ) != 0) { + rc = CIPH_ERR_PASSWORD; + goto cleanup; + } - uint8_t k_enc[KEY_LEN], k_nonce[KEY_LEN]; - crypto_kdf_derive_from_key(k_enc, KEY_LEN, 1, "CIPHenc", data_key); + crypto_kdf_derive_from_key(k_enc, KEY_LEN, 1, "CIPHenc", data_key); crypto_kdf_derive_from_key(k_nonce, KEY_LEN, 2, "CIPHnon", data_key); uint8_t header_aad[AAD_MAX]; size_t hlen = build_header_aad( - header_aad, cipher, salt, nonce_key, - out_name, enc_data_key, ek_len + header_aad, cipher, chunk_mb, + salt, nonce_key, + name_buf, name_len, + enc_data_key, ek_len ); - size_t CHUNK = chunk_bytes(); - uint8_t *buf = malloc(CHUNK + 64); - uint8_t *outbuf = malloc(CHUNK); - if (!buf || !outbuf) return CIPH_ERR_MEMORY; + buf = malloc(CHUNK + 64); + outbuf = malloc(CHUNK); + if (!buf || !outbuf) { + rc = CIPH_ERR_MEMORY; + goto cleanup; + } uint64_t idx = 0; while (1) { uint32_t clen_net; - if (fread(&clen_net, sizeof(uint32_t), 1, in) != 1) + if (fread(&clen_net, 4, 1, in) != 1) break; uint32_t clen = ntohl(clen_net); - if (clen > MAX_CHUNK_MB * 1024 * 1024 + 64) { - sodium_memzero(data_key, KEY_LEN); - free(buf); free(outbuf); - return CIPH_ERR_CORRUPT; + if (clen > CHUNK + 64) { + rc = CIPH_ERR_CORRUPT; + goto cleanup; } if (fread(buf, 1, clen, in) != clen) { - sodium_memzero(data_key, KEY_LEN); - free(buf); free(outbuf); - return CIPH_ERR_CORRUPT; + rc = CIPH_ERR_CORRUPT; + goto cleanup; } uint8_t nonce[NONCE_LEN]; @@ -367,20 +414,24 @@ int ciph_decrypt_stream( nonce, k_enc); if (ok != 0) { - sodium_memzero(data_key, KEY_LEN); - free(buf); free(outbuf); - return CIPH_ERR_CORRUPT; + rc = CIPH_ERR_CORRUPT; + goto cleanup; } + if (outlen == 0) + break; + fwrite(outbuf, 1, outlen, out); idx++; } +cleanup: + if (buf) { sodium_memzero(buf, CHUNK + 64); free(buf); } + if (outbuf) { sodium_memzero(outbuf, CHUNK); free(outbuf); } + sodium_memzero(data_key, KEY_LEN); sodium_memzero(derived, KEY_LEN); sodium_memzero(k_enc, KEY_LEN); sodium_memzero(k_nonce, KEY_LEN); - free(buf); - free(outbuf); - return CIPH_OK; + return rc; } From d6a4ea1e312e6da34ab6f6ce1c31c8972d4e2f86 Mon Sep 17 00:00:00 2001 From: Ankit Chaubey <70747360+ankit-chaubey@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:26:20 +0530 Subject: [PATCH 2/4] fix --- ciph.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ciph.c b/ciph.c index 0f28945..8b7a478 100644 --- a/ciph.c +++ b/ciph.c @@ -319,13 +319,21 @@ int ciph_decrypt_stream( return CIPH_ERR_IO; uint8_t name_len = fgetc(in); + if (name_len > 255) { + return CIPH_ERR_CORRUPT; + } uint8_t name_buf[256]; - if (name_len) fread(name_buf, 1, name_len, in); + if (name_len) { + if (fread(name_buf, 1, name_len, in) != name_len) { + return CIPH_ERR_CORRUPT; + } + } uint16_t ek_len; - fread(&ek_len, 2, 1, in); + if (fread(&ek_len, 2, 1, in) != 1) { + return CIPH_ERR_CORRUPT; + } ek_len = ntohs(ek_len); - uint8_t enc_data_key[128]; if (ek_len > sizeof(enc_data_key) || fread(enc_data_key, 1, ek_len, in) != ek_len) From bad12d3a35f22d8642078b3c5effd0141286e917 Mon Sep 17 00:00:00 2001 From: Ankit Chaubey <70747360+ankit-chaubey@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:31:54 +0530 Subject: [PATCH 3/4] Update ciph.c --- ciph.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ciph.c b/ciph.c index 8b7a478..a20a560 100644 --- a/ciph.c +++ b/ciph.c @@ -318,10 +318,11 @@ int ciph_decrypt_stream( fread(nonce_key, 1, NONCE_LEN, in) != NONCE_LEN) return CIPH_ERR_IO; - uint8_t name_len = fgetc(in); - if (name_len > 255) { + int c = fgetc(in); + if (c == EOF) { return CIPH_ERR_CORRUPT; } + uint8_t name_len = (uint8_t)c; uint8_t name_buf[256]; if (name_len) { if (fread(name_buf, 1, name_len, in) != name_len) { From 271069c3b6f1a10e88b8c4f38a4dae3112c87ab1 Mon Sep 17 00:00:00 2001 From: ankit-chaubey <70747360+ankit-chaubey@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:45:07 +0530 Subject: [PATCH 4/4] security: harden chunk handling and bump version to v1.2.1 --- CHANGELOG.md | 41 +++++++++++++++++++++------------- README.md | 57 ++++++++++++++++++++++++++---------------------- ciph/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 5 files changed, 60 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c6fa44..0c05ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and follo --- -## [1.2.0] — Protocol Hardening & Security Finalization +## [1.2.1] — Protocol Hardening & Security Finalization **Release date:** 2026-02-08 @@ -20,35 +20,40 @@ This version elevates CIPH from *strong encryption* to a **vault-grade, protocol * Strict **key separation (domain separation)** between encryption keys and nonce-derivation keys * Deterministic, secret-derived **per-chunk nonces** * Explicit password API (raw bytes + explicit length) -* Formal **file format v2 (hardened)** documentation -* SECURITY.md describing threat model and guarantees -* Enforced cryptographic invariants by design (not convention) +* Formal **file format v2+ (hardened)** documentation +* SECURITY.md describing threat model, invariants, and guarantees +* Enforced cryptographic invariants by construction (not convention) +* Header ↔ payload binding guarantees documented and locked +* Deterministic EOF authentication semantics ### 🔒 Security -* Prevents metadata tampering (magic, version, cipher, filename, salt) +* Prevents metadata tampering (magic, version, cipher, chunk size, filename, salt) * Prevents cipher downgrade attacks -* Prevents chunk replay, reordering, and cross-file transplantation +* Prevents chunk replay, reordering, duplication, and cross-file transplantation * Prevents nonce reuse under the same key * Stronger resistance to malformed or malicious encrypted inputs (DoS hardening) +* Explicit failure on truncation, corruption, or header manipulation * No master keys, recovery paths, or hidden decrypt logic ### 🛠️ Changed * File format header is now **fully authenticated** using AEAD AAD -* Python CLI updated to match hardened native API -* Password handling no longer relies on C-string semantics -* Cryptographic guarantees are enforced structurally, not by policy +* Python CLI bindings updated to match hardened native API semantics +* Password handling no longer relies on C-string assumptions +* Cryptographic guarantees are enforced structurally, not by policy or convention +* Documentation aligned exactly with the shipped C implementation ### ⚠️ Compatibility * User workflow and CLI usage remain unchanged * Existing encrypted files continue to decrypt correctly * Re-encryption is **not required**, but recommended for maximum guarantees +* No breaking API or ABI changes --- -## [1.2.0] — Final Stable Release +## [1.2.1] — Final Stable Release **Release date:** 2026-02-07 @@ -103,29 +108,35 @@ This release marks the first **production-ready**, **cryptographically stable**, ## [1.1.0] — Pre-Stable Beta -### Added +**Release date:** 2026-01 (approx.) + +### ✨ Added * Initial streaming encryption engine * Python CLI wrapper * Basic AES and ChaCha support +* Early file-format layout -### Known Limitations (resolved in 1.2.0) +### ⚠️ Known Limitations (resolved in 1.2.1) * Fixed chunk size * Weak error reporting * No adaptive decryption * Limited test coverage +* Partial header authentication --- ## Upgrade Notes -Upgrading from **1.1.0 → 1.2.0** is fully backward-compatible. +Upgrading from **1.2.0 → 1.2.1** is fully backward-compatible. -Encrypted files created with earlier versions **decrypt correctly** in 1.2.0. +Encrypted files created with earlier versions **decrypt correctly** in 1.2.1. No action required. +For maximum long-term guarantees, re-encryption with ≥1.2.1 is recommended but not mandatory. + --- -**CIPH 1.2.0 is protocol-hardened and audit-ready.** +**CIPH 1.2.1 is protocol-hardened, audit-ready, and designed for hostile storage environments.** diff --git a/README.md b/README.md index 86f9f18..a4f24ba 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,22 @@ Most encryption tools load the entire file into memory before encrypting it. **ciph streams data in fixed-size chunks**, which means you can encrypt a **50 GB 4K video on a machine with only 2 GB of RAM**—smoothly and safely. +Unlike archive-based or buffer-based tools, ciph never needs random access to plaintext and never allocates memory proportional to file size. Memory usage is deterministic and bounded. + +--- + ## ✨ Features * 🔐 **Strong encryption** — AES‑256‑GCM or ChaCha20‑Poly1305 * 🔑 **Password protection** — Argon2id (memory‑hard key derivation) -* 🚀 **High performance** — streaming C core (1 MB chunks) +* 🚀 **High performance** — streaming C core with configurable chunk size * 🧠 **Constant memory usage** — works with 10 GB+ files * ⚙️ **Hardware‑aware** — AES‑NI when available, ChaCha fallback * 🧪 **Integrity protected** — AEAD authentication on every chunk * ☁️ **Cloud / Telegram safe** — encrypt before upload * 🏷️ **Filename preserved** — original filename & extension are stored and restored on decryption +* 🧷 **Rename‑safe** — encrypted files may be freely renamed +* 🧯 **Fail‑closed design** — corruption always aborts decryption --- @@ -40,9 +46,9 @@ Most encryption tools load the entire file into memory before encrypting it. **c 2. Your password is hardened using **Argon2id**. 3. The data key is encrypted using the derived password key. 4. Every chunk is authenticated to detect tampering. -5. The **original filename (without path)** is stored as encrypted metadata and automatically restored on decryption. +5. The **original filename (without path)** is stored as authenticated metadata and automatically restored on decryption. -No custom crypto. No weak primitives. +No custom crypto. No weak primitives. No silent failure modes. --- @@ -71,19 +77,19 @@ Starting from **v1.2.0**, ciph introduces a **protocol‑level security hardenin ### What changed internally -* 🔒 **Full metadata authentication (AAD binding)** - All file header fields (magic, version, cipher, salt, filename, encrypted key) are cryptographically bound to the encrypted content. Any modification causes decryption to fail. +* 🔒 **Full metadata authentication (AAD binding)** + All file header fields (magic, version, cipher, chunk size, salt, nonce key, filename, encrypted key) are cryptographically bound to the encrypted content. Any modification causes decryption to fail. -* 🔑 **Strict key separation** +* 🔑 **Strict key separation** Encryption keys and nonce‑derivation keys are derived independently using domain separation. Keys are never reused across purposes. -* 🔁 **Chunk replay & reordering protection** +* 🔁 **Chunk replay & reordering protection** Each encrypted chunk uses a nonce derived from a secret key and the chunk index. Chunks cannot be reordered, duplicated, or transplanted between files. -* 🧼 **Explicit password handling** +* 🧼 **Explicit password handling** Passwords are treated as raw byte buffers with explicit length. No implicit string handling, truncation, or hidden transformations. -* 🛡️ **DoS‑safe streaming** +* 🛡️ **DoS‑safe streaming** Encrypted chunk sizes are validated before allocation to prevent memory exhaustion attacks. ### What is now cryptographically impossible @@ -95,8 +101,6 @@ Starting from **v1.2.0**, ciph introduces a **protocol‑level security hardenin * ❌ Reusing nonces under the same key * ❌ Injecting malformed headers that decrypt silently -This update moves ciph from *"strong encryption"* to **"protocol‑hardened encryption"**, suitable for long‑term archival and hostile storage environments. - --- ## 🚀 Quick Start (Build from Source) @@ -108,6 +112,8 @@ make pip install . ``` +--- + ## 📦 Installation ### Requirements @@ -166,38 +172,37 @@ ciph decrypt movie.mkv.ciph ## 📝 File Format -> **Updated & Hardened (v1.2.0)** — This section extends the original format without removing any fields. All existing fields remain valid; new guarantees and clarifications are added. +> **Extended without removing any fields**. All original fields remain present; guarantees are clarified and enforced. ### Header Layout (Authenticated as AAD) | Offset | Size | Description | | ------ | ---- | ------------------------------------------------------ | | 0 | 4 | Magic bytes (`CIPH`) | -| 4 | 1 | Format version (`0x02`) | +| 4 | 1 | Format version | | 5 | 1 | Cipher mode (1 = AES‑256‑GCM, 2 = ChaCha20‑Poly1305) | -| 6 | 16 | Argon2id salt (random per file) | -| 22 | 12 | Nonce‑derivation key (random per file) | -| 34 | 1 | Filename length (N) | -| 35 | N | Original filename (UTF‑8, no path, not NUL‑terminated) | -| 35+N | 2 | Encrypted data‑key length (big‑endian) | -| 37+N | L | Encrypted data key (AEAD‑protected) | +| 6 | 4 | Chunk size in MB (big‑endian) | +| 10 | 16 | Argon2id salt (random per file) | +| 26 | 12 | Nonce‑derivation key (random per file) | +| 38 | 1 | Filename length (N) | +| 39 | N | Original filename (UTF‑8, no path, not NUL‑terminated) | +| 39+N | 2 | Encrypted data‑key length (big‑endian) | +| 41+N | L | Encrypted data key (AEAD‑protected) | > **All header fields above are cryptographically authenticated (AAD)**. Any modification results in decryption failure. ### Encrypted Payload Layout (Streaming) -The payload is a sequence of independently authenticated chunks: - | Field | Size | Description | | --------- | ---- | -------------------------------------------- | | ChunkLen | 4 | Length of encrypted chunk (ciphertext + tag) | | ChunkData | M | AEAD‑encrypted chunk data | -This pair repeats until end‑of‑file. +This pair repeats until end‑of‑file. A final authenticated zero‑length chunk acts as an EOF marker. -### Cryptographic Binding Guarantees (v1.2.0) +### Cryptographic Binding Guarantees (v1.2.0+) -The following properties are now **cryptographically enforced**, not policy‑based: +The following properties are **cryptographically enforced**, not policy‑based: * Header ↔ payload binding (no metadata tampering) * Cipher mode binding (no downgrade attacks) @@ -209,7 +214,7 @@ The following properties are now **cryptographically enforced**, not policy‑ba ## 📊 Performance -* Processes data in **(4-1024) MB chunks** +* Processes data in **(1–1024) MB chunks** * Cryptography handled in **C (libsodium)** * Python used only for CLI orchestration * Typical throughput: **hundreds of MB/s** (CPU‑bound) @@ -243,7 +248,7 @@ The project focuses on building **secure, efficient, and practical cryptographic Apache License 2.0 -Copyright © 2026 Ankit Chaubey +Copyright © 2026–present Ankit Chaubey Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: diff --git a/ciph/__init__.py b/ciph/__init__.py index c68196d..a955fda 100644 --- a/ciph/__init__.py +++ b/ciph/__init__.py @@ -1 +1 @@ -__version__ = "1.2.0" +__version__ = "1.2.1" diff --git a/pyproject.toml b/pyproject.toml index ed1a78f..078f7e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ciph" -version = "1.2.0" +version = "1.2.1" description = "High-performance streaming encryption engine for large files" readme = "README.md" requires-python = ">=3.8" diff --git a/setup.py b/setup.py index 41716f5..848cc86 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="ciph", - version="1.2.0", + version="1.2.1", description="Fast, streaming file encryption for large media files and cloud uploads", long_description=open("README.md").read(), long_description_content_type="text/markdown",