100 lines
3 KiB
Python
100 lines
3 KiB
Python
#!/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)
|
|
|