From b0307788a5c0f103e0248fd33cba01105eef0bea Mon Sep 17 00:00:00 2001 From: juyung Date: Sun, 26 Jul 2026 19:26:27 +0000 Subject: [PATCH] Upload --- README.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++ decrypt.py | 100 +++++++++++++++++++++++++++++++++++++++++++ encrypt.py | 86 +++++++++++++++++++++++++++++++++++++ gen_kyber.py | 18 ++++++++ requirements.txt | 4 ++ 5 files changed, 315 insertions(+) create mode 100644 README.md create mode 100644 decrypt.py create mode 100644 encrypt.py create mode 100644 gen_kyber.py create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..42c698f --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Post-Quantum Hybrid Kyber-1024 + AES-256-CTR File Encryption + +Streaming hybrid file encryption combining ML-KEM-1024 (Kyber-1024) for post-quantum key encapsulation with AES-256-CTR for bulk encryption and HMAC-SHA256 for integrity verification. + +Handles files larger than available RAM via chunked streaming (64 MB chunks). + + + +## Features + +- Post-quantum secure key exchange via liboqs Kyber-1024 KEM (~256-bit PQ security) +- AES-256-CTR symmetric encryption with a streaming architecture that handles files larger than RAM (64 MB chunks) +- HMAC-SHA256 authenticity tag to detect tampering or corruption + +## Requirements + +- `oqs` ([liboqs-python](https://github.com/open-quantum-safe/liboqs-python)) +- `cryptography` ([pyca/cryptography](https://github.com/pyca/cryptography)) + +```bash +python3 -m venv venv && . venv/bin/activate +pip install -r requirements.txt +``` + +## Usage + +1. Generate a Kyber-1024 key pair + +```bash +python3 gen_kyber.py +``` + +Produces: + +- `kyber.pub` - public key (1568 bytes) +- `kyber.sec` - secret key (3168 bytes) + +2. Encrypt a file + +```bash +python3 encrypt.py archive.7z +``` + +Output: `archive.7z.enc` + +The `.enc` file contains: + +Section | Description +-|- +Kyber ciphertext|Encapsulated key (ciphertext from KEM) +IV|16-byte AES-CTR nonce +HMAC key|32-byte HMAC key +File ciphertext|AES-256-CTR encrypted chunks +HMAC tag|32-byte SHA-256 MAC of the file ciphertext + +3. Decrypt a file + +```bash +python3 decrypt.py archive.7z.enc +``` + +Output: `archive.7z` + +The script automatically verifies the HMAC tag. If the file has been tampered with, you'll see: + +``` +Error: Integrity check failed. File corrupted or tampered +``` + +and the partial output gets deleted. + +## File formats + +**Key files (raw binary)** + +File|Content|Size (bytes) +-|-|- +`kyber.pub`|Kyber-1024 public key|1568 +`kyber.sec`|Kyber-1024 secret key|3168 + + +**Encrypted file structure** + +`archive.7z.enc`: +``` +┌─────────────────────────┐ +│ Kyber CT length (4 B) │ ← big-endian uint32 +├─────────────────────────┤ +│ Kyber ciphertext │ ← 1568 bytes(Kyber-1024) +├─────────────────────────┤ +│ IV (16 B) │ ← AES-CTR nonce +├─────────────────────────┤ +│ HMAC key (32 B) │ ← random per file +├─────────────────────────┤ +│ AES-256-CTR ciphertext │ ← streamed in 64 MB chunks +├─────────────────────────┤ +│ HMAC-SHA256 tag (32 B) │ ← over the AES ciphertext only +└─────────────────────────┘ +``` + +## Security notes + +- A new random AES key is generated **per file** via Kyber encapsulation. +- A new random HMAC key is generated **per file**. +- The HMAC tag is computed over the **AES ciphertext** (the encrypted file data), so any tampering is detected before decryption. +- Kyber-1024 is believed to offer security equivalent to ~256-bit classical keys against quantum attackers. +- The secret key file (`kyber.sec`) should be treated like a private SSH key. Store it offline or in a secure vault. diff --git a/decrypt.py b/decrypt.py new file mode 100644 index 0000000..6b36aa3 --- /dev/null +++ b/decrypt.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Hybrid decrypt a file encrypted with AES-256-CTR + HMAC-SHA256 + Kyber-1024. +Streaming handles files larger than RAM. +Usage: + python3 decrypt.py + +Output: original filename (without .enc) +Requires: kyber.sec in the same directory. +""" + +import os +import sys +import struct +import oqs +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.hmac import HMAC +from cryptography.hazmat.primitives import hashes + +KYBER_KEM = "Kyber1024" +IV_LEN = 16 +CHUNK_SIZE = 64 * 1024 * 1024 +HMAC_KEY_LEN = 32 +HMAC_TAG_LEN = 32 + + +def decrypt_file(input_path: str, output_path: str, secret_key: bytes): + with open(input_path, 'rb') as fin: + # Read header + kyber_ct_len = struct.unpack('!I', fin.read(4))[0] + kyber_ct = fin.read(kyber_ct_len) + iv = fin.read(IV_LEN) + hmac_key = fin.read(HMAC_KEY_LEN) + + # Recover AES key + kem = oqs.KeyEncapsulation(KYBER_KEM, secret_key) + aes_key = kem.decap_secret(kyber_ct) + kem.free() + + # Decrypt stream + cipher = Cipher(algorithms.AES(aes_key), modes.CTR(iv)) + decryptor = cipher.decryptor() + hmac = HMAC(hmac_key, hashes.SHA256()) + + # File size minus HMAC tag at the end + fin.seek(0, os.SEEK_END) + total_size = fin.tell() + data_end = total_size - HMAC_TAG_LEN + fin.seek(4 + kyber_ct_len + IV_LEN + HMAC_KEY_LEN) + + total_read = data_end - fin.tell() + + with open(output_path, 'wb') as fout: + read = 0 + while fin.tell() < data_end: + chunk_size = min(CHUNK_SIZE, data_end - fin.tell()) + chunk = fin.read(chunk_size) + hmac.update(chunk) + fout.write(decryptor.update(chunk)) + read += len(chunk) + print(f" Progress: {read}/{total_read} bytes", end='\r') + + # Verify integrity + stored_hmac = fin.read(HMAC_TAG_LEN) + computed_hmac = hmac.finalize() + if stored_hmac != computed_hmac: + os.remove(output_path) + print() + raise ValueError("Error: Integrity check failed. File corrupted or tampered") + + fout.write(decryptor.finalize()) + + print(f"\nSuccess: {input_path} → {output_path} ({total_read} bytes)") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + infile = sys.argv[1] + + if not os.path.exists(infile): + print(f"Error: File not found: {infile}") + sys.exit(1) + + if not infile.endswith(".enc"): + print(f"Warning: File doesn't end with .enc. Decrypting anyway") + + seckey_path = "kyber.sec" + if not os.path.exists(seckey_path): + print(f"Error: No Kyber secret key found.") + sys.exit(1) + + with open(seckey_path, 'rb') as f: + secret_key = f.read() + + outfile = infile[:-4] if infile.endswith(".enc") else infile + ".dec" + decrypt_file(infile, outfile, secret_key) + diff --git a/encrypt.py b/encrypt.py new file mode 100644 index 0000000..5192f2a --- /dev/null +++ b/encrypt.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +Hybrid encrypt a file using AES-256-CTR + HMAC-SHA256 + Kyber-1024. +Streaming handles files larger than RAM. +Usage: + python3 encrypt.py + +Output: .enc +Requires: kyber.pub in the same directory. +""" + +import os +import sys +import struct +import oqs +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.hmac import HMAC +from cryptography.hazmat.primitives import hashes + +KYBER_KEM = "Kyber1024" +IV_LEN = 16 +CHUNK_SIZE = 64 * 1024 * 1024 # 64 MB +HMAC_KEY_LEN = 32 + + +def encrypt_file(plaintext_path: str, output_path: str, pub_key: bytes): + iv = os.urandom(IV_LEN) + hmac_key = os.urandom(HMAC_KEY_LEN) + + kem = oqs.KeyEncapsulation(KYBER_KEM) + kyber_ct, aes_key = kem.encap_secret(pub_key) + kem.free() + + cipher = Cipher(algorithms.AES(aes_key), modes.CTR(iv)) + encryptor = cipher.encryptor() + hmac = HMAC(hmac_key, hashes.SHA256()) + + total_size = os.path.getsize(plaintext_path) + written = 0 + + with open(output_path, 'wb') as fout: + # Header + fout.write(struct.pack('!I', len(kyber_ct))) + fout.write(kyber_ct) + fout.write(iv) + fout.write(hmac_key) + + with open(plaintext_path, 'rb') as fin: + while True: + chunk = fin.read(CHUNK_SIZE) + if not chunk: + break + ct_chunk = encryptor.update(chunk) + hmac.update(ct_chunk) + fout.write(ct_chunk) + written += len(chunk) + print(f" Progress: {written}/{total_size} bytes", end='\r') + + fout.write(encryptor.finalize()) + fout.write(hmac.finalize()) + + print(f"\nSuccess: {plaintext_path} → {output_path} ({total_size} bytes)") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + infile = sys.argv[1] + + if not os.path.exists(infile): + print(f"Error: File not found: {infile}") + sys.exit(1) + + pubkey_path = "kyber.pub" + if not os.path.exists(pubkey_path): + print(f"Error: No Kyber public key found. Run gen_kyber.py first.") + sys.exit(1) + + with open(pubkey_path, 'rb') as f: + pub_key = f.read() + + outfile = infile + ".enc" + encrypt_file(infile, outfile, pub_key) + diff --git a/gen_kyber.py b/gen_kyber.py new file mode 100644 index 0000000..56c50e4 --- /dev/null +++ b/gen_kyber.py @@ -0,0 +1,18 @@ +import oqs + +# ── Generate Kyber-1024 keypair ── +# ML-KEM-1024 gives ~256-bit post-quantum security +kem = oqs.KeyEncapsulation("Kyber1024") +public_key = kem.generate_keypair() # bytes +secret_key = kem.export_secret_key() # bytes +kem.free() + +# ── Save to disk (PEM-like, but just raw bytes is fine) ── +with open("kyber.pub", "wb") as f: + f.write(public_key) +with open("kyber.sec", "wb") as f: + f.write(secret_key) + +print(f"Public key size: {len(public_key)} bytes") # 1568 for Kyber-1024 +print(f"Secret key size: {len(secret_key)} bytes") # 3168 for Kyber-1024 + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..43f56a9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +cffi==2.1.0 +cryptography==49.0.0 +liboqs-python==0.15.0 +pycparser==3.0