How to encrypt files with a post-quantum key in Python¶
Create a hybrid ML-KEM-768 + X25519 key pair, store it on disk with the private key sealed under a passphrase, encrypt a file to the public key and decrypt it again. Uses pqblocks-files.
The problem¶
Archives, backups and exported reports are exactly what harvest now, decrypt later targets: they are stored for years. Files encrypted to an RSA or X25519 public key today can be opened once a quantum computer exists. pqblocks-files wraps each file key with both ML-KEM-768 and X25519 and seals the content in authenticated 64 KiB ChaCha20-Poly1305 chunks.
Install¶
ML-KEM comes from liboqs; see the package page.
Complete working code¶
encrypt_files.py
"""Encrypt a file to a post-quantum hybrid public key and decrypt it (pqblocks-files)."""
import os
import tempfile
from pqblocks_files.encrypt import decrypt, encrypt
from pqblocks_files.keys import keygen, load_private_key, load_public_key, save_private_key, save_public_key
from pqblocks_files.pqtypes import KeyPair
with tempfile.TemporaryDirectory() as d:
# 1. Create a key pair and store it; the private key is sealed under a passphrase (scrypt, mode 0600).
passphrase = os.urandom(16).hex().encode() # demo only: prompt for it or use a secret store
kp = keygen() # ML-KEM-768 + X25519
save_public_key(kp, os.path.join(d, "alice.pub"))
save_private_key(kp, os.path.join(d, "alice.key"), passphrase)
# 2. Anyone with the public key can encrypt.
report = b"Q3 board report - confidential\n" * 1000
recipient = load_public_key(os.path.join(d, "alice.pub"))
with open(os.path.join(d, "report.pfsh"), "wb") as fh:
fh.write(encrypt(report, [recipient]))
# 3. Only the private key holder can decrypt.
me = KeyPair(public=recipient, private=load_private_key(os.path.join(d, "alice.key"), passphrase))
with open(os.path.join(d, "report.pfsh"), "rb") as fh:
assert decrypt(fh.read(), me) == report
print("encrypted", len(report), "bytes and decrypted them back: OK")
Output:
How it works¶
keygen()creates one ML-KEM-768 and one X25519 key pair.save_private_keyseals the private half with scrypt and a random salt and writes it with mode0600.encryptdraws a fresh random file key, wraps it for each recipient with both algorithms, and encrypts the content in authenticated chunks. Several recipients can be listed.decryptrefuses files that were wrapped with X25519 only, unless you explicitly passrequire_pq=False. A downgraded file cannot be slipped past you.
Things to know¶
- Pre-1.0 and not independently audited; not FIPS or CMVP validated. Read the security notes.
- Never hard-code the passphrase. Prompt for it or read it from a secret store.
- A command-line interface ships with the package; see its README.