Initial commit
This commit is contained in:
0
aegis/__init__.py
Normal file
0
aegis/__init__.py
Normal file
505
aegis/_loader.py
Normal file
505
aegis/_loader.py
Normal file
@@ -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]
|
||||
848
aegis/aegis128l.py
Normal file
848
aegis/aegis128l.py
Normal file
@@ -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",
|
||||
]
|
||||
848
aegis/aegis128x2.py
Normal file
848
aegis/aegis128x2.py
Normal file
@@ -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",
|
||||
]
|
||||
848
aegis/aegis128x4.py
Normal file
848
aegis/aegis128x4.py
Normal file
@@ -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",
|
||||
]
|
||||
848
aegis/aegis256.py
Normal file
848
aegis/aegis256.py
Normal file
@@ -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",
|
||||
]
|
||||
848
aegis/aegis256x2.py
Normal file
848
aegis/aegis256x2.py
Normal file
@@ -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",
|
||||
]
|
||||
848
aegis/aegis256x4.py
Normal file
848
aegis/aegis256x4.py
Normal file
@@ -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",
|
||||
]
|
||||
97
examples/aegis256x4_demo.py
Normal file
97
examples/aegis256x4_demo.py
Normal file
@@ -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()
|
||||
105
examples/benchmark.py
Normal file
105
examples/benchmark.py
Normal file
@@ -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)
|
||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user