Skip to main content

age-compatible post-quantum encryption with hybrid ML-KEM-1024 + X25519

Project description

pq-age logo

pq-age

age-compatible post-quantum encryption for Python
Hybrid ML-KEM-1024 + X25519 with full age v1 format interoperability

age v1 format · ML-KEM-1024 + X25519 hybrid · SSH Ed25519 keys · scrypt · ChaCha20-Poly1305

CI Coverage License PyPI

FeaturesInstallUsageInteropSecurity


What is pq-age?

pq-age is a Python implementation of the age encryption format with an additional hybrid post-quantum recipient type (mlkem1024-x25519-v1). It is fully interoperable with:

  • age (Go reference implementation)
  • rage (Rust implementation)
  • Any other age-compatible tool

Key differentiator: pq-age adds a hybrid ML-KEM-1024 + X25519 recipient that provides defense-in-depth against both classical and quantum attacks. Both algorithms must be broken to compromise the encryption.

🔐 Features

age-Compatible

  • age v1 format: Files encrypted with pq-age (using X25519, SSH, or scrypt) can be decrypted by age/rage
  • SSH Ed25519 keys: Encrypt to ~/.ssh/id_ed25519.pub, decrypt with ~/.ssh/id_ed25519
  • scrypt passwords: Password-based encryption compatible with age -p
  • X25519 recipients: Standard age public keys (age1...)

Post-Quantum Extension

  • Hybrid ML-KEM-1024 + X25519: New recipient type for quantum resistance
  • Defense-in-depth: Both classical and PQ algorithms must be broken
  • NIST Level-5: ML-KEM-1024 provides equivalent security to AES-256

Security

  • STREAM cipher: ChaCha20-Poly1305 with authenticated streaming (age standard)
  • Header MAC: HMAC-SHA256 for header integrity
  • Constant-time comparisons: Uses hmac.compare_digest where possible
  • Memory wiping: Best-effort in Python; native extension uses mlock + zeroize
  • scrypt: Password-based encryption uses age-compatible scrypt

📦 Installation

Quick Install

# Clone and install
git clone https://github.com/pqdude/pq-age.git
cd pq-age

# Install liboqs (required for ML-KEM)
./scripts/install-liboqs.sh

# Create venv and install
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e .

# Verify installation
pqage --version

Production Installation

# Install with native extension (REQUIRED for production)
pip install pq-age[native]

The native extension provides:

  • mlock() - prevents secrets from being swapped to disk
  • Guaranteed memory zeroization via Rust zeroize crate
  • Constant-time comparisons via subtle crate

Dependencies

  • Python >= 3.12
  • liboqs >= 0.15.0 (for ML-KEM-1024)
  • pynacl >= 1.5.0
  • liboqs-python == 0.14.0 (pinned for security)
  • bech32 >= 1.2.0 (for standard age key format)

🚀 Usage

Generate Hybrid Keypair

# Generate new identity (outputs to stdout)
pqage-keygen

# Save to file
pqage-keygen -o ~/.pqage/identity.txt

# Output:
# Public key: age1pq<base64...>
# Identity saved to: ~/.pqage/identity.txt

Encrypt Files

# Encrypt with hybrid post-quantum key
pqage -r "age1pq<public-key>" -o secret.age plaintext.txt

# Encrypt to SSH key (age-compatible)
pqage -R ~/.ssh/id_ed25519.pub -o secret.age plaintext.txt

# Encrypt with password (age-compatible)
pqage -p -o secret.age plaintext.txt

# Multiple recipients (any can decrypt)
pqage -r "age1pq<alice>" -r "age1pq<bob>" -R ~/.ssh/carol.pub -o secret.age file.txt

# ASCII-armored output
pqage -a -r "age1pq<key>" -o secret.age.asc plaintext.txt

Decrypt Files

# Decrypt with hybrid identity
pqage -d -i ~/.pqage/identity.txt -o plaintext.txt secret.age

# Decrypt with SSH key
pqage -d -i ~/.ssh/id_ed25519 -o plaintext.txt secret.age

# Decrypt with password
pqage -d -o plaintext.txt secret.age
# (prompts for password)

CLI Reference

pqage [OPTIONS] [INPUT]

Encryption (default):
  -e, --encrypt    Encrypt mode (default)
  -r RECIPIENT     Recipient public key (age1pq... or age1...)
  -R PATH          SSH public key file for recipient
  -p               Encrypt with passphrase (scrypt)
  -o PATH          Output file (default: stdout)
  -a               ASCII-armored output
  -f               Overwrite existing output file

Decryption:
  -d, --decrypt    Decrypt mode
  -i PATH          Identity file (pq-age or SSH private key)
  -o PATH          Output file (default: stdout)

