Key Serialization
The aegisq.keys module provides file-oriented helpers built on top of KeyPair. It handles the common patterns of saving public keys to disk, loading them back, and round-tripping encrypted secret keys with a password.
The module is v1.3.0 and lives in aegisq/keys.py. Every function accepts either a string path or a pathlib.Path.
Quick Reference
Section titled “Quick Reference”| Function | Direction | Format | Encryption |
|---|---|---|---|
save_public_key | KeyPair → file | PEM (default) or JSON | None |
load_public_key | file → bytes | Auto-detect PEM/JSON | None |
save_secret_key | KeyPair → file | Encrypted PEM | AES-256-GCM (HKDF-SHA3-256) |
load_secret_key | file → bytes | Encrypted PEM | AES-256-GCM (HKDF-SHA3-256) |
public_key_to_pem | KeyPair → str | PEM | None |
public_key_to_json | KeyPair → str | JSON | None |
secret_key_to_pem | KeyPair → str | Encrypted PEM | AES-256-GCM (HKDF-SHA3-256) |
Public-Key Persistence
Section titled “Public-Key Persistence”save_public_key
Section titled “save_public_key”def save_public_key(keypair: KeyPair, path: str | Path, *, fmt: str = "pem") -> NoneWrites the public key to path in either PEM (default) or JSON format.
| Parameter | Type | Default | Description |
|---|---|---|---|
keypair | KeyPair | — | Source keypair. |
path | str | Path | — | Destination file. Use .pem for PEM or .json for JSON. |
fmt | str | "pem" | Either "pem" or "json". |
Raises: ValueError if fmt is not "pem" or "json"; OSError on filesystem failure.
from aegisq import AegisCipherfrom aegisq.keys import save_public_key
cipher = AegisCipher()keypair = cipher.generate_keypair()
# PEM (recommended for long-term storage)save_public_key(keypair, "recipient.pem")
# JSON (self-describing, useful for interop)save_public_key(keypair, "recipient.json", fmt="json")load_public_key
Section titled “load_public_key”def load_public_key(path: str | Path, *, level: SecurityLevel | None = None) -> bytesReads a public key back from disk and returns it as bytes (ready to pass to AegisCipher.encrypt()).
Format detection is automatic — the function inspects the first non-empty line of the file:
-----BEGIN ML-KEM PUBLIC KEY-----→ PEM (caller must supplylevel=){→ JSON (level is read from the"level"field andlevelargument is ignored)
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | Path | — | Source file. |
level | SecurityLevel | None | None | Required for PEM. Ignored for JSON. |
Returns: bytes — the public key.
Raises:
ValueError— file format not recognizable, or PEM file withoutlevel=KeySerializationError— malformed PEM header or JSON fieldsInvalidParameterError— decoded size does not match the levelOSError— file not readable
from aegisq import SecurityLevelfrom aegisq.keys import load_public_key
# PEM file — you must know the levelpk = load_public_key("recipient.pem", level=SecurityLevel.ML_KEM_768)
# JSON file — level is read from the filepk = load_public_key("recipient.json")Secret-Key Persistence (Encrypted)
Section titled “Secret-Key Persistence (Encrypted)”save_secret_key
Section titled “save_secret_key”def save_secret_key(keypair: KeyPair, path: str | Path, *, password: bytes) -> NoneEncrypts the secret key with AES-256-GCM (key derived via HKDF-SHA3-256 from your password) and writes it as Encrypted PEM.
The file looks like:
-----BEGIN ENCRYPTED ML-KEM PRIVATE KEY-----<Base64 STANDARD of encrypted blob>-----END ENCRYPTED ML-KEM PRIVATE KEY-----The blob contains magic || version || level_id || salt (16 B) || nonce (12 B) || ciphertext || tag (16 B). The bridge uses getrandom::fill for fresh salt and nonce and propagates RNG failures; repeat exports are randomized, not guaranteed distinct.
Raises: RngError if the OS CSPRNG is unavailable; OSError on filesystem failure.
from aegisq.keys import save_secret_key
import secretspassword = secrets.token_bytes(32) # Retain securely for later recoverysave_secret_key(keypair, "private.key", password=password)load_secret_key
Section titled “load_secret_key”def load_secret_key(path: str | Path, *, password: bytes) -> bytesReads an Encrypted PEM file, verifies the auth tag, and returns the secret key as bytes.
Raises:
DecryptionError— wrong password or tampered fileKeySerializationError— invalid PEM header, Base64, or magic/versionOSError— file not readable
from aegisq.keys import load_secret_key
sk = load_secret_key("private.key", password=password)plaintext = cipher.decrypt(encrypted_package, sk)String-Only Convenience Functions
Section titled “String-Only Convenience Functions”These three functions produce or consume str instead of files, which is useful when keys live in environment variables, secret managers, or database columns.
public_key_to_pem
Section titled “public_key_to_pem”def public_key_to_pem(keypair: KeyPair) -> strEquivalent to keypair.public_key_pem() exposed as a module-level function for ergonomic imports.
public_key_to_json
Section titled “public_key_to_json”def public_key_to_json(keypair: KeyPair) -> strEquivalent to keypair.public_key_json() exposed as a module-level function for ergonomic imports.
secret_key_to_pem
Section titled “secret_key_to_pem”def secret_key_to_pem(keypair: KeyPair, *, password: bytes) -> strEquivalent to keypair.export_secret_key_pem(password) exposed as a module-level function for ergonomic imports.
End-to-End Example
Section titled “End-to-End Example”from aegisq import AegisCipher, SecurityLevelfrom aegisq.keys import ( save_public_key, load_public_key, save_secret_key, load_secret_key,)
# Receiver side: generate and persistcipher = AegisCipher()keypair = cipher.generate_keypair()save_public_key(keypair, "alice.pub.pem", fmt="pem")import secretspassword = secrets.token_bytes(32) # Store separately in a secret managersave_secret_key(keypair, "alice.sec.pem", password=password)
# Later (or in another process): load backcipher = AegisCipher(level=SecurityLevel.ML_KEM_768)pub = load_public_key("alice.pub.pem", level=SecurityLevel.ML_KEM_768)sec = load_secret_key("alice.sec.pem", password=password)
# Use as normalpackage = cipher.encrypt(b"secret message", pub)plaintext = cipher.decrypt(package, sec)assert plaintext == b"secret message"Format Reference
Section titled “Format Reference”PEM Public Key
Section titled “PEM Public Key”-----BEGIN ML-KEM PUBLIC KEY-----<Base64 STANDARD of public_key bytes>-----END ML-KEM PUBLIC KEY-----JSON Public Key
Section titled “JSON Public Key”{ "algorithm": "ML-KEM", "level": "ML_KEM_768", "public_key": "<Base64 URL-safe without padding>"}Encrypted PEM Secret Key
Section titled “Encrypted PEM Secret Key”-----BEGIN ENCRYPTED ML-KEM PRIVATE KEY-----<Base64 STANDARD of: magic || version || level_id || salt || nonce || ciphertext || tag>-----END ENCRYPTED ML-KEM PRIVATE KEY-----The internal blob format is an implementation detail — treat it as opaque. Public file-based decryption goes through aegisq.keys.load_secret_key(path, password=...); callers should not import the native bridge directly.
See Also
Section titled “See Also”KeyPair— the underlying class with raw bytes and serialization methodsMlKem— low-level KEM API withload_public_key_b64- Exceptions —
KeySerializationError,DecryptionErrorraised by these helpers