From 22299a16765940907a3a85dfb295ad17d2d77452 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 5 Nov 2025 06:14:07 +0000 Subject: [PATCH] Initial commit --- aegis/__init__.py | 0 aegis/_loader.py | 505 +++++++++++++++++++++ aegis/aegis128l.py | 848 ++++++++++++++++++++++++++++++++++++ aegis/aegis128x2.py | 848 ++++++++++++++++++++++++++++++++++++ aegis/aegis128x4.py | 848 ++++++++++++++++++++++++++++++++++++ aegis/aegis256.py | 848 ++++++++++++++++++++++++++++++++++++ aegis/aegis256x2.py | 848 ++++++++++++++++++++++++++++++++++++ aegis/aegis256x4.py | 848 ++++++++++++++++++++++++++++++++++++ examples/aegis256x4_demo.py | 97 +++++ examples/benchmark.py | 105 +++++ pyproject.toml | 20 + 11 files changed, 5815 insertions(+) create mode 100644 aegis/__init__.py create mode 100644 aegis/_loader.py create mode 100644 aegis/aegis128l.py create mode 100644 aegis/aegis128x2.py create mode 100644 aegis/aegis128x4.py create mode 100644 aegis/aegis256.py create mode 100644 aegis/aegis256x2.py create mode 100644 aegis/aegis256x4.py create mode 100644 examples/aegis256x4_demo.py create mode 100644 examples/benchmark.py create mode 100644 pyproject.toml diff --git a/aegis/__init__.py b/aegis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aegis/_loader.py b/aegis/_loader.py new file mode 100644 index 0000000..435250c --- /dev/null +++ b/aegis/_loader.py @@ -0,0 +1,505 @@ +"""Dynamic loader for libaegis using CFFI (ABI mode). + +This module avoids compiling any C shim. It loads the shared library built by +the project (e.g., build-shared/libaegis.so) or a system-installed libaegis. + +Environment variables: +- AEGIS_LIB_PATH: full path to the libaegis shared library to load +- AEGIS_LIB_DIR: directory containing the shared library (libaegis.so) + +Exports: +- ffi: a cffi.FFI instance with the libaegis API declared +- lib: the loaded libaegis shared library (ffi.dlopen) +- libc: the C runtime (for posix_memalign/free on POSIX) +- alloc_aligned(size, alignment): return (void*) pointer with requested alignment +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path +from typing import Any, Iterable + +from cffi import FFI + +try: + # ctypes is only used to resolve libc reliably across platforms + import ctypes.util as _ctypes_util # type: ignore +except Exception: # pragma: no cover - very unlikely to happen + _ctypes_util = None # type: ignore[assignment] + + +ffi = FFI() + +# Public API from headers (aegis.h and all aegis variant headers). Keep it macro-free. +ffi.cdef( + r""" + typedef unsigned char uint8_t; + typedef unsigned long size_t; + + /* aegis.h */ + int aegis_init(void); + int aegis_verify_16(const uint8_t *x, const uint8_t *y); + int aegis_verify_32(const uint8_t *x, const uint8_t *y); + + /* aegis128l.h */ + typedef struct { + /* CRYPTO_ALIGN(32) */ uint8_t opaque[256]; + } aegis128l_state; + + typedef struct { + /* CRYPTO_ALIGN(32) */ uint8_t opaque[384]; + } aegis128l_mac_state; + + size_t aegis128l_keybytes(void); + size_t aegis128l_npubbytes(void); + size_t aegis128l_abytes_min(void); + size_t aegis128l_abytes_max(void); + size_t aegis128l_tailbytes_max(void); + + int aegis128l_encrypt_detached(uint8_t *c, uint8_t *mac, size_t maclen, const uint8_t *m, + size_t mlen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis128l_decrypt_detached(uint8_t *m, const uint8_t *c, size_t clen, const uint8_t *mac, + size_t maclen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis128l_encrypt(uint8_t *c, size_t maclen, const uint8_t *m, size_t mlen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + int aegis128l_decrypt(uint8_t *m, const uint8_t *c, size_t clen, size_t maclen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + void aegis128l_state_init(aegis128l_state *st_, const uint8_t *ad, size_t adlen, + const uint8_t *npub, const uint8_t *k); + + int aegis128l_state_encrypt_update(aegis128l_state *st_, uint8_t *c, size_t clen_max, + size_t *written, const uint8_t *m, size_t mlen); + + int aegis128l_state_encrypt_detached_final(aegis128l_state *st_, uint8_t *c, size_t clen_max, + size_t *written, uint8_t *mac, size_t maclen); + + int aegis128l_state_encrypt_final(aegis128l_state *st_, uint8_t *c, size_t clen_max, + size_t *written, size_t maclen); + + int aegis128l_state_decrypt_detached_update(aegis128l_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *c, size_t clen); + + int aegis128l_state_decrypt_detached_final(aegis128l_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *mac, size_t maclen); + + void aegis128l_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); + void aegis128l_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, size_t mlen, + const uint8_t *npub, const uint8_t *k); + void aegis128l_decrypt_unauthenticated(uint8_t *m, const uint8_t *c, size_t clen, + const uint8_t *npub, const uint8_t *k); + + void aegis128l_mac_init(aegis128l_mac_state *st_, const uint8_t *k, const uint8_t *npub); + int aegis128l_mac_update(aegis128l_mac_state *st_, const uint8_t *m, size_t mlen); + int aegis128l_mac_final(aegis128l_mac_state *st_, uint8_t *mac, size_t maclen); + int aegis128l_mac_verify(aegis128l_mac_state *st_, const uint8_t *mac, size_t maclen); + void aegis128l_mac_reset(aegis128l_mac_state *st_); + void aegis128l_mac_state_clone(aegis128l_mac_state *dst, const aegis128l_mac_state *src); + + /* aegis128x2.h */ + typedef struct { + /* CRYPTO_ALIGN(64) */ uint8_t opaque[448]; + } aegis128x2_state; + + typedef struct { + /* CRYPTO_ALIGN(64) */ uint8_t opaque[704]; + } aegis128x2_mac_state; + + size_t aegis128x2_keybytes(void); + size_t aegis128x2_npubbytes(void); + size_t aegis128x2_abytes_min(void); + size_t aegis128x2_abytes_max(void); + size_t aegis128x2_tailbytes_max(void); + + int aegis128x2_encrypt_detached(uint8_t *c, uint8_t *mac, size_t maclen, const uint8_t *m, + size_t mlen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis128x2_decrypt_detached(uint8_t *m, const uint8_t *c, size_t clen, const uint8_t *mac, + size_t maclen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis128x2_encrypt(uint8_t *c, size_t maclen, const uint8_t *m, size_t mlen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + int aegis128x2_decrypt(uint8_t *m, const uint8_t *c, size_t clen, size_t maclen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + void aegis128x2_state_init(aegis128x2_state *st_, const uint8_t *ad, size_t adlen, + const uint8_t *npub, const uint8_t *k); + + int aegis128x2_state_encrypt_update(aegis128x2_state *st_, uint8_t *c, size_t clen_max, + size_t *written, const uint8_t *m, size_t mlen); + + int aegis128x2_state_encrypt_detached_final(aegis128x2_state *st_, uint8_t *c, size_t clen_max, + size_t *written, uint8_t *mac, size_t maclen); + + int aegis128x2_state_encrypt_final(aegis128x2_state *st_, uint8_t *c, size_t clen_max, + size_t *written, size_t maclen); + + int aegis128x2_state_decrypt_detached_update(aegis128x2_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *c, size_t clen); + + int aegis128x2_state_decrypt_detached_final(aegis128x2_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *mac, size_t maclen); + + void aegis128x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); + void aegis128x2_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, size_t mlen, + const uint8_t *npub, const uint8_t *k); + void aegis128x2_decrypt_unauthenticated(uint8_t *m, const uint8_t *c, size_t clen, + const uint8_t *npub, const uint8_t *k); + + void aegis128x2_mac_init(aegis128x2_mac_state *st_, const uint8_t *k, const uint8_t *npub); + int aegis128x2_mac_update(aegis128x2_mac_state *st_, const uint8_t *m, size_t mlen); + int aegis128x2_mac_final(aegis128x2_mac_state *st_, uint8_t *mac, size_t maclen); + int aegis128x2_mac_verify(aegis128x2_mac_state *st_, const uint8_t *mac, size_t maclen); + void aegis128x2_mac_reset(aegis128x2_mac_state *st_); + void aegis128x2_mac_state_clone(aegis128x2_mac_state *dst, const aegis128x2_mac_state *src); + + /* aegis128x4.h */ + typedef struct { + /* CRYPTO_ALIGN(64) */ uint8_t opaque[832]; + } aegis128x4_state; + + typedef struct { + /* CRYPTO_ALIGN(64) */ uint8_t opaque[1344]; + } aegis128x4_mac_state; + + size_t aegis128x4_keybytes(void); + size_t aegis128x4_npubbytes(void); + size_t aegis128x4_abytes_min(void); + size_t aegis128x4_abytes_max(void); + size_t aegis128x4_tailbytes_max(void); + + int aegis128x4_encrypt_detached(uint8_t *c, uint8_t *mac, size_t maclen, const uint8_t *m, + size_t mlen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis128x4_decrypt_detached(uint8_t *m, const uint8_t *c, size_t clen, const uint8_t *mac, + size_t maclen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis128x4_encrypt(uint8_t *c, size_t maclen, const uint8_t *m, size_t mlen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + int aegis128x4_decrypt(uint8_t *m, const uint8_t *c, size_t clen, size_t maclen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + void aegis128x4_state_init(aegis128x4_state *st_, const uint8_t *ad, size_t adlen, + const uint8_t *npub, const uint8_t *k); + + int aegis128x4_state_encrypt_update(aegis128x4_state *st_, uint8_t *c, size_t clen_max, + size_t *written, const uint8_t *m, size_t mlen); + + int aegis128x4_state_encrypt_detached_final(aegis128x4_state *st_, uint8_t *c, size_t clen_max, + size_t *written, uint8_t *mac, size_t maclen); + + int aegis128x4_state_encrypt_final(aegis128x4_state *st_, uint8_t *c, size_t clen_max, + size_t *written, size_t maclen); + + int aegis128x4_state_decrypt_detached_update(aegis128x4_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *c, size_t clen); + + int aegis128x4_state_decrypt_detached_final(aegis128x4_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *mac, size_t maclen); + + void aegis128x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); + void aegis128x4_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, size_t mlen, + const uint8_t *npub, const uint8_t *k); + void aegis128x4_decrypt_unauthenticated(uint8_t *m, const uint8_t *c, size_t clen, + const uint8_t *npub, const uint8_t *k); + + void aegis128x4_mac_init(aegis128x4_mac_state *st_, const uint8_t *k, const uint8_t *npub); + int aegis128x4_mac_update(aegis128x4_mac_state *st_, const uint8_t *m, size_t mlen); + int aegis128x4_mac_final(aegis128x4_mac_state *st_, uint8_t *mac, size_t maclen); + int aegis128x4_mac_verify(aegis128x4_mac_state *st_, const uint8_t *mac, size_t maclen); + void aegis128x4_mac_reset(aegis128x4_mac_state *st_); + void aegis128x4_mac_state_clone(aegis128x4_mac_state *dst, const aegis128x4_mac_state *src); + + /* aegis256.h */ + typedef struct { + /* CRYPTO_ALIGN(16) */ uint8_t opaque[192]; + } aegis256_state; + + typedef struct { + /* CRYPTO_ALIGN(16) */ uint8_t opaque[288]; + } aegis256_mac_state; + + size_t aegis256_keybytes(void); + size_t aegis256_npubbytes(void); + size_t aegis256_abytes_min(void); + size_t aegis256_abytes_max(void); + size_t aegis256_tailbytes_max(void); + + int aegis256_encrypt_detached(uint8_t *c, uint8_t *mac, size_t maclen, const uint8_t *m, + size_t mlen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis256_decrypt_detached(uint8_t *m, const uint8_t *c, size_t clen, const uint8_t *mac, + size_t maclen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis256_encrypt(uint8_t *c, size_t maclen, const uint8_t *m, size_t mlen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + int aegis256_decrypt(uint8_t *m, const uint8_t *c, size_t clen, size_t maclen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + void aegis256_state_init(aegis256_state *st_, const uint8_t *ad, size_t adlen, + const uint8_t *npub, const uint8_t *k); + + int aegis256_state_encrypt_update(aegis256_state *st_, uint8_t *c, size_t clen_max, + size_t *written, const uint8_t *m, size_t mlen); + + int aegis256_state_encrypt_detached_final(aegis256_state *st_, uint8_t *c, size_t clen_max, + size_t *written, uint8_t *mac, size_t maclen); + + int aegis256_state_encrypt_final(aegis256_state *st_, uint8_t *c, size_t clen_max, + size_t *written, size_t maclen); + + int aegis256_state_decrypt_detached_update(aegis256_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *c, size_t clen); + + int aegis256_state_decrypt_detached_final(aegis256_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *mac, size_t maclen); + + void aegis256_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); + void aegis256_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, size_t mlen, + const uint8_t *npub, const uint8_t *k); + void aegis256_decrypt_unauthenticated(uint8_t *m, const uint8_t *c, size_t clen, + const uint8_t *npub, const uint8_t *k); + + void aegis256_mac_init(aegis256_mac_state *st_, const uint8_t *k, const uint8_t *npub); + int aegis256_mac_update(aegis256_mac_state *st_, const uint8_t *m, size_t mlen); + int aegis256_mac_final(aegis256_mac_state *st_, uint8_t *mac, size_t maclen); + int aegis256_mac_verify(aegis256_mac_state *st_, const uint8_t *mac, size_t maclen); + void aegis256_mac_reset(aegis256_mac_state *st_); + void aegis256_mac_state_clone(aegis256_mac_state *dst, const aegis256_mac_state *src); + + /* aegis256x2.h */ + typedef struct { + /* CRYPTO_ALIGN(32) */ uint8_t opaque[320]; + } aegis256x2_state; + + typedef struct { + /* CRYPTO_ALIGN(32) */ uint8_t opaque[512]; + } aegis256x2_mac_state; + + size_t aegis256x2_keybytes(void); + size_t aegis256x2_npubbytes(void); + size_t aegis256x2_abytes_min(void); + size_t aegis256x2_abytes_max(void); + size_t aegis256x2_tailbytes_max(void); + + int aegis256x2_encrypt_detached(uint8_t *c, uint8_t *mac, size_t maclen, const uint8_t *m, + size_t mlen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis256x2_decrypt_detached(uint8_t *m, const uint8_t *c, size_t clen, const uint8_t *mac, + size_t maclen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis256x2_encrypt(uint8_t *c, size_t maclen, const uint8_t *m, size_t mlen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + int aegis256x2_decrypt(uint8_t *m, const uint8_t *c, size_t clen, size_t maclen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + void aegis256x2_state_init(aegis256x2_state *st_, const uint8_t *ad, size_t adlen, + const uint8_t *npub, const uint8_t *k); + + int aegis256x2_state_encrypt_update(aegis256x2_state *st_, uint8_t *c, size_t clen_max, + size_t *written, const uint8_t *m, size_t mlen); + + int aegis256x2_state_encrypt_detached_final(aegis256x2_state *st_, uint8_t *c, size_t clen_max, + size_t *written, uint8_t *mac, size_t maclen); + + int aegis256x2_state_encrypt_final(aegis256x2_state *st_, uint8_t *c, size_t clen_max, + size_t *written, size_t maclen); + + int aegis256x2_state_decrypt_detached_update(aegis256x2_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *c, size_t clen); + + int aegis256x2_state_decrypt_detached_final(aegis256x2_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *mac, size_t maclen); + + void aegis256x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); + void aegis256x2_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, size_t mlen, + const uint8_t *npub, const uint8_t *k); + void aegis256x2_decrypt_unauthenticated(uint8_t *m, const uint8_t *c, size_t clen, + const uint8_t *npub, const uint8_t *k); + + void aegis256x2_mac_init(aegis256x2_mac_state *st_, const uint8_t *k, const uint8_t *npub); + int aegis256x2_mac_update(aegis256x2_mac_state *st_, const uint8_t *m, size_t mlen); + int aegis256x2_mac_final(aegis256x2_mac_state *st_, uint8_t *mac, size_t maclen); + int aegis256x2_mac_verify(aegis256x2_mac_state *st_, const uint8_t *mac, size_t maclen); + void aegis256x2_mac_reset(aegis256x2_mac_state *st_); + void aegis256x2_mac_state_clone(aegis256x2_mac_state *dst, const aegis256x2_mac_state *src); + + /* aegis256x4.h */ + typedef struct { + /* CRYPTO_ALIGN(64) */ uint8_t opaque[576]; + } aegis256x4_state; + + typedef struct { + /* CRYPTO_ALIGN(64) */ uint8_t opaque[960]; + } aegis256x4_mac_state; + + size_t aegis256x4_keybytes(void); + size_t aegis256x4_npubbytes(void); + size_t aegis256x4_abytes_min(void); + size_t aegis256x4_abytes_max(void); + size_t aegis256x4_tailbytes_max(void); + + int aegis256x4_encrypt_detached(uint8_t *c, uint8_t *mac, size_t maclen, const uint8_t *m, + size_t mlen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis256x4_decrypt_detached(uint8_t *m, const uint8_t *c, size_t clen, const uint8_t *mac, + size_t maclen, const uint8_t *ad, size_t adlen, const uint8_t *npub, + const uint8_t *k); + + int aegis256x4_encrypt(uint8_t *c, size_t maclen, const uint8_t *m, size_t mlen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + int aegis256x4_decrypt(uint8_t *m, const uint8_t *c, size_t clen, size_t maclen, const uint8_t *ad, + size_t adlen, const uint8_t *npub, const uint8_t *k); + + void aegis256x4_state_init(aegis256x4_state *st_, const uint8_t *ad, size_t adlen, + const uint8_t *npub, const uint8_t *k); + + int aegis256x4_state_encrypt_update(aegis256x4_state *st_, uint8_t *c, size_t clen_max, + size_t *written, const uint8_t *m, size_t mlen); + + int aegis256x4_state_encrypt_detached_final(aegis256x4_state *st_, uint8_t *c, size_t clen_max, + size_t *written, uint8_t *mac, size_t maclen); + + int aegis256x4_state_encrypt_final(aegis256x4_state *st_, uint8_t *c, size_t clen_max, + size_t *written, size_t maclen); + + int aegis256x4_state_decrypt_detached_update(aegis256x4_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *c, size_t clen); + + int aegis256x4_state_decrypt_detached_final(aegis256x4_state *st_, uint8_t *m, size_t mlen_max, + size_t *written, const uint8_t *mac, size_t maclen); + + void aegis256x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k); + void aegis256x4_encrypt_unauthenticated(uint8_t *c, const uint8_t *m, size_t mlen, + const uint8_t *npub, const uint8_t *k); + void aegis256x4_decrypt_unauthenticated(uint8_t *m, const uint8_t *c, size_t clen, + const uint8_t *npub, const uint8_t *k); + + void aegis256x4_mac_init(aegis256x4_mac_state *st_, const uint8_t *k, const uint8_t *npub); + int aegis256x4_mac_update(aegis256x4_mac_state *st_, const uint8_t *m, size_t mlen); + int aegis256x4_mac_final(aegis256x4_mac_state *st_, uint8_t *mac, size_t maclen); + int aegis256x4_mac_verify(aegis256x4_mac_state *st_, const uint8_t *mac, size_t maclen); + void aegis256x4_mac_reset(aegis256x4_mac_state *st_); + void aegis256x4_mac_state_clone(aegis256x4_mac_state *dst, const aegis256x4_mac_state *src); + + /* libc bits for aligned allocation on POSIX */ + int posix_memalign(void **memptr, size_t alignment, size_t size); + void free(void *ptr); + """ +) + + +def _candidate_paths() -> Iterable[str]: + # 1) Explicit override + p = os.environ.get("AEGIS_LIB_PATH") + if p: + yield p + + # 2) Directory override + d = os.environ.get("AEGIS_LIB_DIR") + if d: + yield str(Path(d) / _platform_lib_name()) + + # 3) Local builds relative to this file + here = Path(__file__).resolve() + repo_root = here.parents[2] # python/aegis/_loader.py -> repo root + for rel in ( + "build-shared-clang/libaegis.so", + "build-shared/libaegis.so", + "build/libaegis.so", + "zig-out/lib/libaegis.so", + ): + path = repo_root / rel + if path.exists(): + yield str(path) + + # 4) Let the dynamic loader search system paths + yield _platform_lib_name() # e.g. "libaegis.so" or "aegis" + + +def _platform_lib_name() -> str: + if sys.platform.startswith("linux"): + return "libaegis.so" + if sys.platform == "darwin": + return "libaegis.dylib" + if os.name == "nt": + return "aegis.dll" + return "libaegis.so" + + +def _load_libaegis(): + last_err: Exception | None = None + for cand in _candidate_paths(): + try: + lib = ffi.dlopen(str(cand)) + return lib + except Exception as e: # try next candidate + last_err = e + continue + # If we get here, we couldn't load the library + hint = ( + "Set AEGIS_LIB_PATH to the full path of libaegis or AEGIS_LIB_DIR to the folder " + "containing it, or install libaegis system-wide." + ) + raise OSError(f"Could not load libaegis: {last_err}\n{hint}") + + +def _load_libc(): + # Use ctypes.util to find a usable libc name; fallback to None-dlopen on POSIX + if _ctypes_util is not None: + libc_name = _ctypes_util.find_library("c") # type: ignore[attr-defined] + if libc_name: + try: + return ffi.dlopen(libc_name) + except Exception: + pass + # Fallback: try process globals (works on many Unix platforms) + try: + return ffi.dlopen(None) + except Exception as e: + raise OSError(f"Unable to load libc for aligned allocation: {e}") + + +lib: Any = _load_libaegis() +libc: Any = _load_libc() + +# Initialize CPU feature selection (recommended by the library) +try: + lib.aegis_init() +except Exception: + # Non-fatal; functions will still work, maybe slower + pass + + +def alloc_aligned(size: int, alignment: int = 64): + """Allocate aligned memory via posix_memalign(); returns a void* cdata. + + The returned pointer must be freed with libc.free(). Attach a GC finalizer + at call sites using ffi.gc(ptr, libc.free) after casting to the target type. + """ + memptr = ffi.new("void **") + rc = libc.posix_memalign(memptr, alignment, size) + if rc != 0 or memptr[0] == ffi.NULL: + raise MemoryError(f"posix_memalign({alignment}, {size}) failed with rc={rc}") + return memptr[0] diff --git a/aegis/aegis128l.py b/aegis/aegis128l.py new file mode 100644 index 0000000..a98bad1 --- /dev/null +++ b/aegis/aegis128l.py @@ -0,0 +1,848 @@ +"""aegis128l Python submodule. + +Simplified API: single functions can return newly allocated buffers or write +into user-provided buffers via optional `into=` (and `mac_into=` for detached). + +Error return codes from the C library raise ValueError. +""" + +import errno +from collections.abc import Buffer + +from ._loader import alloc_aligned, ffi, libc +from ._loader import lib as _lib + +# Constants exposed as functions in C; mirror them as integers at module import time +KEYBYTES = _lib.aegis128l_keybytes() +NPUBBYTES = _lib.aegis128l_npubbytes() +ABYTES_MIN = _lib.aegis128l_abytes_min() +ABYTES_MAX = _lib.aegis128l_abytes_max() +TAILBYTES_MAX = _lib.aegis128l_tailbytes_max() + + +def _ptr(buf): + """Return an ffi pointer for a Python buffer, or NULL for None. + + Args: + buf: Any object supporting the Python buffer protocol, or None. + + Returns: + An ffi pointer obtained with ffi.from_buffer, or ffi.NULL when buf is None. + """ + return ffi.NULL if buf is None else ffi.from_buffer(buf) + + +def encrypt_detached( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + ct_into: Buffer | None = None, + mac_into: Buffer | None = None, +) -> tuple[memoryview, memoryview]: + """Encrypt message with associated data, returning ciphertext and MAC separately. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + ct_into: Buffer to write ciphertext into (default: bytearray created). + mac_into: Buffer to write MAC into (default: bytearray created). + + Returns: + Tuple of (ciphertext, mac) + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + ct_into = memoryview(ct_into) if ct_into is not None else None + mac_into = memoryview(mac_into) if mac_into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + + c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) + mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) + if c.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + if mac.nbytes != maclen: + raise TypeError("mac_into length must equal maclen") + + rc = _lib.aegis128l_encrypt_detached( + ffi.from_buffer(c), + ffi.from_buffer(mac), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt detached failed: {err_name}") + return c, mac + + +def decrypt_detached( + nonce: Buffer, + key: Buffer, + ct: Buffer, + mac: Buffer, + ad: Buffer | None = None, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with detached MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext to decrypt. + mac: The MAC to verify. + ad: Associated data (optional). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + mac = memoryview(mac) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if m_out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + + rc = _lib.aegis128l_decrypt_detached( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + _ptr(mac), + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return memoryview(m_out) + + +def encrypt( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message with associated data, returning ciphertext with appended MAC. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write ciphertext+MAC into (default: bytearray created). + + Returns: + Ciphertext with appended MAC as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) + if out.nbytes != message.nbytes + maclen: + raise TypeError("into length must be len(message)+maclen") + + rc = _lib.aegis128l_encrypt( + ffi.from_buffer(out), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt failed: {err_name}") + return out + + +def decrypt( + nonce: Buffer, + key: Buffer, + ct: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with appended MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext with MAC to decrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if ct.nbytes < maclen: + raise TypeError("ciphertext too short for tag") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) + if m_out.nbytes != ct.nbytes - maclen: + raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + + rc = _lib.aegis128l_decrypt( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return m_out + + +def stream( + nonce: Buffer | None, + key: Buffer, + length: int | None = None, + into: Buffer | None = None, +) -> memoryview: + """Generate a stream of pseudorandom bytes. + + Args: + nonce: Nonce (32 bytes, uses zeroes for nonce if None). + key: Key (32 bytes). + length: Number of bytes to generate (required if into is None). + into: Buffer to write stream into (default: bytearray created). + + Returns: + Pseudorandom bytes as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid or neither length nor into provided. + """ + nonce = memoryview(nonce) if nonce is not None else None + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce is not None and nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if into is None and length is None: + raise TypeError("provide either into or length") + out = into if into is not None else memoryview(bytearray(int(length or 0))) + _lib.aegis128l_stream( + ffi.from_buffer(out), + out.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def encrypt_unauthenticated( + message: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message without authentication (for testing/debugging). + + Args: + message: The plaintext message to encrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write ciphertext into (default: bytearray created). + + Returns: + Ciphertext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + message = memoryview(message) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes)) + if out.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + _lib.aegis128l_encrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(message), + message.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def decrypt_unauthenticated( + ct: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext without authentication (for testing/debugging). + + Args: + ct: The ciphertext to decrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + ct = memoryview(ct) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + _lib.aegis128l_decrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(ct), + ct.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +# This is missing from C API but convenient to have here +def mac( + data: Buffer, + nonce: Buffer, + key: Buffer, + maclen: int = ABYTES_MIN, +) -> memoryview: + """Compute a MAC for the given data in one shot. + + Args: + data: Data to MAC + nonce: Nonce (32 bytes) + key: Key (32 bytes) + maclen: MAC length (16 or 32, default 16) + + Returns: + MAC bytes + """ + mac_state = Mac(nonce, key) + mac_state.update(data) + return mac_state.final(maclen) + + +class Mac: + """AEGIS-256X4 MAC state wrapper. + + Usage: + mac = Mac(nonce, key) + mac.update(data) + tag = mac.final() # defaults to 16-byte MAC + # or verify: + mac2 = Mac(nonce, key); mac2.update(data); mac2.verify(tag) + """ + + __slots__ = ("_st", "_nonce", "_key") + + def __init__( + self, + nonce: Buffer, + key: Buffer, + _other=None, + ) -> None: + """Initialize a MAC state with a nonce and key. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + raw = alloc_aligned(ffi.sizeof("aegis128l_mac_state"), 32) + st = ffi.cast("aegis128l_mac_state *", raw) + self._st = ffi.gc(st, libc.free) + if _other is not None: + _lib.aegis128l_mac_state_clone(self._st, _other._st) + return + # Normal init + nonce = memoryview(nonce) + key = memoryview(key) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES=}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES=}") + _lib.aegis128l_mac_init(self._st, _ptr(key), _ptr(nonce)) + + def __deepcopy__(self) -> "Mac": + """Return a clone of current MAC state.""" + return Mac(b"", b"", _other=self) + + clone = __deepcopy__ + + def reset(self) -> None: + """Reset the MAC state so it can be reused with the same nonce and key.""" + _lib.aegis128l_mac_reset(self._st) + + def update(self, data: Buffer) -> None: + """Absorb data into the MAC state. + + Args: + data: Bytes-like object to authenticate. + + Raises: + RuntimeError: If the underlying C function reports an error. + """ + data = memoryview(data) + rc = _lib.aegis128l_mac_update(self._st, _ptr(data), data.nbytes) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac update failed: {err_name}") + + def final( + self, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, + ) -> memoryview: + """Finalize and return the MAC tag. + + Args: + maclen: Tag length in bytes (16 or 32). Defaults to 16. + into: Optional buffer to write the tag into (default: bytearray created). + + Returns: + The tag as a memoryview; if ``into`` is provided, it views that buffer. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If finalization fails in the C library. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = into if into is not None else bytearray(maclen) + out = memoryview(out) + if out.nbytes != maclen: + raise TypeError("into length must equal maclen") + rc = _lib.aegis128l_mac_final(self._st, ffi.from_buffer(out), maclen) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac final failed: {err_name}") + return out + + def verify(self, mac: Buffer) -> bool: + """Verify a tag for the current MAC state. + + Args: + mac: The tag to verify (16 or 32 bytes). + + Returns: + True if verification succeeds. + + Raises: + TypeError: If tag length is invalid. + ValueError: If verification fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + rc = _lib.aegis128l_mac_verify(self._st, _ptr(mac), maclen) + if rc != 0: + raise ValueError("mac verification failed") + return True + + +class Encryptor: + """Incremental encryptor. + + - update(message[, into]) -> returns produced ciphertext bytes + - final([into], maclen=16) -> returns tail+tag bytes + - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental encryptor. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data to bind to the encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis128l_state"), 32) + st = ffi.cast("aegis128l_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis128l_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + """Encrypt a chunk of the message. + + Args: + message: Plaintext bytes to encrypt. + into: Optional destination buffer; must be >= len(message). + + Returns: + The ciphertext for this chunk as a memoryview; when ``into`` is + provided, a view of that buffer up to the number of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + message = memoryview(message) + out = memoryview(into if into is not None else bytearray(message.nbytes)) + if out.nbytes < message.nbytes: + raise TypeError("into length must be >= len(message)") + written = ffi.new("size_t *") + rc = _lib.aegis128l_state_encrypt_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(message), + message.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out[: int(written[0])] + + def final(self, into: Buffer | None = None, maclen: int = ABYTES_MIN) -> memoryview: + """Finalize encryption, writing any remaining bytes and the tag. + + Args: + into: Optional destination buffer for the tail and tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A memoryview of the produced bytes (tail + tag). When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If maclen is invalid. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag + out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis128l_state_encrypt_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt final failed: {err_name}") + return out[: int(written[0])] + + def final_detached( + self, + ct_into: bytearray | None = None, + mac_into: bytearray | None = None, + maclen: int = ABYTES_MIN, + ) -> tuple[bytearray, bytearray]: + """Finalize encryption, producing detached tail bytes and tag. + + Args: + ct_into: Optional destination for the remaining ciphertext tail. + mac_into: Optional destination for the tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A tuple of (tail_bytes, mac). When destination buffers are provided, + the first element is a slice of ``ct_into`` up to the number of bytes + written, and the second is ``mac_into``. + + Raises: + TypeError: If maclen is invalid or mac_into has the wrong length. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = ct_into if ct_into is not None else bytearray(TAILBYTES_MAX) + mac = mac_into if mac_into is not None else bytearray(maclen) + if len(mac) != maclen: + raise TypeError("mac_into length must equal maclen") + written = ffi.new("size_t *") + rc = _lib.aegis128l_state_encrypt_detached_final( + self._st, + ffi.from_buffer(out), + len(out), + written, + ffi.from_buffer(mac), + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt detached final failed: {err_name}") + return out[: int(written[0])], mac + + +class Decryptor: + """Incremental decryptor. + + - update(ciphertext[, into]) -> returns plaintext bytes + - final(mac[, into]) -> returns any remaining plaintext bytes + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental decryptor for detached tags. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data used during encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis128l_state"), 32) + st = ffi.cast("aegis128l_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis128l_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + """Process a chunk of ciphertext. + + Args: + ct: Ciphertext bytes (without MAC). + into: Optional destination buffer; must be >= len(ciphertext). + + Returns: + A memoryview of the decrypted bytes for this chunk. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + ct = memoryview(ct) + out = into if into is not None else bytearray(ct.nbytes) + out = memoryview(out) + if out.nbytes < ct.nbytes: + raise TypeError("into length must be >= len(ciphertext)") + written = ffi.new("size_t *") + rc = _lib.aegis128l_state_decrypt_detached_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(ct), + ct.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state decrypt update failed: {err_name}") + w = int(written[0]) + return out[:w] + + def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: + """Finalize decryption by verifying tag and flushing remaining bytes. + + Args: + mac: Tag to verify (16 or 32 bytes). + into: Optional destination buffer for the remaining plaintext bytes. + + Returns: + A memoryview of the remaining plaintext bytes. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If tag length is invalid. + ValueError: If authentication fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + out = into if into is not None else bytearray(TAILBYTES_MAX) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis128l_state_decrypt_detached_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(mac), + maclen, + ) + if rc != 0: + raise ValueError("authentication failed") + w = int(written[0]) + return out[:w] + + +def new_state(): + """Allocate and return a new aegis128l_state* with proper alignment. + + The returned object is an ffi cdata pointer with automatic finalizer. + """ + # Allocate with 64-byte alignment using libc.posix_memalign + raw = alloc_aligned(ffi.sizeof("aegis128l_state"), 32) + ptr = ffi.cast("aegis128l_state *", raw) + return ffi.gc(ptr, libc.free) + + +def new_mac_state(): + """Allocate and return a new aegis128l_mac_state* with proper alignment.""" + raw = alloc_aligned(ffi.sizeof("aegis128l_mac_state"), 32) + ptr = ffi.cast("aegis128l_mac_state *", raw) + return ffi.gc(ptr, libc.free) + + +__all__ = [ + # constants + "KEYBYTES", + "NPUBBYTES", + "ABYTES_MIN", + "ABYTES_MAX", + "TAILBYTES_MAX", + # one-shot functions + "encrypt_detached", + "decrypt_detached", + "encrypt", + "decrypt", + "stream", + "encrypt_unauthenticated", + "decrypt_unauthenticated", + "mac", + # incremental classes + "Encryptor", + "Decryptor", + "Mac", +] diff --git a/aegis/aegis128x2.py b/aegis/aegis128x2.py new file mode 100644 index 0000000..9bf91af --- /dev/null +++ b/aegis/aegis128x2.py @@ -0,0 +1,848 @@ +"""aegis128x2 Python submodule. + +Simplified API: single functions can return newly allocated buffers or write +into user-provided buffers via optional `into=` (and `mac_into=` for detached). + +Error return codes from the C library raise ValueError. +""" + +import errno +from collections.abc import Buffer + +from ._loader import alloc_aligned, ffi, libc +from ._loader import lib as _lib + +# Constants exposed as functions in C; mirror them as integers at module import time +KEYBYTES = _lib.aegis128x2_keybytes() +NPUBBYTES = _lib.aegis128x2_npubbytes() +ABYTES_MIN = _lib.aegis128x2_abytes_min() +ABYTES_MAX = _lib.aegis128x2_abytes_max() +TAILBYTES_MAX = _lib.aegis128x2_tailbytes_max() + + +def _ptr(buf): + """Return an ffi pointer for a Python buffer, or NULL for None. + + Args: + buf: Any object supporting the Python buffer protocol, or None. + + Returns: + An ffi pointer obtained with ffi.from_buffer, or ffi.NULL when buf is None. + """ + return ffi.NULL if buf is None else ffi.from_buffer(buf) + + +def encrypt_detached( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + ct_into: Buffer | None = None, + mac_into: Buffer | None = None, +) -> tuple[memoryview, memoryview]: + """Encrypt message with associated data, returning ciphertext and MAC separately. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + ct_into: Buffer to write ciphertext into (default: bytearray created). + mac_into: Buffer to write MAC into (default: bytearray created). + + Returns: + Tuple of (ciphertext, mac) + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + ct_into = memoryview(ct_into) if ct_into is not None else None + mac_into = memoryview(mac_into) if mac_into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + + c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) + mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) + if c.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + if mac.nbytes != maclen: + raise TypeError("mac_into length must equal maclen") + + rc = _lib.aegis128x2_encrypt_detached( + ffi.from_buffer(c), + ffi.from_buffer(mac), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt detached failed: {err_name}") + return c, mac + + +def decrypt_detached( + nonce: Buffer, + key: Buffer, + ct: Buffer, + mac: Buffer, + ad: Buffer | None = None, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with detached MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext to decrypt. + mac: The MAC to verify. + ad: Associated data (optional). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + mac = memoryview(mac) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if m_out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + + rc = _lib.aegis128x2_decrypt_detached( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + _ptr(mac), + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return memoryview(m_out) + + +def encrypt( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message with associated data, returning ciphertext with appended MAC. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write ciphertext+MAC into (default: bytearray created). + + Returns: + Ciphertext with appended MAC as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) + if out.nbytes != message.nbytes + maclen: + raise TypeError("into length must be len(message)+maclen") + + rc = _lib.aegis128x2_encrypt( + ffi.from_buffer(out), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt failed: {err_name}") + return out + + +def decrypt( + nonce: Buffer, + key: Buffer, + ct: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with appended MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext with MAC to decrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if ct.nbytes < maclen: + raise TypeError("ciphertext too short for tag") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) + if m_out.nbytes != ct.nbytes - maclen: + raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + + rc = _lib.aegis128x2_decrypt( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return m_out + + +def stream( + nonce: Buffer | None, + key: Buffer, + length: int | None = None, + into: Buffer | None = None, +) -> memoryview: + """Generate a stream of pseudorandom bytes. + + Args: + nonce: Nonce (32 bytes, uses zeroes for nonce if None). + key: Key (32 bytes). + length: Number of bytes to generate (required if into is None). + into: Buffer to write stream into (default: bytearray created). + + Returns: + Pseudorandom bytes as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid or neither length nor into provided. + """ + nonce = memoryview(nonce) if nonce is not None else None + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce is not None and nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if into is None and length is None: + raise TypeError("provide either into or length") + out = into if into is not None else memoryview(bytearray(int(length or 0))) + _lib.aegis128x2_stream( + ffi.from_buffer(out), + out.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def encrypt_unauthenticated( + message: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message without authentication (for testing/debugging). + + Args: + message: The plaintext message to encrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write ciphertext into (default: bytearray created). + + Returns: + Ciphertext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + message = memoryview(message) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes)) + if out.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + _lib.aegis128x2_encrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(message), + message.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def decrypt_unauthenticated( + ct: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext without authentication (for testing/debugging). + + Args: + ct: The ciphertext to decrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + ct = memoryview(ct) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + _lib.aegis128x2_decrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(ct), + ct.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +# This is missing from C API but convenient to have here +def mac( + data: Buffer, + nonce: Buffer, + key: Buffer, + maclen: int = ABYTES_MIN, +) -> memoryview: + """Compute a MAC for the given data in one shot. + + Args: + data: Data to MAC + nonce: Nonce (32 bytes) + key: Key (32 bytes) + maclen: MAC length (16 or 32, default 16) + + Returns: + MAC bytes + """ + mac_state = Mac(nonce, key) + mac_state.update(data) + return mac_state.final(maclen) + + +class Mac: + """AEGIS-256X4 MAC state wrapper. + + Usage: + mac = Mac(nonce, key) + mac.update(data) + tag = mac.final() # defaults to 16-byte MAC + # or verify: + mac2 = Mac(nonce, key); mac2.update(data); mac2.verify(tag) + """ + + __slots__ = ("_st", "_nonce", "_key") + + def __init__( + self, + nonce: Buffer, + key: Buffer, + _other=None, + ) -> None: + """Initialize a MAC state with a nonce and key. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + raw = alloc_aligned(ffi.sizeof("aegis128x2_mac_state"), 64) + st = ffi.cast("aegis128x2_mac_state *", raw) + self._st = ffi.gc(st, libc.free) + if _other is not None: + _lib.aegis128x2_mac_state_clone(self._st, _other._st) + return + # Normal init + nonce = memoryview(nonce) + key = memoryview(key) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES=}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES=}") + _lib.aegis128x2_mac_init(self._st, _ptr(key), _ptr(nonce)) + + def __deepcopy__(self) -> "Mac": + """Return a clone of current MAC state.""" + return Mac(b"", b"", _other=self) + + clone = __deepcopy__ + + def reset(self) -> None: + """Reset the MAC state so it can be reused with the same nonce and key.""" + _lib.aegis128x2_mac_reset(self._st) + + def update(self, data: Buffer) -> None: + """Absorb data into the MAC state. + + Args: + data: Bytes-like object to authenticate. + + Raises: + RuntimeError: If the underlying C function reports an error. + """ + data = memoryview(data) + rc = _lib.aegis128x2_mac_update(self._st, _ptr(data), data.nbytes) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac update failed: {err_name}") + + def final( + self, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, + ) -> memoryview: + """Finalize and return the MAC tag. + + Args: + maclen: Tag length in bytes (16 or 32). Defaults to 16. + into: Optional buffer to write the tag into (default: bytearray created). + + Returns: + The tag as a memoryview; if ``into`` is provided, it views that buffer. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If finalization fails in the C library. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = into if into is not None else bytearray(maclen) + out = memoryview(out) + if out.nbytes != maclen: + raise TypeError("into length must equal maclen") + rc = _lib.aegis128x2_mac_final(self._st, ffi.from_buffer(out), maclen) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac final failed: {err_name}") + return out + + def verify(self, mac: Buffer) -> bool: + """Verify a tag for the current MAC state. + + Args: + mac: The tag to verify (16 or 32 bytes). + + Returns: + True if verification succeeds. + + Raises: + TypeError: If tag length is invalid. + ValueError: If verification fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + rc = _lib.aegis128x2_mac_verify(self._st, _ptr(mac), maclen) + if rc != 0: + raise ValueError("mac verification failed") + return True + + +class Encryptor: + """Incremental encryptor. + + - update(message[, into]) -> returns produced ciphertext bytes + - final([into], maclen=16) -> returns tail+tag bytes + - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental encryptor. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data to bind to the encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis128x2_state"), 64) + st = ffi.cast("aegis128x2_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis128x2_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + """Encrypt a chunk of the message. + + Args: + message: Plaintext bytes to encrypt. + into: Optional destination buffer; must be >= len(message). + + Returns: + The ciphertext for this chunk as a memoryview; when ``into`` is + provided, a view of that buffer up to the number of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + message = memoryview(message) + out = memoryview(into if into is not None else bytearray(message.nbytes)) + if out.nbytes < message.nbytes: + raise TypeError("into length must be >= len(message)") + written = ffi.new("size_t *") + rc = _lib.aegis128x2_state_encrypt_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(message), + message.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out[: int(written[0])] + + def final(self, into: Buffer | None = None, maclen: int = ABYTES_MIN) -> memoryview: + """Finalize encryption, writing any remaining bytes and the tag. + + Args: + into: Optional destination buffer for the tail and tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A memoryview of the produced bytes (tail + tag). When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If maclen is invalid. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag + out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis128x2_state_encrypt_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt final failed: {err_name}") + return out[: int(written[0])] + + def final_detached( + self, + ct_into: bytearray | None = None, + mac_into: bytearray | None = None, + maclen: int = ABYTES_MIN, + ) -> tuple[bytearray, bytearray]: + """Finalize encryption, producing detached tail bytes and tag. + + Args: + ct_into: Optional destination for the remaining ciphertext tail. + mac_into: Optional destination for the tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A tuple of (tail_bytes, mac). When destination buffers are provided, + the first element is a slice of ``ct_into`` up to the number of bytes + written, and the second is ``mac_into``. + + Raises: + TypeError: If maclen is invalid or mac_into has the wrong length. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = ct_into if ct_into is not None else bytearray(TAILBYTES_MAX) + mac = mac_into if mac_into is not None else bytearray(maclen) + if len(mac) != maclen: + raise TypeError("mac_into length must equal maclen") + written = ffi.new("size_t *") + rc = _lib.aegis128x2_state_encrypt_detached_final( + self._st, + ffi.from_buffer(out), + len(out), + written, + ffi.from_buffer(mac), + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt detached final failed: {err_name}") + return out[: int(written[0])], mac + + +class Decryptor: + """Incremental decryptor. + + - update(ciphertext[, into]) -> returns plaintext bytes + - final(mac[, into]) -> returns any remaining plaintext bytes + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental decryptor for detached tags. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data used during encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis128x2_state"), 64) + st = ffi.cast("aegis128x2_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis128x2_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + """Process a chunk of ciphertext. + + Args: + ct: Ciphertext bytes (without MAC). + into: Optional destination buffer; must be >= len(ciphertext). + + Returns: + A memoryview of the decrypted bytes for this chunk. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + ct = memoryview(ct) + out = into if into is not None else bytearray(ct.nbytes) + out = memoryview(out) + if out.nbytes < ct.nbytes: + raise TypeError("into length must be >= len(ciphertext)") + written = ffi.new("size_t *") + rc = _lib.aegis128x2_state_decrypt_detached_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(ct), + ct.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state decrypt update failed: {err_name}") + w = int(written[0]) + return out[:w] + + def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: + """Finalize decryption by verifying tag and flushing remaining bytes. + + Args: + mac: Tag to verify (16 or 32 bytes). + into: Optional destination buffer for the remaining plaintext bytes. + + Returns: + A memoryview of the remaining plaintext bytes. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If tag length is invalid. + ValueError: If authentication fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + out = into if into is not None else bytearray(TAILBYTES_MAX) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis128x2_state_decrypt_detached_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(mac), + maclen, + ) + if rc != 0: + raise ValueError("authentication failed") + w = int(written[0]) + return out[:w] + + +def new_state(): + """Allocate and return a new aegis128x2_state* with proper alignment. + + The returned object is an ffi cdata pointer with automatic finalizer. + """ + # Allocate with 64-byte alignment using libc.posix_memalign + raw = alloc_aligned(ffi.sizeof("aegis128x2_state"), 64) + ptr = ffi.cast("aegis128x2_state *", raw) + return ffi.gc(ptr, libc.free) + + +def new_mac_state(): + """Allocate and return a new aegis128x2_mac_state* with proper alignment.""" + raw = alloc_aligned(ffi.sizeof("aegis128x2_mac_state"), 64) + ptr = ffi.cast("aegis128x2_mac_state *", raw) + return ffi.gc(ptr, libc.free) + + +__all__ = [ + # constants + "KEYBYTES", + "NPUBBYTES", + "ABYTES_MIN", + "ABYTES_MAX", + "TAILBYTES_MAX", + # one-shot functions + "encrypt_detached", + "decrypt_detached", + "encrypt", + "decrypt", + "stream", + "encrypt_unauthenticated", + "decrypt_unauthenticated", + "mac", + # incremental classes + "Encryptor", + "Decryptor", + "Mac", +] diff --git a/aegis/aegis128x4.py b/aegis/aegis128x4.py new file mode 100644 index 0000000..b305389 --- /dev/null +++ b/aegis/aegis128x4.py @@ -0,0 +1,848 @@ +"""aegis128x4 Python submodule. + +Simplified API: single functions can return newly allocated buffers or write +into user-provided buffers via optional `into=` (and `mac_into=` for detached). + +Error return codes from the C library raise ValueError. +""" + +import errno +from collections.abc import Buffer + +from ._loader import alloc_aligned, ffi, libc +from ._loader import lib as _lib + +# Constants exposed as functions in C; mirror them as integers at module import time +KEYBYTES = _lib.aegis128x4_keybytes() +NPUBBYTES = _lib.aegis128x4_npubbytes() +ABYTES_MIN = _lib.aegis128x4_abytes_min() +ABYTES_MAX = _lib.aegis128x4_abytes_max() +TAILBYTES_MAX = _lib.aegis128x4_tailbytes_max() + + +def _ptr(buf): + """Return an ffi pointer for a Python buffer, or NULL for None. + + Args: + buf: Any object supporting the Python buffer protocol, or None. + + Returns: + An ffi pointer obtained with ffi.from_buffer, or ffi.NULL when buf is None. + """ + return ffi.NULL if buf is None else ffi.from_buffer(buf) + + +def encrypt_detached( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + ct_into: Buffer | None = None, + mac_into: Buffer | None = None, +) -> tuple[memoryview, memoryview]: + """Encrypt message with associated data, returning ciphertext and MAC separately. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + ct_into: Buffer to write ciphertext into (default: bytearray created). + mac_into: Buffer to write MAC into (default: bytearray created). + + Returns: + Tuple of (ciphertext, mac) + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + ct_into = memoryview(ct_into) if ct_into is not None else None + mac_into = memoryview(mac_into) if mac_into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + + c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) + mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) + if c.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + if mac.nbytes != maclen: + raise TypeError("mac_into length must equal maclen") + + rc = _lib.aegis128x4_encrypt_detached( + ffi.from_buffer(c), + ffi.from_buffer(mac), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt detached failed: {err_name}") + return c, mac + + +def decrypt_detached( + nonce: Buffer, + key: Buffer, + ct: Buffer, + mac: Buffer, + ad: Buffer | None = None, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with detached MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext to decrypt. + mac: The MAC to verify. + ad: Associated data (optional). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + mac = memoryview(mac) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if m_out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + + rc = _lib.aegis128x4_decrypt_detached( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + _ptr(mac), + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return memoryview(m_out) + + +def encrypt( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message with associated data, returning ciphertext with appended MAC. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write ciphertext+MAC into (default: bytearray created). + + Returns: + Ciphertext with appended MAC as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) + if out.nbytes != message.nbytes + maclen: + raise TypeError("into length must be len(message)+maclen") + + rc = _lib.aegis128x4_encrypt( + ffi.from_buffer(out), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt failed: {err_name}") + return out + + +def decrypt( + nonce: Buffer, + key: Buffer, + ct: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with appended MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext with MAC to decrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if ct.nbytes < maclen: + raise TypeError("ciphertext too short for tag") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) + if m_out.nbytes != ct.nbytes - maclen: + raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + + rc = _lib.aegis128x4_decrypt( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return m_out + + +def stream( + nonce: Buffer | None, + key: Buffer, + length: int | None = None, + into: Buffer | None = None, +) -> memoryview: + """Generate a stream of pseudorandom bytes. + + Args: + nonce: Nonce (32 bytes, uses zeroes for nonce if None). + key: Key (32 bytes). + length: Number of bytes to generate (required if into is None). + into: Buffer to write stream into (default: bytearray created). + + Returns: + Pseudorandom bytes as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid or neither length nor into provided. + """ + nonce = memoryview(nonce) if nonce is not None else None + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce is not None and nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if into is None and length is None: + raise TypeError("provide either into or length") + out = into if into is not None else memoryview(bytearray(int(length or 0))) + _lib.aegis128x4_stream( + ffi.from_buffer(out), + out.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def encrypt_unauthenticated( + message: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message without authentication (for testing/debugging). + + Args: + message: The plaintext message to encrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write ciphertext into (default: bytearray created). + + Returns: + Ciphertext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + message = memoryview(message) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes)) + if out.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + _lib.aegis128x4_encrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(message), + message.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def decrypt_unauthenticated( + ct: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext without authentication (for testing/debugging). + + Args: + ct: The ciphertext to decrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + ct = memoryview(ct) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + _lib.aegis128x4_decrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(ct), + ct.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +# This is missing from C API but convenient to have here +def mac( + data: Buffer, + nonce: Buffer, + key: Buffer, + maclen: int = ABYTES_MIN, +) -> memoryview: + """Compute a MAC for the given data in one shot. + + Args: + data: Data to MAC + nonce: Nonce (32 bytes) + key: Key (32 bytes) + maclen: MAC length (16 or 32, default 16) + + Returns: + MAC bytes + """ + mac_state = Mac(nonce, key) + mac_state.update(data) + return mac_state.final(maclen) + + +class Mac: + """AEGIS-256X4 MAC state wrapper. + + Usage: + mac = Mac(nonce, key) + mac.update(data) + tag = mac.final() # defaults to 16-byte MAC + # or verify: + mac2 = Mac(nonce, key); mac2.update(data); mac2.verify(tag) + """ + + __slots__ = ("_st", "_nonce", "_key") + + def __init__( + self, + nonce: Buffer, + key: Buffer, + _other=None, + ) -> None: + """Initialize a MAC state with a nonce and key. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + raw = alloc_aligned(ffi.sizeof("aegis128x4_mac_state"), 64) + st = ffi.cast("aegis128x4_mac_state *", raw) + self._st = ffi.gc(st, libc.free) + if _other is not None: + _lib.aegis128x4_mac_state_clone(self._st, _other._st) + return + # Normal init + nonce = memoryview(nonce) + key = memoryview(key) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES=}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES=}") + _lib.aegis128x4_mac_init(self._st, _ptr(key), _ptr(nonce)) + + def __deepcopy__(self) -> "Mac": + """Return a clone of current MAC state.""" + return Mac(b"", b"", _other=self) + + clone = __deepcopy__ + + def reset(self) -> None: + """Reset the MAC state so it can be reused with the same nonce and key.""" + _lib.aegis128x4_mac_reset(self._st) + + def update(self, data: Buffer) -> None: + """Absorb data into the MAC state. + + Args: + data: Bytes-like object to authenticate. + + Raises: + RuntimeError: If the underlying C function reports an error. + """ + data = memoryview(data) + rc = _lib.aegis128x4_mac_update(self._st, _ptr(data), data.nbytes) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac update failed: {err_name}") + + def final( + self, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, + ) -> memoryview: + """Finalize and return the MAC tag. + + Args: + maclen: Tag length in bytes (16 or 32). Defaults to 16. + into: Optional buffer to write the tag into (default: bytearray created). + + Returns: + The tag as a memoryview; if ``into`` is provided, it views that buffer. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If finalization fails in the C library. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = into if into is not None else bytearray(maclen) + out = memoryview(out) + if out.nbytes != maclen: + raise TypeError("into length must equal maclen") + rc = _lib.aegis128x4_mac_final(self._st, ffi.from_buffer(out), maclen) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac final failed: {err_name}") + return out + + def verify(self, mac: Buffer) -> bool: + """Verify a tag for the current MAC state. + + Args: + mac: The tag to verify (16 or 32 bytes). + + Returns: + True if verification succeeds. + + Raises: + TypeError: If tag length is invalid. + ValueError: If verification fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + rc = _lib.aegis128x4_mac_verify(self._st, _ptr(mac), maclen) + if rc != 0: + raise ValueError("mac verification failed") + return True + + +class Encryptor: + """Incremental encryptor. + + - update(message[, into]) -> returns produced ciphertext bytes + - final([into], maclen=16) -> returns tail+tag bytes + - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental encryptor. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data to bind to the encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis128x4_state"), 64) + st = ffi.cast("aegis128x4_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis128x4_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + """Encrypt a chunk of the message. + + Args: + message: Plaintext bytes to encrypt. + into: Optional destination buffer; must be >= len(message). + + Returns: + The ciphertext for this chunk as a memoryview; when ``into`` is + provided, a view of that buffer up to the number of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + message = memoryview(message) + out = memoryview(into if into is not None else bytearray(message.nbytes)) + if out.nbytes < message.nbytes: + raise TypeError("into length must be >= len(message)") + written = ffi.new("size_t *") + rc = _lib.aegis128x4_state_encrypt_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(message), + message.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out[: int(written[0])] + + def final(self, into: Buffer | None = None, maclen: int = ABYTES_MIN) -> memoryview: + """Finalize encryption, writing any remaining bytes and the tag. + + Args: + into: Optional destination buffer for the tail and tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A memoryview of the produced bytes (tail + tag). When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If maclen is invalid. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag + out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis128x4_state_encrypt_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt final failed: {err_name}") + return out[: int(written[0])] + + def final_detached( + self, + ct_into: bytearray | None = None, + mac_into: bytearray | None = None, + maclen: int = ABYTES_MIN, + ) -> tuple[bytearray, bytearray]: + """Finalize encryption, producing detached tail bytes and tag. + + Args: + ct_into: Optional destination for the remaining ciphertext tail. + mac_into: Optional destination for the tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A tuple of (tail_bytes, mac). When destination buffers are provided, + the first element is a slice of ``ct_into`` up to the number of bytes + written, and the second is ``mac_into``. + + Raises: + TypeError: If maclen is invalid or mac_into has the wrong length. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = ct_into if ct_into is not None else bytearray(TAILBYTES_MAX) + mac = mac_into if mac_into is not None else bytearray(maclen) + if len(mac) != maclen: + raise TypeError("mac_into length must equal maclen") + written = ffi.new("size_t *") + rc = _lib.aegis128x4_state_encrypt_detached_final( + self._st, + ffi.from_buffer(out), + len(out), + written, + ffi.from_buffer(mac), + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt detached final failed: {err_name}") + return out[: int(written[0])], mac + + +class Decryptor: + """Incremental decryptor. + + - update(ciphertext[, into]) -> returns plaintext bytes + - final(mac[, into]) -> returns any remaining plaintext bytes + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental decryptor for detached tags. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data used during encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis128x4_state"), 64) + st = ffi.cast("aegis128x4_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis128x4_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + """Process a chunk of ciphertext. + + Args: + ct: Ciphertext bytes (without MAC). + into: Optional destination buffer; must be >= len(ciphertext). + + Returns: + A memoryview of the decrypted bytes for this chunk. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + ct = memoryview(ct) + out = into if into is not None else bytearray(ct.nbytes) + out = memoryview(out) + if out.nbytes < ct.nbytes: + raise TypeError("into length must be >= len(ciphertext)") + written = ffi.new("size_t *") + rc = _lib.aegis128x4_state_decrypt_detached_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(ct), + ct.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state decrypt update failed: {err_name}") + w = int(written[0]) + return out[:w] + + def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: + """Finalize decryption by verifying tag and flushing remaining bytes. + + Args: + mac: Tag to verify (16 or 32 bytes). + into: Optional destination buffer for the remaining plaintext bytes. + + Returns: + A memoryview of the remaining plaintext bytes. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If tag length is invalid. + ValueError: If authentication fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + out = into if into is not None else bytearray(TAILBYTES_MAX) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis128x4_state_decrypt_detached_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(mac), + maclen, + ) + if rc != 0: + raise ValueError("authentication failed") + w = int(written[0]) + return out[:w] + + +def new_state(): + """Allocate and return a new aegis128x4_state* with proper alignment. + + The returned object is an ffi cdata pointer with automatic finalizer. + """ + # Allocate with 64-byte alignment using libc.posix_memalign + raw = alloc_aligned(ffi.sizeof("aegis128x4_state"), 64) + ptr = ffi.cast("aegis128x4_state *", raw) + return ffi.gc(ptr, libc.free) + + +def new_mac_state(): + """Allocate and return a new aegis128x4_mac_state* with proper alignment.""" + raw = alloc_aligned(ffi.sizeof("aegis128x4_mac_state"), 64) + ptr = ffi.cast("aegis128x4_mac_state *", raw) + return ffi.gc(ptr, libc.free) + + +__all__ = [ + # constants + "KEYBYTES", + "NPUBBYTES", + "ABYTES_MIN", + "ABYTES_MAX", + "TAILBYTES_MAX", + # one-shot functions + "encrypt_detached", + "decrypt_detached", + "encrypt", + "decrypt", + "stream", + "encrypt_unauthenticated", + "decrypt_unauthenticated", + "mac", + # incremental classes + "Encryptor", + "Decryptor", + "Mac", +] diff --git a/aegis/aegis256.py b/aegis/aegis256.py new file mode 100644 index 0000000..c67d7d0 --- /dev/null +++ b/aegis/aegis256.py @@ -0,0 +1,848 @@ +"""aegis256 Python submodule. + +Simplified API: single functions can return newly allocated buffers or write +into user-provided buffers via optional `into=` (and `mac_into=` for detached). + +Error return codes from the C library raise ValueError. +""" + +import errno +from collections.abc import Buffer + +from ._loader import alloc_aligned, ffi, libc +from ._loader import lib as _lib + +# Constants exposed as functions in C; mirror them as integers at module import time +KEYBYTES = _lib.aegis256_keybytes() +NPUBBYTES = _lib.aegis256_npubbytes() +ABYTES_MIN = _lib.aegis256_abytes_min() +ABYTES_MAX = _lib.aegis256_abytes_max() +TAILBYTES_MAX = _lib.aegis256_tailbytes_max() + + +def _ptr(buf): + """Return an ffi pointer for a Python buffer, or NULL for None. + + Args: + buf: Any object supporting the Python buffer protocol, or None. + + Returns: + An ffi pointer obtained with ffi.from_buffer, or ffi.NULL when buf is None. + """ + return ffi.NULL if buf is None else ffi.from_buffer(buf) + + +def encrypt_detached( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + ct_into: Buffer | None = None, + mac_into: Buffer | None = None, +) -> tuple[memoryview, memoryview]: + """Encrypt message with associated data, returning ciphertext and MAC separately. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + ct_into: Buffer to write ciphertext into (default: bytearray created). + mac_into: Buffer to write MAC into (default: bytearray created). + + Returns: + Tuple of (ciphertext, mac) + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + ct_into = memoryview(ct_into) if ct_into is not None else None + mac_into = memoryview(mac_into) if mac_into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + + c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) + mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) + if c.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + if mac.nbytes != maclen: + raise TypeError("mac_into length must equal maclen") + + rc = _lib.aegis256_encrypt_detached( + ffi.from_buffer(c), + ffi.from_buffer(mac), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt detached failed: {err_name}") + return c, mac + + +def decrypt_detached( + nonce: Buffer, + key: Buffer, + ct: Buffer, + mac: Buffer, + ad: Buffer | None = None, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with detached MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext to decrypt. + mac: The MAC to verify. + ad: Associated data (optional). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + mac = memoryview(mac) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if m_out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + + rc = _lib.aegis256_decrypt_detached( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + _ptr(mac), + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return memoryview(m_out) + + +def encrypt( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message with associated data, returning ciphertext with appended MAC. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write ciphertext+MAC into (default: bytearray created). + + Returns: + Ciphertext with appended MAC as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) + if out.nbytes != message.nbytes + maclen: + raise TypeError("into length must be len(message)+maclen") + + rc = _lib.aegis256_encrypt( + ffi.from_buffer(out), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt failed: {err_name}") + return out + + +def decrypt( + nonce: Buffer, + key: Buffer, + ct: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with appended MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext with MAC to decrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if ct.nbytes < maclen: + raise TypeError("ciphertext too short for tag") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) + if m_out.nbytes != ct.nbytes - maclen: + raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + + rc = _lib.aegis256_decrypt( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return m_out + + +def stream( + nonce: Buffer | None, + key: Buffer, + length: int | None = None, + into: Buffer | None = None, +) -> memoryview: + """Generate a stream of pseudorandom bytes. + + Args: + nonce: Nonce (32 bytes, uses zeroes for nonce if None). + key: Key (32 bytes). + length: Number of bytes to generate (required if into is None). + into: Buffer to write stream into (default: bytearray created). + + Returns: + Pseudorandom bytes as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid or neither length nor into provided. + """ + nonce = memoryview(nonce) if nonce is not None else None + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce is not None and nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if into is None and length is None: + raise TypeError("provide either into or length") + out = into if into is not None else memoryview(bytearray(int(length or 0))) + _lib.aegis256_stream( + ffi.from_buffer(out), + out.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def encrypt_unauthenticated( + message: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message without authentication (for testing/debugging). + + Args: + message: The plaintext message to encrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write ciphertext into (default: bytearray created). + + Returns: + Ciphertext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + message = memoryview(message) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes)) + if out.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + _lib.aegis256_encrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(message), + message.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def decrypt_unauthenticated( + ct: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext without authentication (for testing/debugging). + + Args: + ct: The ciphertext to decrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + ct = memoryview(ct) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + _lib.aegis256_decrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(ct), + ct.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +# This is missing from C API but convenient to have here +def mac( + data: Buffer, + nonce: Buffer, + key: Buffer, + maclen: int = ABYTES_MIN, +) -> memoryview: + """Compute a MAC for the given data in one shot. + + Args: + data: Data to MAC + nonce: Nonce (32 bytes) + key: Key (32 bytes) + maclen: MAC length (16 or 32, default 16) + + Returns: + MAC bytes + """ + mac_state = Mac(nonce, key) + mac_state.update(data) + return mac_state.final(maclen) + + +class Mac: + """AEGIS-256X4 MAC state wrapper. + + Usage: + mac = Mac(nonce, key) + mac.update(data) + tag = mac.final() # defaults to 16-byte MAC + # or verify: + mac2 = Mac(nonce, key); mac2.update(data); mac2.verify(tag) + """ + + __slots__ = ("_st", "_nonce", "_key") + + def __init__( + self, + nonce: Buffer, + key: Buffer, + _other=None, + ) -> None: + """Initialize a MAC state with a nonce and key. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + raw = alloc_aligned(ffi.sizeof("aegis256_mac_state"), 16) + st = ffi.cast("aegis256_mac_state *", raw) + self._st = ffi.gc(st, libc.free) + if _other is not None: + _lib.aegis256_mac_state_clone(self._st, _other._st) + return + # Normal init + nonce = memoryview(nonce) + key = memoryview(key) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES=}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES=}") + _lib.aegis256_mac_init(self._st, _ptr(key), _ptr(nonce)) + + def __deepcopy__(self) -> "Mac": + """Return a clone of current MAC state.""" + return Mac(b"", b"", _other=self) + + clone = __deepcopy__ + + def reset(self) -> None: + """Reset the MAC state so it can be reused with the same nonce and key.""" + _lib.aegis256_mac_reset(self._st) + + def update(self, data: Buffer) -> None: + """Absorb data into the MAC state. + + Args: + data: Bytes-like object to authenticate. + + Raises: + RuntimeError: If the underlying C function reports an error. + """ + data = memoryview(data) + rc = _lib.aegis256_mac_update(self._st, _ptr(data), data.nbytes) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac update failed: {err_name}") + + def final( + self, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, + ) -> memoryview: + """Finalize and return the MAC tag. + + Args: + maclen: Tag length in bytes (16 or 32). Defaults to 16. + into: Optional buffer to write the tag into (default: bytearray created). + + Returns: + The tag as a memoryview; if ``into`` is provided, it views that buffer. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If finalization fails in the C library. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = into if into is not None else bytearray(maclen) + out = memoryview(out) + if out.nbytes != maclen: + raise TypeError("into length must equal maclen") + rc = _lib.aegis256_mac_final(self._st, ffi.from_buffer(out), maclen) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac final failed: {err_name}") + return out + + def verify(self, mac: Buffer) -> bool: + """Verify a tag for the current MAC state. + + Args: + mac: The tag to verify (16 or 32 bytes). + + Returns: + True if verification succeeds. + + Raises: + TypeError: If tag length is invalid. + ValueError: If verification fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + rc = _lib.aegis256_mac_verify(self._st, _ptr(mac), maclen) + if rc != 0: + raise ValueError("mac verification failed") + return True + + +class Encryptor: + """Incremental encryptor. + + - update(message[, into]) -> returns produced ciphertext bytes + - final([into], maclen=16) -> returns tail+tag bytes + - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental encryptor. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data to bind to the encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis256_state"), 16) + st = ffi.cast("aegis256_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis256_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + """Encrypt a chunk of the message. + + Args: + message: Plaintext bytes to encrypt. + into: Optional destination buffer; must be >= len(message). + + Returns: + The ciphertext for this chunk as a memoryview; when ``into`` is + provided, a view of that buffer up to the number of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + message = memoryview(message) + out = memoryview(into if into is not None else bytearray(message.nbytes)) + if out.nbytes < message.nbytes: + raise TypeError("into length must be >= len(message)") + written = ffi.new("size_t *") + rc = _lib.aegis256_state_encrypt_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(message), + message.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out[: int(written[0])] + + def final(self, into: Buffer | None = None, maclen: int = ABYTES_MIN) -> memoryview: + """Finalize encryption, writing any remaining bytes and the tag. + + Args: + into: Optional destination buffer for the tail and tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A memoryview of the produced bytes (tail + tag). When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If maclen is invalid. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag + out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis256_state_encrypt_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt final failed: {err_name}") + return out[: int(written[0])] + + def final_detached( + self, + ct_into: bytearray | None = None, + mac_into: bytearray | None = None, + maclen: int = ABYTES_MIN, + ) -> tuple[bytearray, bytearray]: + """Finalize encryption, producing detached tail bytes and tag. + + Args: + ct_into: Optional destination for the remaining ciphertext tail. + mac_into: Optional destination for the tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A tuple of (tail_bytes, mac). When destination buffers are provided, + the first element is a slice of ``ct_into`` up to the number of bytes + written, and the second is ``mac_into``. + + Raises: + TypeError: If maclen is invalid or mac_into has the wrong length. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = ct_into if ct_into is not None else bytearray(TAILBYTES_MAX) + mac = mac_into if mac_into is not None else bytearray(maclen) + if len(mac) != maclen: + raise TypeError("mac_into length must equal maclen") + written = ffi.new("size_t *") + rc = _lib.aegis256_state_encrypt_detached_final( + self._st, + ffi.from_buffer(out), + len(out), + written, + ffi.from_buffer(mac), + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt detached final failed: {err_name}") + return out[: int(written[0])], mac + + +class Decryptor: + """Incremental decryptor. + + - update(ciphertext[, into]) -> returns plaintext bytes + - final(mac[, into]) -> returns any remaining plaintext bytes + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental decryptor for detached tags. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data used during encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis256_state"), 16) + st = ffi.cast("aegis256_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis256_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + """Process a chunk of ciphertext. + + Args: + ct: Ciphertext bytes (without MAC). + into: Optional destination buffer; must be >= len(ciphertext). + + Returns: + A memoryview of the decrypted bytes for this chunk. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + ct = memoryview(ct) + out = into if into is not None else bytearray(ct.nbytes) + out = memoryview(out) + if out.nbytes < ct.nbytes: + raise TypeError("into length must be >= len(ciphertext)") + written = ffi.new("size_t *") + rc = _lib.aegis256_state_decrypt_detached_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(ct), + ct.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state decrypt update failed: {err_name}") + w = int(written[0]) + return out[:w] + + def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: + """Finalize decryption by verifying tag and flushing remaining bytes. + + Args: + mac: Tag to verify (16 or 32 bytes). + into: Optional destination buffer for the remaining plaintext bytes. + + Returns: + A memoryview of the remaining plaintext bytes. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If tag length is invalid. + ValueError: If authentication fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + out = into if into is not None else bytearray(TAILBYTES_MAX) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis256_state_decrypt_detached_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(mac), + maclen, + ) + if rc != 0: + raise ValueError("authentication failed") + w = int(written[0]) + return out[:w] + + +def new_state(): + """Allocate and return a new aegis256_state* with proper alignment. + + The returned object is an ffi cdata pointer with automatic finalizer. + """ + # Allocate with 64-byte alignment using libc.posix_memalign + raw = alloc_aligned(ffi.sizeof("aegis256_state"), 16) + ptr = ffi.cast("aegis256_state *", raw) + return ffi.gc(ptr, libc.free) + + +def new_mac_state(): + """Allocate and return a new aegis256_mac_state* with proper alignment.""" + raw = alloc_aligned(ffi.sizeof("aegis256_mac_state"), 16) + ptr = ffi.cast("aegis256_mac_state *", raw) + return ffi.gc(ptr, libc.free) + + +__all__ = [ + # constants + "KEYBYTES", + "NPUBBYTES", + "ABYTES_MIN", + "ABYTES_MAX", + "TAILBYTES_MAX", + # one-shot functions + "encrypt_detached", + "decrypt_detached", + "encrypt", + "decrypt", + "stream", + "encrypt_unauthenticated", + "decrypt_unauthenticated", + "mac", + # incremental classes + "Encryptor", + "Decryptor", + "Mac", +] diff --git a/aegis/aegis256x2.py b/aegis/aegis256x2.py new file mode 100644 index 0000000..646e0ef --- /dev/null +++ b/aegis/aegis256x2.py @@ -0,0 +1,848 @@ +"""aegis256x2 Python submodule. + +Simplified API: single functions can return newly allocated buffers or write +into user-provided buffers via optional `into=` (and `mac_into=` for detached). + +Error return codes from the C library raise ValueError. +""" + +import errno +from collections.abc import Buffer + +from ._loader import alloc_aligned, ffi, libc +from ._loader import lib as _lib + +# Constants exposed as functions in C; mirror them as integers at module import time +KEYBYTES = _lib.aegis256x2_keybytes() +NPUBBYTES = _lib.aegis256x2_npubbytes() +ABYTES_MIN = _lib.aegis256x2_abytes_min() +ABYTES_MAX = _lib.aegis256x2_abytes_max() +TAILBYTES_MAX = _lib.aegis256x2_tailbytes_max() + + +def _ptr(buf): + """Return an ffi pointer for a Python buffer, or NULL for None. + + Args: + buf: Any object supporting the Python buffer protocol, or None. + + Returns: + An ffi pointer obtained with ffi.from_buffer, or ffi.NULL when buf is None. + """ + return ffi.NULL if buf is None else ffi.from_buffer(buf) + + +def encrypt_detached( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + ct_into: Buffer | None = None, + mac_into: Buffer | None = None, +) -> tuple[memoryview, memoryview]: + """Encrypt message with associated data, returning ciphertext and MAC separately. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + ct_into: Buffer to write ciphertext into (default: bytearray created). + mac_into: Buffer to write MAC into (default: bytearray created). + + Returns: + Tuple of (ciphertext, mac) + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + ct_into = memoryview(ct_into) if ct_into is not None else None + mac_into = memoryview(mac_into) if mac_into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + + c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) + mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) + if c.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + if mac.nbytes != maclen: + raise TypeError("mac_into length must equal maclen") + + rc = _lib.aegis256x2_encrypt_detached( + ffi.from_buffer(c), + ffi.from_buffer(mac), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt detached failed: {err_name}") + return c, mac + + +def decrypt_detached( + nonce: Buffer, + key: Buffer, + ct: Buffer, + mac: Buffer, + ad: Buffer | None = None, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with detached MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext to decrypt. + mac: The MAC to verify. + ad: Associated data (optional). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + mac = memoryview(mac) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if m_out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + + rc = _lib.aegis256x2_decrypt_detached( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + _ptr(mac), + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return memoryview(m_out) + + +def encrypt( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message with associated data, returning ciphertext with appended MAC. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write ciphertext+MAC into (default: bytearray created). + + Returns: + Ciphertext with appended MAC as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) + if out.nbytes != message.nbytes + maclen: + raise TypeError("into length must be len(message)+maclen") + + rc = _lib.aegis256x2_encrypt( + ffi.from_buffer(out), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt failed: {err_name}") + return out + + +def decrypt( + nonce: Buffer, + key: Buffer, + ct: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with appended MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext with MAC to decrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if ct.nbytes < maclen: + raise TypeError("ciphertext too short for tag") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) + if m_out.nbytes != ct.nbytes - maclen: + raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + + rc = _lib.aegis256x2_decrypt( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return m_out + + +def stream( + nonce: Buffer | None, + key: Buffer, + length: int | None = None, + into: Buffer | None = None, +) -> memoryview: + """Generate a stream of pseudorandom bytes. + + Args: + nonce: Nonce (32 bytes, uses zeroes for nonce if None). + key: Key (32 bytes). + length: Number of bytes to generate (required if into is None). + into: Buffer to write stream into (default: bytearray created). + + Returns: + Pseudorandom bytes as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid or neither length nor into provided. + """ + nonce = memoryview(nonce) if nonce is not None else None + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce is not None and nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if into is None and length is None: + raise TypeError("provide either into or length") + out = into if into is not None else memoryview(bytearray(int(length or 0))) + _lib.aegis256x2_stream( + ffi.from_buffer(out), + out.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def encrypt_unauthenticated( + message: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message without authentication (for testing/debugging). + + Args: + message: The plaintext message to encrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write ciphertext into (default: bytearray created). + + Returns: + Ciphertext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + message = memoryview(message) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes)) + if out.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + _lib.aegis256x2_encrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(message), + message.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def decrypt_unauthenticated( + ct: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext without authentication (for testing/debugging). + + Args: + ct: The ciphertext to decrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + ct = memoryview(ct) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + _lib.aegis256x2_decrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(ct), + ct.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +# This is missing from C API but convenient to have here +def mac( + data: Buffer, + nonce: Buffer, + key: Buffer, + maclen: int = ABYTES_MIN, +) -> memoryview: + """Compute a MAC for the given data in one shot. + + Args: + data: Data to MAC + nonce: Nonce (32 bytes) + key: Key (32 bytes) + maclen: MAC length (16 or 32, default 16) + + Returns: + MAC bytes + """ + mac_state = Mac(nonce, key) + mac_state.update(data) + return mac_state.final(maclen) + + +class Mac: + """AEGIS-256X4 MAC state wrapper. + + Usage: + mac = Mac(nonce, key) + mac.update(data) + tag = mac.final() # defaults to 16-byte MAC + # or verify: + mac2 = Mac(nonce, key); mac2.update(data); mac2.verify(tag) + """ + + __slots__ = ("_st", "_nonce", "_key") + + def __init__( + self, + nonce: Buffer, + key: Buffer, + _other=None, + ) -> None: + """Initialize a MAC state with a nonce and key. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + raw = alloc_aligned(ffi.sizeof("aegis256x2_mac_state"), 32) + st = ffi.cast("aegis256x2_mac_state *", raw) + self._st = ffi.gc(st, libc.free) + if _other is not None: + _lib.aegis256x2_mac_state_clone(self._st, _other._st) + return + # Normal init + nonce = memoryview(nonce) + key = memoryview(key) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES=}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES=}") + _lib.aegis256x2_mac_init(self._st, _ptr(key), _ptr(nonce)) + + def __deepcopy__(self) -> "Mac": + """Return a clone of current MAC state.""" + return Mac(b"", b"", _other=self) + + clone = __deepcopy__ + + def reset(self) -> None: + """Reset the MAC state so it can be reused with the same nonce and key.""" + _lib.aegis256x2_mac_reset(self._st) + + def update(self, data: Buffer) -> None: + """Absorb data into the MAC state. + + Args: + data: Bytes-like object to authenticate. + + Raises: + RuntimeError: If the underlying C function reports an error. + """ + data = memoryview(data) + rc = _lib.aegis256x2_mac_update(self._st, _ptr(data), data.nbytes) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac update failed: {err_name}") + + def final( + self, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, + ) -> memoryview: + """Finalize and return the MAC tag. + + Args: + maclen: Tag length in bytes (16 or 32). Defaults to 16. + into: Optional buffer to write the tag into (default: bytearray created). + + Returns: + The tag as a memoryview; if ``into`` is provided, it views that buffer. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If finalization fails in the C library. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = into if into is not None else bytearray(maclen) + out = memoryview(out) + if out.nbytes != maclen: + raise TypeError("into length must equal maclen") + rc = _lib.aegis256x2_mac_final(self._st, ffi.from_buffer(out), maclen) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac final failed: {err_name}") + return out + + def verify(self, mac: Buffer) -> bool: + """Verify a tag for the current MAC state. + + Args: + mac: The tag to verify (16 or 32 bytes). + + Returns: + True if verification succeeds. + + Raises: + TypeError: If tag length is invalid. + ValueError: If verification fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + rc = _lib.aegis256x2_mac_verify(self._st, _ptr(mac), maclen) + if rc != 0: + raise ValueError("mac verification failed") + return True + + +class Encryptor: + """Incremental encryptor. + + - update(message[, into]) -> returns produced ciphertext bytes + - final([into], maclen=16) -> returns tail+tag bytes + - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental encryptor. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data to bind to the encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis256x2_state"), 32) + st = ffi.cast("aegis256x2_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis256x2_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + """Encrypt a chunk of the message. + + Args: + message: Plaintext bytes to encrypt. + into: Optional destination buffer; must be >= len(message). + + Returns: + The ciphertext for this chunk as a memoryview; when ``into`` is + provided, a view of that buffer up to the number of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + message = memoryview(message) + out = memoryview(into if into is not None else bytearray(message.nbytes)) + if out.nbytes < message.nbytes: + raise TypeError("into length must be >= len(message)") + written = ffi.new("size_t *") + rc = _lib.aegis256x2_state_encrypt_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(message), + message.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out[: int(written[0])] + + def final(self, into: Buffer | None = None, maclen: int = ABYTES_MIN) -> memoryview: + """Finalize encryption, writing any remaining bytes and the tag. + + Args: + into: Optional destination buffer for the tail and tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A memoryview of the produced bytes (tail + tag). When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If maclen is invalid. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag + out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis256x2_state_encrypt_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt final failed: {err_name}") + return out[: int(written[0])] + + def final_detached( + self, + ct_into: bytearray | None = None, + mac_into: bytearray | None = None, + maclen: int = ABYTES_MIN, + ) -> tuple[bytearray, bytearray]: + """Finalize encryption, producing detached tail bytes and tag. + + Args: + ct_into: Optional destination for the remaining ciphertext tail. + mac_into: Optional destination for the tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A tuple of (tail_bytes, mac). When destination buffers are provided, + the first element is a slice of ``ct_into`` up to the number of bytes + written, and the second is ``mac_into``. + + Raises: + TypeError: If maclen is invalid or mac_into has the wrong length. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = ct_into if ct_into is not None else bytearray(TAILBYTES_MAX) + mac = mac_into if mac_into is not None else bytearray(maclen) + if len(mac) != maclen: + raise TypeError("mac_into length must equal maclen") + written = ffi.new("size_t *") + rc = _lib.aegis256x2_state_encrypt_detached_final( + self._st, + ffi.from_buffer(out), + len(out), + written, + ffi.from_buffer(mac), + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt detached final failed: {err_name}") + return out[: int(written[0])], mac + + +class Decryptor: + """Incremental decryptor. + + - update(ciphertext[, into]) -> returns plaintext bytes + - final(mac[, into]) -> returns any remaining plaintext bytes + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental decryptor for detached tags. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data used during encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis256x2_state"), 32) + st = ffi.cast("aegis256x2_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis256x2_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + """Process a chunk of ciphertext. + + Args: + ct: Ciphertext bytes (without MAC). + into: Optional destination buffer; must be >= len(ciphertext). + + Returns: + A memoryview of the decrypted bytes for this chunk. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + ct = memoryview(ct) + out = into if into is not None else bytearray(ct.nbytes) + out = memoryview(out) + if out.nbytes < ct.nbytes: + raise TypeError("into length must be >= len(ciphertext)") + written = ffi.new("size_t *") + rc = _lib.aegis256x2_state_decrypt_detached_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(ct), + ct.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state decrypt update failed: {err_name}") + w = int(written[0]) + return out[:w] + + def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: + """Finalize decryption by verifying tag and flushing remaining bytes. + + Args: + mac: Tag to verify (16 or 32 bytes). + into: Optional destination buffer for the remaining plaintext bytes. + + Returns: + A memoryview of the remaining plaintext bytes. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If tag length is invalid. + ValueError: If authentication fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + out = into if into is not None else bytearray(TAILBYTES_MAX) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis256x2_state_decrypt_detached_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(mac), + maclen, + ) + if rc != 0: + raise ValueError("authentication failed") + w = int(written[0]) + return out[:w] + + +def new_state(): + """Allocate and return a new aegis256x2_state* with proper alignment. + + The returned object is an ffi cdata pointer with automatic finalizer. + """ + # Allocate with 64-byte alignment using libc.posix_memalign + raw = alloc_aligned(ffi.sizeof("aegis256x2_state"), 32) + ptr = ffi.cast("aegis256x2_state *", raw) + return ffi.gc(ptr, libc.free) + + +def new_mac_state(): + """Allocate and return a new aegis256x2_mac_state* with proper alignment.""" + raw = alloc_aligned(ffi.sizeof("aegis256x2_mac_state"), 32) + ptr = ffi.cast("aegis256x2_mac_state *", raw) + return ffi.gc(ptr, libc.free) + + +__all__ = [ + # constants + "KEYBYTES", + "NPUBBYTES", + "ABYTES_MIN", + "ABYTES_MAX", + "TAILBYTES_MAX", + # one-shot functions + "encrypt_detached", + "decrypt_detached", + "encrypt", + "decrypt", + "stream", + "encrypt_unauthenticated", + "decrypt_unauthenticated", + "mac", + # incremental classes + "Encryptor", + "Decryptor", + "Mac", +] diff --git a/aegis/aegis256x4.py b/aegis/aegis256x4.py new file mode 100644 index 0000000..7213ca3 --- /dev/null +++ b/aegis/aegis256x4.py @@ -0,0 +1,848 @@ +"""aegis256x4 Python submodule. + +Simplified API: single functions can return newly allocated buffers or write +into user-provided buffers via optional `into=` (and `mac_into=` for detached). + +Error return codes from the C library raise ValueError. +""" + +import errno +from collections.abc import Buffer + +from ._loader import alloc_aligned, ffi, libc +from ._loader import lib as _lib + +# Constants exposed as functions in C; mirror them as integers at module import time +KEYBYTES = _lib.aegis256x4_keybytes() +NPUBBYTES = _lib.aegis256x4_npubbytes() +ABYTES_MIN = _lib.aegis256x4_abytes_min() +ABYTES_MAX = _lib.aegis256x4_abytes_max() +TAILBYTES_MAX = _lib.aegis256x4_tailbytes_max() + + +def _ptr(buf): + """Return an ffi pointer for a Python buffer, or NULL for None. + + Args: + buf: Any object supporting the Python buffer protocol, or None. + + Returns: + An ffi pointer obtained with ffi.from_buffer, or ffi.NULL when buf is None. + """ + return ffi.NULL if buf is None else ffi.from_buffer(buf) + + +def encrypt_detached( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + ct_into: Buffer | None = None, + mac_into: Buffer | None = None, +) -> tuple[memoryview, memoryview]: + """Encrypt message with associated data, returning ciphertext and MAC separately. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + ct_into: Buffer to write ciphertext into (default: bytearray created). + mac_into: Buffer to write MAC into (default: bytearray created). + + Returns: + Tuple of (ciphertext, mac) + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + ct_into = memoryview(ct_into) if ct_into is not None else None + mac_into = memoryview(mac_into) if mac_into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + + c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) + mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) + if c.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + if mac.nbytes != maclen: + raise TypeError("mac_into length must equal maclen") + + rc = _lib.aegis256x4_encrypt_detached( + ffi.from_buffer(c), + ffi.from_buffer(mac), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt detached failed: {err_name}") + return c, mac + + +def decrypt_detached( + nonce: Buffer, + key: Buffer, + ct: Buffer, + mac: Buffer, + ad: Buffer | None = None, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with detached MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext to decrypt. + mac: The MAC to verify. + ad: Associated data (optional). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + mac = memoryview(mac) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if m_out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + + rc = _lib.aegis256x4_decrypt_detached( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + _ptr(mac), + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return memoryview(m_out) + + +def encrypt( + nonce: Buffer, + key: Buffer, + message: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message with associated data, returning ciphertext with appended MAC. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + message: The plaintext message to encrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write ciphertext+MAC into (default: bytearray created). + + Returns: + Ciphertext with appended MAC as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If encryption fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + message = memoryview(message) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) + if out.nbytes != message.nbytes + maclen: + raise TypeError("into length must be len(message)+maclen") + + rc = _lib.aegis256x4_encrypt( + ffi.from_buffer(out), + maclen, + _ptr(message), + message.nbytes, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"encrypt failed: {err_name}") + return out + + +def decrypt( + nonce: Buffer, + key: Buffer, + ct: Buffer, + ad: Buffer | None = None, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext with appended MAC and associated data. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ct: The ciphertext with MAC to decrypt. + ad: Associated data (optional). + maclen: MAC length (16 or 32, default 16). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + ValueError: If authentication fails. + """ + nonce = memoryview(nonce) + key = memoryview(key) + ct = memoryview(ct) + ad = memoryview(ad) if ad is not None else None + into = memoryview(into) if into is not None else None + + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if ct.nbytes < maclen: + raise TypeError("ciphertext too short for tag") + m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) + if m_out.nbytes != ct.nbytes - maclen: + raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + + rc = _lib.aegis256x4_decrypt( + ffi.from_buffer(m_out), + _ptr(ct), + ct.nbytes, + maclen, + _ptr(ad), + 0 if ad is None else ad.nbytes, + _ptr(nonce), + _ptr(key), + ) + if rc != 0: + raise ValueError("authentication failed") + return m_out + + +def stream( + nonce: Buffer | None, + key: Buffer, + length: int | None = None, + into: Buffer | None = None, +) -> memoryview: + """Generate a stream of pseudorandom bytes. + + Args: + nonce: Nonce (32 bytes, uses zeroes for nonce if None). + key: Key (32 bytes). + length: Number of bytes to generate (required if into is None). + into: Buffer to write stream into (default: bytearray created). + + Returns: + Pseudorandom bytes as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid or neither length nor into provided. + """ + nonce = memoryview(nonce) if nonce is not None else None + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce is not None and nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + if into is None and length is None: + raise TypeError("provide either into or length") + out = into if into is not None else memoryview(bytearray(int(length or 0))) + _lib.aegis256x4_stream( + ffi.from_buffer(out), + out.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def encrypt_unauthenticated( + message: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Encrypt message without authentication (for testing/debugging). + + Args: + message: The plaintext message to encrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write ciphertext into (default: bytearray created). + + Returns: + Ciphertext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + message = memoryview(message) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(message.nbytes)) + if out.nbytes != message.nbytes: + raise TypeError("into length must equal len(message)") + _lib.aegis256x4_encrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(message), + message.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +def decrypt_unauthenticated( + ct: Buffer, + nonce: Buffer, + key: Buffer, + into: Buffer | None = None, +) -> memoryview: + """Decrypt ciphertext without authentication (for testing/debugging). + + Args: + ct: The ciphertext to decrypt. + nonce: Nonce (32 bytes). + key: Key (32 bytes). + into: Buffer to write plaintext into (default: bytearray created). + + Returns: + Plaintext as bytearray if into not provided. + + Raises: + TypeError: If lengths are invalid. + """ + ct = memoryview(ct) + nonce = memoryview(nonce) + key = memoryview(key) + into = memoryview(into) if into is not None else None + + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + out = into if into is not None else memoryview(bytearray(ct.nbytes)) + if out.nbytes != ct.nbytes: + raise TypeError("into length must equal len(ciphertext)") + _lib.aegis256x4_decrypt_unauthenticated( + ffi.from_buffer(out), + _ptr(ct), + ct.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out + + +# This is missing from C API but convenient to have here +def mac( + data: Buffer, + nonce: Buffer, + key: Buffer, + maclen: int = ABYTES_MIN, +) -> memoryview: + """Compute a MAC for the given data in one shot. + + Args: + data: Data to MAC + nonce: Nonce (32 bytes) + key: Key (32 bytes) + maclen: MAC length (16 or 32, default 16) + + Returns: + MAC bytes + """ + mac_state = Mac(nonce, key) + mac_state.update(data) + return mac_state.final(maclen) + + +class Mac: + """AEGIS-256X4 MAC state wrapper. + + Usage: + mac = Mac(nonce, key) + mac.update(data) + tag = mac.final() # defaults to 16-byte MAC + # or verify: + mac2 = Mac(nonce, key); mac2.update(data); mac2.verify(tag) + """ + + __slots__ = ("_st", "_nonce", "_key") + + def __init__( + self, + nonce: Buffer, + key: Buffer, + _other=None, + ) -> None: + """Initialize a MAC state with a nonce and key. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + raw = alloc_aligned(ffi.sizeof("aegis256x4_mac_state"), 64) + st = ffi.cast("aegis256x4_mac_state *", raw) + self._st = ffi.gc(st, libc.free) + if _other is not None: + _lib.aegis256x4_mac_state_clone(self._st, _other._st) + return + # Normal init + nonce = memoryview(nonce) + key = memoryview(key) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES=}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES=}") + _lib.aegis256x4_mac_init(self._st, _ptr(key), _ptr(nonce)) + + def __deepcopy__(self) -> "Mac": + """Return a clone of current MAC state.""" + return Mac(b"", b"", _other=self) + + clone = __deepcopy__ + + def reset(self) -> None: + """Reset the MAC state so it can be reused with the same nonce and key.""" + _lib.aegis256x4_mac_reset(self._st) + + def update(self, data: Buffer) -> None: + """Absorb data into the MAC state. + + Args: + data: Bytes-like object to authenticate. + + Raises: + RuntimeError: If the underlying C function reports an error. + """ + data = memoryview(data) + rc = _lib.aegis256x4_mac_update(self._st, _ptr(data), data.nbytes) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac update failed: {err_name}") + + def final( + self, + maclen: int = ABYTES_MIN, + into: Buffer | None = None, + ) -> memoryview: + """Finalize and return the MAC tag. + + Args: + maclen: Tag length in bytes (16 or 32). Defaults to 16. + into: Optional buffer to write the tag into (default: bytearray created). + + Returns: + The tag as a memoryview; if ``into`` is provided, it views that buffer. + + Raises: + TypeError: If lengths are invalid. + RuntimeError: If finalization fails in the C library. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = into if into is not None else bytearray(maclen) + out = memoryview(out) + if out.nbytes != maclen: + raise TypeError("into length must equal maclen") + rc = _lib.aegis256x4_mac_final(self._st, ffi.from_buffer(out), maclen) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"mac final failed: {err_name}") + return out + + def verify(self, mac: Buffer) -> bool: + """Verify a tag for the current MAC state. + + Args: + mac: The tag to verify (16 or 32 bytes). + + Returns: + True if verification succeeds. + + Raises: + TypeError: If tag length is invalid. + ValueError: If verification fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + rc = _lib.aegis256x4_mac_verify(self._st, _ptr(mac), maclen) + if rc != 0: + raise ValueError("mac verification failed") + return True + + +class Encryptor: + """Incremental encryptor. + + - update(message[, into]) -> returns produced ciphertext bytes + - final([into], maclen=16) -> returns tail+tag bytes + - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental encryptor. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data to bind to the encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis256x4_state"), 64) + st = ffi.cast("aegis256x4_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis256x4_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + """Encrypt a chunk of the message. + + Args: + message: Plaintext bytes to encrypt. + into: Optional destination buffer; must be >= len(message). + + Returns: + The ciphertext for this chunk as a memoryview; when ``into`` is + provided, a view of that buffer up to the number of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + message = memoryview(message) + out = memoryview(into if into is not None else bytearray(message.nbytes)) + if out.nbytes < message.nbytes: + raise TypeError("into length must be >= len(message)") + written = ffi.new("size_t *") + rc = _lib.aegis256x4_state_encrypt_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(message), + message.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt update failed: {err_name}") + return out[: int(written[0])] + + def final(self, into: Buffer | None = None, maclen: int = ABYTES_MIN) -> memoryview: + """Finalize encryption, writing any remaining bytes and the tag. + + Args: + into: Optional destination buffer for the tail and tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A memoryview of the produced bytes (tail + tag). When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If maclen is invalid. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag + out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis256x4_state_encrypt_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt final failed: {err_name}") + return out[: int(written[0])] + + def final_detached( + self, + ct_into: bytearray | None = None, + mac_into: bytearray | None = None, + maclen: int = ABYTES_MIN, + ) -> tuple[bytearray, bytearray]: + """Finalize encryption, producing detached tail bytes and tag. + + Args: + ct_into: Optional destination for the remaining ciphertext tail. + mac_into: Optional destination for the tag. + maclen: Tag length (16 or 32). Defaults to 16. + + Returns: + A tuple of (tail_bytes, mac). When destination buffers are provided, + the first element is a slice of ``ct_into`` up to the number of bytes + written, and the second is ``mac_into``. + + Raises: + TypeError: If maclen is invalid or mac_into has the wrong length. + RuntimeError: If the C final call fails. + """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") + out = ct_into if ct_into is not None else bytearray(TAILBYTES_MAX) + mac = mac_into if mac_into is not None else bytearray(maclen) + if len(mac) != maclen: + raise TypeError("mac_into length must equal maclen") + written = ffi.new("size_t *") + rc = _lib.aegis256x4_state_encrypt_detached_final( + self._st, + ffi.from_buffer(out), + len(out), + written, + ffi.from_buffer(mac), + maclen, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state encrypt detached final failed: {err_name}") + return out[: int(written[0])], mac + + +class Decryptor: + """Incremental decryptor. + + - update(ciphertext[, into]) -> returns plaintext bytes + - final(mac[, into]) -> returns any remaining plaintext bytes + """ + + __slots__ = ("_st",) + + def __init__(self, nonce: Buffer, key: Buffer, ad: Buffer | None = None): + """Create an incremental decryptor for detached tags. + + Args: + nonce: Nonce (32 bytes). + key: Key (32 bytes). + ad: Associated data used during encryption (optional). + + Raises: + TypeError: If key or nonce lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NPUBBYTES: + raise TypeError(f"nonce length must be {NPUBBYTES}") + raw = alloc_aligned(ffi.sizeof("aegis256x4_state"), 64) + st = ffi.cast("aegis256x4_state *", raw) + st = ffi.gc(st, libc.free) + _lib.aegis256x4_state_init( + st, + _ptr(ad) if ad is not None else ffi.NULL, + 0 if ad is None else memoryview(ad).nbytes, + _ptr(nonce), + _ptr(key), + ) + self._st = st + + def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + """Process a chunk of ciphertext. + + Args: + ct: Ciphertext bytes (without MAC). + into: Optional destination buffer; must be >= len(ciphertext). + + Returns: + A memoryview of the decrypted bytes for this chunk. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If destination buffer is too small. + RuntimeError: If the C update call fails. + """ + ct = memoryview(ct) + out = into if into is not None else bytearray(ct.nbytes) + out = memoryview(out) + if out.nbytes < ct.nbytes: + raise TypeError("into length must be >= len(ciphertext)") + written = ffi.new("size_t *") + rc = _lib.aegis256x4_state_decrypt_detached_update( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(ct), + ct.nbytes, + ) + if rc != 0: + err_num = ffi.errno + err_name = errno.errorcode.get(err_num, f"errno_{err_num}") + raise RuntimeError(f"state decrypt update failed: {err_name}") + w = int(written[0]) + return out[:w] + + def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: + """Finalize decryption by verifying tag and flushing remaining bytes. + + Args: + mac: Tag to verify (16 or 32 bytes). + into: Optional destination buffer for the remaining plaintext bytes. + + Returns: + A memoryview of the remaining plaintext bytes. When ``into`` is + provided, the returned view references that buffer up to the number + of bytes written. + + Raises: + TypeError: If tag length is invalid. + ValueError: If authentication fails. + """ + mac = memoryview(mac) + maclen = mac.nbytes + if maclen not in (16, 32): + raise TypeError("mac length must be 16 or 32") + out = into if into is not None else bytearray(TAILBYTES_MAX) + out = memoryview(out) + written = ffi.new("size_t *") + rc = _lib.aegis256x4_state_decrypt_detached_final( + self._st, + ffi.from_buffer(out), + out.nbytes, + written, + _ptr(mac), + maclen, + ) + if rc != 0: + raise ValueError("authentication failed") + w = int(written[0]) + return out[:w] + + +def new_state(): + """Allocate and return a new aegis256x4_state* with proper alignment. + + The returned object is an ffi cdata pointer with automatic finalizer. + """ + # Allocate with 64-byte alignment using libc.posix_memalign + raw = alloc_aligned(ffi.sizeof("aegis256x4_state"), 64) + ptr = ffi.cast("aegis256x4_state *", raw) + return ffi.gc(ptr, libc.free) + + +def new_mac_state(): + """Allocate and return a new aegis256x4_mac_state* with proper alignment.""" + raw = alloc_aligned(ffi.sizeof("aegis256x4_mac_state"), 64) + ptr = ffi.cast("aegis256x4_mac_state *", raw) + return ffi.gc(ptr, libc.free) + + +__all__ = [ + # constants + "KEYBYTES", + "NPUBBYTES", + "ABYTES_MIN", + "ABYTES_MAX", + "TAILBYTES_MAX", + # one-shot functions + "encrypt_detached", + "decrypt_detached", + "encrypt", + "decrypt", + "stream", + "encrypt_unauthenticated", + "decrypt_unauthenticated", + "mac", + # incremental classes + "Encryptor", + "Decryptor", + "Mac", +] diff --git a/examples/aegis256x4_demo.py b/examples/aegis256x4_demo.py new file mode 100644 index 0000000..7c25449 --- /dev/null +++ b/examples/aegis256x4_demo.py @@ -0,0 +1,97 @@ +"""Demonstration script for aegis.aegis256x4 + +Covers: +- encrypt_detached / decrypt_detached +- encrypt / decrypt (attached tag) +- stream_into +- encrypt_unauthenticated_into / decrypt_unauthenticated_into +- MAC (mac_init/update/final/verify) +""" + +import time + +from aegis import aegis256x4 as a + + +def hx(b, limit: int | None = None) -> str: + data = bytes(b) + if limit is not None: + data = data[:limit] + return data.hex() + + +def demo(): + print("KEYBYTES:", a.KEYBYTES, "NPUBBYTES:", a.NPUBBYTES) + + key = b"K" * a.KEYBYTES + nonce = b"N" * a.NPUBBYTES + message = b"hello world" + associated_data = b"header" + + # Detached encrypt/decrypt + ciphertext, mac = a.encrypt_detached( + nonce, key, message, associated_data, maclen=16 + ) + plaintext = a.decrypt_detached(nonce, key, ciphertext, mac, associated_data) + print( + "detached enc: c=", + hx(ciphertext), + " mac=", + hx(mac), + " dec_ok=", + plaintext == message, + ) + + # Attached encrypt/decrypt + ciphertext_with_tag = a.encrypt(nonce, key, message, associated_data, maclen=32) + plaintext2 = a.decrypt(nonce, key, ciphertext_with_tag, associated_data, maclen=32) + print( + "attached enc: ct=", hx(ciphertext_with_tag), " dec_ok=", plaintext2 == message + ) + + # Stream generation (None nonce allowed) -> deterministic for a given key + stream = bytearray(64) + a.stream(None, key, into=stream) + print("stream (first 16 bytes):", hx(stream, 16)) + + # Unauthenticated mode round-trip (INSECURE; compatibility only) + c2 = bytearray(len(message)) + a.encrypt_unauthenticated(message, nonce, key, into=c2) + m2 = bytearray(len(message)) + a.decrypt_unauthenticated(c2, nonce, key, into=m2) + print("unauth round-trip ok:", bytes(m2) == message) + + # MAC: compute then verify + mac_state = a.Mac(nonce, key) + mac_state.update(message) + mac32 = mac_state.final(32) + + mac_verify_state = a.Mac(nonce, key) + mac_verify_state.update(message) + try: + mac_verify_state.verify(mac32) + print("mac verify: ok", " mac=", hx(mac32)) + except ValueError: + print("mac verify: failed") + + # Benchmark: unauthenticated encryption of 1 GiB as a single operation + total_bytes = 1 << 30 # 1 GiB + + def bench_unauth_single(total: int): + src = bytearray(total) + dst = bytearray(total) + t0 = time.perf_counter() + a.encrypt_unauthenticated(src, nonce, key, into=dst) + t1 = time.perf_counter() + secs = t1 - t0 + gib = total / float(1 << 30) + gbps = gib / secs if secs > 0 else float("inf") + print( + f"unauth 1GiB bench (single call): size={gib:.3f} GiB, time={secs:.3f} s, throughput={gbps:.2f} GiB/s" + ) + + bench_unauth_single(total_bytes) + + +if __name__ == "__main__": + demo() diff --git a/examples/benchmark.py b/examples/benchmark.py new file mode 100644 index 0000000..754b030 --- /dev/null +++ b/examples/benchmark.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Python benchmark matching src/test/benchmark.zig for all supported Aegis algorithms. + +It performs two benchmarks with the same parameters as the Zig version: +- AEGIS encrypt (attached tag, maclen = ABYTES_MIN) +- AEGIS MAC (clone state pattern) + +Output format and throughput units mirror the Zig benchmark (Mb/s). +""" + +import os +import time + +from aegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 + +MSG_LEN = 16384000 # 16 MiB +ITERATIONS = 100 + +ALGORITHMS = [ + ("AEGIS-128L", aegis128l), + ("AEGIS-128X2", aegis128x2), + ("AEGIS-128X4", aegis128x4), + ("AEGIS-256", aegis256), + ("AEGIS-256X2", aegis256x2), + ("AEGIS-256X4", aegis256x4), +] + + +def _random_bytes(n: int) -> bytes: + return os.urandom(n) + + +def bench_encrypt(alg_name: str, a) -> None: + key = _random_bytes(a.KEYBYTES) + nonce = _random_bytes(a.NPUBBYTES) + + # Single buffer, as in Zig: c_out == m buffer, with tag appended + maclen = a.ABYTES_MIN + buf = bytearray(MSG_LEN + maclen) + # Initialize buffer with random data + buf[:] = _random_bytes(len(buf)) + + mview = memoryview(buf)[:MSG_LEN] + + t0 = time.perf_counter() + for _ in range(ITERATIONS): + a.encrypt(nonce, key, mview, None, maclen=maclen, into=buf) + t1 = time.perf_counter() + + # Prevent any unrealistic optimization assumptions + _ = buf[0] + + bits = MSG_LEN * ITERATIONS * 8 + elapsed_s = t1 - t0 + throughput_mbps = ( + (bits / (elapsed_s * 1_000_000)) if elapsed_s > 0 else float("inf") + ) + print(f"{alg_name}\t{throughput_mbps:10.2f} Mb/s") + + +def bench_mac(alg_name: str, a) -> None: + key = _random_bytes(a.KEYBYTES) + nonce = _random_bytes(a.NPUBBYTES) + + buf = bytearray(MSG_LEN) + buf[:] = _random_bytes(len(buf)) + + mac0 = a.Mac(nonce, key) + mac_out = bytearray(a.ABYTES_MAX) + + t0 = time.perf_counter() + for _ in range(ITERATIONS): + mac = mac0.clone() + mac.update(buf) + mac.final(maclen=a.ABYTES_MAX, into=mac_out) + t1 = time.perf_counter() + + _ = mac_out[0] + + bits = MSG_LEN * ITERATIONS * 8 + elapsed_s = t1 - t0 + throughput_mbps = ( + (bits / (elapsed_s * 1_000_000)) if elapsed_s > 0 else float("inf") + ) + print(f"{alg_name} MAC\t{throughput_mbps:10.2f} Mb/s") + + +if __name__ == "__main__": + # aegis_init() is called in the loader at import time already + # Run encrypt benchmarks in order: 256, 256x2, 256x4, 128l, 128x2, 128x4 + bench_encrypt("AEGIS-256", aegis256) + bench_encrypt("AEGIS-256X2", aegis256x2) + bench_encrypt("AEGIS-256X4", aegis256x4) + bench_encrypt("AEGIS-128L", aegis128l) + bench_encrypt("AEGIS-128X2", aegis128x2) + bench_encrypt("AEGIS-128X4", aegis128x4) + + # Run MAC benchmarks in order: 128l, 128x2, 128x4, 256, 256x2, 256x4 + bench_mac("AEGIS-128L", aegis128l) + bench_mac("AEGIS-128X2", aegis128x2) + bench_mac("AEGIS-128X4", aegis128x4) + bench_mac("AEGIS-256", aegis256) + bench_mac("AEGIS-256X2", aegis256x2) + bench_mac("AEGIS-256X4", aegis256x4) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b84bf51 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "aegis" +version = "0.1.0" +description = "Python bindings for libaegis (links to system library)" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "License :: OSI Approved :: ISC License (ISCL)", + "Operating System :: OS Independent", + "Topic :: Security :: Cryptography", +] + +[project.urls] +Homepage = "https://github.com/aegis-aead/libaegis"