Other:
  -v, --verbose    Verbose output
  --version        Show version

pqage-keygen [OPTIONS]
  -o PATH          Output identity file (default: stdout)
  -f               Overwrite existing file

🔄 Interoperability

With age/rage (Classical Recipients)

# Encrypt with pq-age, decrypt with age (SSH recipient)
pqage -R ~/.ssh/id_ed25519.pub -o secret.age plaintext.txt
age -d -i ~/.ssh/id_ed25519 -o plaintext.txt secret.age  # Works!

# Encrypt with age, decrypt with pq-age (password)
age -p -o secret.age plaintext.txt
pqage -d -o plaintext.txt secret.age  # Works!

Hybrid Recipients (pq-age only)

# Encrypt with hybrid PQ recipient
pqage -r "age1pq<hybrid-key>" -o secret.age plaintext.txt

# Decrypt requires pq-age
pqage -d -i ~/.pqage/identity.txt -o plaintext.txt secret.age
# age/rage cannot decrypt (unknown recipient type)

📁 File Format

pq-age uses the standard age v1 format with an additional recipient type:

age-encryption.org/v1
-> mlkem1024-x25519-v1 <fingerprint-b64> <mlkem-ct-b64> <x25519-eph-b64>
<wrapped-file-key-b64>
-> X25519 <ephemeral-share-b64>
<wrapped-file-key-b64>
-> ssh-ed25519 <key-hash-b64> <ephemeral-share-b64>
<wrapped-file-key-b64>
-> scrypt <salt-b64> <log2-N>
<wrapped-file-key-b64>
--- <header-mac-b64>
<STREAM-encrypted-payload>

Recipient Types

Type Description Interop
X25519 Classical age recipient age, rage, pq-age
ssh-ed25519 SSH Ed25519 key age, rage, pq-age
scrypt Password-based age, rage, pq-age
mlkem1024-x25519-v1 Hybrid post-quantum pq-age only

🏗️ Architecture

pqage/
├── age_format.py       # age v1 format parser/writer
├── age_file_ops.py     # High-level encrypt/decrypt
├── age_cli.py          # CLI interface
└── crypto/
    ├── age_stream.py   # age STREAM cipher
    ├── age_recipients.py   # Recipient implementations
    ├── ssh.py          # SSH key parsing
    ├── keys.py         # Key generation (SecureKeyBundle)
    ├── kdf.py          # HKDF-SHA256 key derivation
    ├── x25519.py       # X25519 helpers (clamping, ephemeral)
    ├── kem.py          # Hybrid ML-KEM-1024 + X25519
    └── utils.py        # Security utilities (secure_wipe)

🧪 Testing

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# With coverage
pytest tests/ --cov=pqage --cov-report=html

⚠️ Security Considerations

Note: This is a hobby project, not audited production software.

  1. Not standardized: The mlkem1024-x25519-v1 recipient type is a custom extension - only pq-age can decrypt it
  2. Post-quantum only with hybrid: Standard X25519/SSH/scrypt recipients provide classical security only
  3. Best-effort memory wiping: Python cannot guarantee memory is wiped; native extension helps but isn't magic
  4. Protect your keys: Use chmod 600 on identity files

For more details, see SECURITY.md.

📄 License

Apache License 2.0 - see LICENSE for details.

Acknowledgments

  • age by Filippo Valsorda - Original format specification and reference implementation
  • rage by str4d - Rust implementation (used for interoperability testing)
  • Open Quantum Safe - liboqs ML-KEM-1024 implementation
  • PyNaCl - X25519 and ChaCha20-Poly1305 via libsodium
  • C2SP age spec - Formal protocol specification

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pq_age-0.1.0.tar.gz (86.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pq_age-0.1.0-py3-none-any.whl (56.2 kB view details)

Uploaded Python 3

File details

Details for the file pq_age-0.1.0.tar.gz.

File metadata

  • Download URL: pq_age-0.1.0.tar.gz
  • Upload date:
  • Size: 86.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for pq_age-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3c81fe2a1b88b9f193bc39861fc87188d93e4b97b1c594767a95510e37643307
MD5 b5c16ec18f5928e6e3ff9c14deb64e62
BLAKE2b-256 b7aefad44cfecac767eb16de794886c62b9a29182d97266de9c970d18bd09c8c

See more details on using hashes here.

File details

Details for the file pq_age-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pq_age-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 56.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for pq_age-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 74a367a170bd104befb3619338a795e9907c07ea876a3387827a432ed227a37b
MD5 29869ec7fc8eeb64d67a2a311c4e1e7b
BLAKE2b-256 fae1c8107fb48440c9676483297e5f6ccb12a23b2a07a2908d8a5f01454fceba

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page