Skip to main content

pqblocks-files

Hybrid post-quantum file encryption for Python and the command line: ML-KEM-768 plus X25519 key encapsulation, with the payload sealed in 64 KiB ChaCha20-Poly1305 chunks (the pfsh v1 file format). Use it to encrypt files or byte strings to one or more recipients' public keys so that a future quantum computer, or a break of either primitive alone, does not expose them. Implements NIST FIPS 203 (ML-KEM-768, via liboqs), RFC 7748 (X25519), RFC 8439 (ChaCha20-Poly1305) and RFC 5869 (HKDF). Not FIPS or CMVP validated. Pre-1.0: not independently audited.

pip install pqblocks-files        # needs a system liboqs 0.15.x or 0.16.x, see Requirements
from pqblocks_files.encrypt import encrypt, decrypt
from pqblocks_files.keys import keygen

keypair = keygen()                                    # ML-KEM-768 + X25519
blob = encrypt(b"quarterly numbers", [keypair.public])  # fresh random file key every call
assert decrypt(blob, keypair) == b"quarterly numbers"   # refuses classical-only files by default

Requirements

  • Python 3.11 or newer; cryptography and bech32 (installed by pip).
  • A shared liboqs (liboqs.so, version 0.15.x or 0.16.x) found by the system loader, or pointed to with OQS_LIB_PATH=/path/to/liboqs.so.9. Any other liboqs version is refused at import with a RuntimeError. pip does not install liboqs. This was verified against liboqs 0.16.0; the CI workflow builds 0.15.0.

Why PQBlocks

  • Hybrid by default: every file key is wrapped under ML-KEM-768 and X25519. Classical-only files are a loud, explicit opt-in on both write (classical_only=True) and read (require_pq=False).
  • No foot-guns in the API: no nonce, file-key or algorithm parameters. The file key is always fresh from the OS CSPRNG.
  • Private keys are passphrase-protected properly: scrypt (n=2^17, r=8, p=1), a random 16-byte salt per file, versioned format, created 0600 with O_EXCL. The CLI has no default passphrase.
  • Tested against hostile input: 279 tests including Wycheproof and ACVP ML-KEM-768 vectors, and property-based fuzzing of the parsers. See TESTING.md.
  • Honest limits: see Limitations and SECURITY_NOTES.md. The open medium-severity items are listed there.
  • Licence: Apache-2.0.

Usage

Library

from pqblocks_files.keys import keygen, save_public_key, save_private_key, load_public_key, load_private_key
from pqblocks_files.encrypt import encrypt, decrypt, decrypt_with_mode
from pqblocks_files.pqtypes import KeyPair

kp = keygen()
save_public_key(kp, "alice.pub")                       # Bech32 text, mode 0444
save_private_key(kp, "alice.key", b"a long passphrase")  # scrypt + ChaCha20-Poly1305, mode 0600, never overwrites

alice = KeyPair(public=load_public_key("alice.pub"), private=load_private_key("alice.key", b"a long passphrase"))
blob = encrypt(open("report.pdf", "rb").read(), [alice.public])
result = decrypt_with_mode(blob, alice)                # result.plaintext, result.mode == "hybrid"

encrypt takes a list of recipients; any one of their private keys opens the file.

Command line

pqblocks-files keygen --output alice.pub --output-sec alice.key      # prompts for a passphrase (twice)
pqblocks-files encrypt --recipient alice.pub --output report.pfsh report.pdf
pqblocks-files decrypt --key alice.key --output report.pdf report.pfsh   # prompts for the passphrase

For scripts use --passphrase-file FILE (one trailing newline is stripped; an empty passphrase is refused). The passphrase is never accepted as an argument, never has a default, and is never printed.

Exit codes: 0 success; 1 bad input or malformed file; 2 key not found in header or key file could not be unlocked; 3 ML-KEM key-check abort; 4 classical-only file refused (re-run with --allow-classical).

Downgrade policy

decrypt() has require_pq=True by default. A file that your key can open only through a classical-only (X25519) stanza raises ClassicalOnlyRejected instead of being read. Pass require_pq=False (CLI: --allow-classical) to accept it; decrypt_with_mode() tells you which mode opened the file.

API reference

