Source code for quantum_safe.backends

"""
quantum_safe.backends
~~~~~~~~~~~~~~~~~~~~~

Backend registry and auto-selection logic.

Backends are loaded lazily — we don't import liboqs until someone actually
needs it. This keeps the import time fast even when liboqs isn't installed.

The selection priority for auto mode:
  1. rustcrypto  — preferred for FIPS-subset algorithms (ML-KEM, ML-DSA,
                   SLH-DSA). Fastest on native Python, WASM-compatible.
  2. liboqs      — fallback for the full algorithm set (includes BIKE,
                   FrodoKEM, HQC, etc. that RustCrypto doesn't have yet).
  3. noble       — JS/WASM environments only. Never selected in native Python.

You can override selection with the QUANTUM_SAFE_BACKEND environment variable
or the `backend=` parameter on KEM/Sign constructors.
"""

from __future__ import annotations

import os
from functools import cache
from typing import TYPE_CHECKING

from quantum_safe.exceptions import BackendNotAvailable

if TYPE_CHECKING:
    from quantum_safe.backends.base import AbstractKEMBackend, AbstractSignatureBackend


# All known backend names (for validation)
_KNOWN_BACKENDS = {"liboqs", "rustcrypto", "noble", "auto"}


@cache
def _load_liboqs_kem() -> AbstractKEMBackend:
    """Load and return the liboqs KEM backend. Cached after first load."""
    from quantum_safe.backends.liboqs import LiboqsKEMBackend

    return LiboqsKEMBackend()


@cache
def _load_rustcrypto_kem() -> AbstractKEMBackend:
    from quantum_safe.backends.rustcrypto import RustCryptoKEMBackend

    return RustCryptoKEMBackend()


@cache
def _load_liboqs_sig() -> AbstractSignatureBackend:
    from quantum_safe.backends.liboqs import LiboqsSignatureBackend

    return LiboqsSignatureBackend()


@cache
def _load_rustcrypto_sig() -> AbstractSignatureBackend:
    from quantum_safe.backends.rustcrypto import RustCryptoSignatureBackend

    return RustCryptoSignatureBackend()


[docs] def get_kem_backend(name: str = "auto") -> AbstractKEMBackend: """Return the KEM backend for the given name. Args: name: "auto", "liboqs", or "rustcrypto". Falls back through the priority list if "auto". Raises: BackendNotAvailable: if the requested backend is not installed. ValueError: if the backend name is unknown. """ env_override = os.environ.get("QUANTUM_SAFE_BACKEND", "").strip().lower() if env_override and name == "auto": name = env_override if name not in _KNOWN_BACKENDS: raise ValueError(f"Unknown backend '{name}'. Valid options: {sorted(_KNOWN_BACKENDS)}") if name == "auto": return _auto_select_kem_backend() if name == "rustcrypto": b = _load_rustcrypto_kem() if not b.is_available(): raise BackendNotAvailable("rustcrypto") return b if name == "liboqs": b = _load_liboqs_kem() if not b.is_available(): raise BackendNotAvailable("liboqs") return b if name == "noble": raise BackendNotAvailable("noble") # noble is JS-only raise BackendNotAvailable(name)
[docs] def get_signature_backend(name: str = "auto") -> AbstractSignatureBackend: """Return the signature backend for the given name.""" env_override = os.environ.get("QUANTUM_SAFE_BACKEND", "").strip().lower() if env_override and name == "auto": name = env_override if name not in _KNOWN_BACKENDS: raise ValueError(f"Unknown backend '{name}'. Valid options: {sorted(_KNOWN_BACKENDS)}") if name == "auto": return _auto_select_sig_backend() if name == "rustcrypto": b = _load_rustcrypto_sig() if not b.is_available(): raise BackendNotAvailable("rustcrypto") return b if name == "liboqs": b = _load_liboqs_sig() if not b.is_available(): raise BackendNotAvailable("liboqs") return b raise BackendNotAvailable(name)
def _auto_select_kem_backend() -> AbstractKEMBackend: """Try backends in priority order, return the first available one.""" for loader in (_load_rustcrypto_kem, _load_liboqs_kem): try: b = loader() if b.is_available(): return b except Exception: # noqa: BLE001, S112 continue raise BackendNotAvailable("auto") def _auto_select_sig_backend() -> AbstractSignatureBackend: """Try signature backends in priority order.""" for loader in (_load_rustcrypto_sig, _load_liboqs_sig): try: b = loader() if b.is_available(): return b except Exception: # noqa: BLE001, S112 continue raise BackendNotAvailable("auto")
[docs] def list_available_backends() -> dict[str, bool]: """Return availability status for all known backends. Useful for diagnostics: `python -c "from quantum_safe.backends import list_available_backends; print(list_available_backends())"`. """ results: dict[str, bool] = {} for name, loader in [("rustcrypto", _load_rustcrypto_kem), ("liboqs", _load_liboqs_kem)]: try: b = loader() results[name] = b.is_available() except Exception: # noqa: BLE001 results[name] = False results["noble"] = False # always False in Python return results