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