encrypt(plaintext: bytes, recipients: list[Recipient], classical_only: bool = False) -> bytes Complete pfsh v1 file. classical_only=True writes X25519-only stanzas (no post-quantum protection). Raises ValueError for no recipients or an all-zero ML-KEM key (a classical-only key used without classical_only=True), TypeError for wrong argument types.

decrypt(pfsh_bytes: bytes, keypair: KeyPair, require_pq: bool = True) -> bytes Plaintext, or an exception; never partial output. Exceptions: HeaderMACMismatch (no stanza opens under this key, or the header is malformed), ClassicalOnlyRejected, TruncatedFile, TagVerificationFailed, NonMonotonicCounter, MLKEMKeyCheckFailed, ValueError.

decrypt_with_mode(pfsh_bytes, keypair, require_pq=True) -> DecryptResult DecryptResult(plaintext, mode), mode is "hybrid" or "classical" (MODE_HYBRID, MODE_CLASSICAL in pqblocks_files.encrypt). plaintext is hidden from repr.

keygen(classical_only: bool = False) -> KeyPair; KeyPair(public: Recipient, private: bytes) (private is hidden from repr); Recipient(hybrid_pk: bytes, label: str = "").

save_public_key(keypair, path), load_public_key(path) -> Recipient, save_private_key(keypair, path, passphrase: bytes), load_private_key(path, passphrase: bytes) -> bytes. Loaders raise ValueError on any malformed, truncated, tampered or unknown-version file, and on an empty passphrase.

parse_header and the framing helpers in pqblocks_files.encrypt are internal and may change. Helpers that take a caller-chosen file key or ephemeral key (_chunk, _encode_header, the _eph_x25519_sk parameters in kem) are private on purpose: reusing a key would reuse a keystream.

Limitations

  • Whole file in memory: encrypt and decrypt take and return bytes; there is no streaming API.
  • Decrypt is slower than encrypt: at 10 MB the library-level median was 0.18 s to decrypt against 0.02 s to encrypt (see BENCHMARKS.md). The cause was not profiled.
  • No sender authentication or signatures: anyone who has your public key can create a file for you. Use a signature scheme on top if you need provenance.
  • No zeroization of keys or plaintext (Python bytes are immutable); see SECURITY_NOTES.md.
  • Pre-1.0 format and API: the key-file format is versioned (v1) but may change before 1.0. Key files from earlier unreleased builds are refused.
  • Key distribution is out of scope: nothing authenticates a public key file to a person.
  • Open medium-severity design items (combiner does not bind the X25519 ciphertext and public keys; shared HKDF label in classical mode; 128-bit file key) are listed in SECURITY_NOTES.md.

Security notes

See SECURITY_NOTES.md and SECURITY.md. Not independently audited; not FIPS 140-3 or CMVP validated.

Verification

TESTING.md · BENCHMARKS.md · EVIDENCE.md · VERIFICATION_REPORT.md · USE_CASES.md · AGENTS.md

Part of PQBlocks

PQBlocks builds open, tested building blocks for the post-quantum transition. This package is its hybrid file-encryption tool.

Metadata

Release files for pqblocks-files 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pqblocks-files 0.1.0
File Size Uploaded Provenance Actions
pqblocks_files-0.1.0.tar.gz 54.5 kB Without provenance Details for pqblocks_files-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pqblocks-files 0.1.0
File Interpreter ABI Platform Actions
pqblocks_files-0.1.0-py3-none-any.whl Python 3 none any Details for pqblocks_files-0.1.0-py3-none-any.whl

Total release size: 85.3 kB

Release files / pqblocks_files-0.1.0.tar.gz

Download URL pqblocks_files-0.1.0.tar.gz
Size 54.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4899f6c5239abc6a0428c7af2c82000f96392f74b732a6e116d5673916975e19
BLAKE2b-256 checksum
How to use checksums
af3dcb2606cae3b52f8f32e4a67e096542a8aa3ecd45c553f555bbc70a5feb37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / pqblocks_files-0.1.0-py3-none-any.whl

Download URL pqblocks_files-0.1.0-py3-none-any.whl
Size 30.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
98a30b5fcc4d67c9489e52b5bca72ca5c6fe9beb74f89bbfc5ca77a6385716dd
BLAKE2b-256 checksum
How to use checksums
86a244a163c3fe0c46622024d82b3904397ccef13bb844a4bc197fafa890cb3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for pqblocks-files 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page