Skip to content

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.

FunctionDirectionFormatEncryption
save_public_keyKeyPair → filePEM (default) or JSONNone
load_public_keyfile → bytesAuto-detect PEM/JSONNone
save_secret_keyKeyPair → fileEncrypted PEMAES-256-GCM (HKDF-SHA3-256)
load_secret_keyfile → bytesEncrypted PEMAES-256-GCM (HKDF-SHA3-256)
public_key_to_pemKeyPair → strPEMNone
public_key_to_jsonKeyPair → strJSONNone
secret_key_to_pemKeyPair → strEncrypted PEMAES-256-GCM (HKDF-SHA3-256)
def save_public_key(keypair: KeyPair, path: str | Path, *, fmt: str = "pem") -> None

Writes the public key to path in either PEM (default) or JSON format.

ParameterTypeDefaultDescription
keypairKeyPair—Source keypair.
pathstr | Path—Destination file. Use .pem for PEM or .json for JSON.
fmtstr"pem"Either "pem" or "json".

Raises: ValueError if fmt is not "pem" or "json"; OSError on filesystem failure.

from aegisq import AegisCipher
from 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")
def load_public_key(path: str | Path, *, level: SecurityLevel | None = None) -> bytes

Reads 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 supply level=)
  • { → JSON (level is read from the "level" field and level argument is ignored)
ParameterTypeDefaultDescription
pathstr | Path—Source file.
levelSecurityLevel | NoneNoneRequired for PEM. Ignored for JSON.

Returns: bytes — the public key.

Raises:

  • ValueError — file format not recognizable, or PEM file without level=
  • KeySerializationError — malformed PEM header or JSON fields
  • InvalidParameterError — decoded size does not match the level
  • OSError — file not readable
from aegisq import SecurityLevel
from aegisq.keys import load_public_key
# PEM file — you must know the level
pk = load_public_key("recipient.pem", level=SecurityLevel.ML_KEM_768)
# JSON file — level is read from the file
pk = load_public_key("recipient.json")
def save_secret_key(keypair: KeyPair, path: str | Path, *, password: bytes) -> None

Encrypts 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 secrets
password = secrets.token_bytes(32) # Retain securely for later recovery
save_secret_key(keypair, "private.key", password=password)
def load_secret_key(path: str | Path, *, password: bytes) -> bytes

Reads an Encrypted PEM file, verifies the auth tag, and returns the secret key as bytes.

Raises:

  • DecryptionError — wrong password or tampered file
  • KeySerializationError — invalid PEM header, Base64, or magic/version
  • OSError — file not readable
from aegisq.keys import load_secret_key
sk = load_secret_key("private.key", password=password)
plaintext = cipher.decrypt(encrypted_package, sk)

These three functions produce or consume str instead of files, which is useful when keys live in environment variables, secret managers, or database columns.

def public_key_to_pem(keypair: KeyPair) -> str

Equivalent to keypair.public_key_pem() exposed as a module-level function for ergonomic imports.

def public_key_to_json(keypair: KeyPair) -> str

Equivalent to keypair.public_key_json() exposed as a module-level function for ergonomic imports.

def secret_key_to_pem(keypair: KeyPair, *, password: bytes) -> str

Equivalent to keypair.export_secret_key_pem(password) exposed as a module-level function for ergonomic imports.

from aegisq import AegisCipher, SecurityLevel
from aegisq.keys import (
save_public_key, load_public_key,
save_secret_key, load_secret_key,
)
# Receiver side: generate and persist
cipher = AegisCipher()
keypair = cipher.generate_keypair()
save_public_key(keypair, "alice.pub.pem", fmt="pem")
import secrets
password = secrets.token_bytes(32) # Store separately in a secret manager
save_secret_key(keypair, "alice.sec.pem", password=password)
# Later (or in another process): load back
cipher = 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 normal
package = cipher.encrypt(b"secret message", pub)
plaintext = cipher.decrypt(package, sec)
assert plaintext == b"secret message"
-----BEGIN ML-KEM PUBLIC KEY-----
<Base64 STANDARD of public_key bytes>
-----END ML-KEM PUBLIC KEY-----
{
"algorithm": "ML-KEM",
"level": "ML_KEM_768",
"public_key": "<Base64 URL-safe without padding>"
}
-----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.

  • KeyPair — the underlying class with raw bytes and serialization methods
  • MlKem — low-level KEM API with load_public_key_b64
  • Exceptions — KeySerializationError, DecryptionError raised by these helpers