Skip to content

pqdude/pq-age

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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

Features β€’ Install β€’ Usage β€’ Interop β€’ Security


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

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors