Skip to content

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

pip install git+https://github.com/PQBlocks/hybrid-file-encryption

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:

encrypted 31000 bytes and decrypted them back: OK

How it works

  1. keygen() creates one ML-KEM-768 and one X25519 key pair.
  2. save_private_key seals the private half with scrypt and a random salt and writes it with mode 0600.
  3. encrypt draws 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.
  4. decrypt refuses files that were wrapped with X25519 only, unless you explicitly pass require_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.