Upload
This commit is contained in:
commit
b0307788a5
5 changed files with 315 additions and 0 deletions
107
README.md
Normal file
107
README.md
Normal file
|
|
@ -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).
|
||||||
|
|
||||||
|
<a href="https://notes.juyung.com/gallery/6nIr0qID0vri0DR4Jy7HCbS6/hJv-1N1hIqFxzXIv85KqR9dq" target="_blank"/><img src='https://notes.juyung.com/uploads/small2x/f9/20/0805d761b05dbbf2046a68268b58.png' style="height: 720px;" /></a>
|
||||||
|
|
||||||
|
## 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.
|
||||||
100
decrypt.py
Normal file
100
decrypt.py
Normal file
|
|
@ -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 <filename.enc>
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
86
encrypt.py
Normal file
86
encrypt.py
Normal file
|
|
@ -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 <filename>
|
||||||
|
|
||||||
|
Output: <filename>.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)
|
||||||
|
|
||||||
18
gen_kyber.py
Normal file
18
gen_kyber.py
Normal file
|
|
@ -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
|
||||||
|
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
cffi==2.1.0
|
||||||
|
cryptography==49.0.0
|
||||||
|
liboqs-python==0.15.0
|
||||||
|
pycparser==3.0
|
||||||
Loading…
Reference in a new issue