Add RAF (random-access encrypted file) support and aeg.random CSPRNG
Implement Python bindings for the libaegis 0.10.6 RAF API: - Thin per-variant wrappers (raf_create/open/read/write/truncate/size/ sync/close, merkle_rebuild/verify/commitment) generated from a template in tools/generate.py, with errno-mapped exceptions. - aeg.raf: high-level file-like API (Raf, create/open/probe, derive_master_key) with pluggable Storage backends (FileStorage, BytesIOStorage) and Merkle tree integrity via a MerkleHasher protocol (SHA256MerkleHasher default), adapted from pyaegis (MIT). - aeg.random: CSPRNG facility. Random produces AEGIS keystream seeded once from the OS, exposed per cipher module as cipher.random() and usable as the RAF RNG (as_raf_rng) or standalone RandomSource. - Tests: roundtrip/random-access/seek/truncate/Merkle coverage for all six variants, pyaegis-style API checks, and a Merkle fuzz test.
This commit is contained in:
1 parent
e70c7a37d2
commit
e01d84a560
14 files changed
+5374
-3
No files matched your search
@@ -1,6 +1,7 @@
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .random import Random
|
||||
from .util import Buffer
|
||||
|
||||
__all__ = ["Cipher"]
|
||||
@@ -43,6 +44,8 @@ class Cipher(Protocol):
|
||||
Encryptor: type[_Encryptor]
|
||||
Decryptor: type[_Decryptor]
|
||||
|
||||
@staticmethod
|
||||
def random() -> "Random": ...
|
||||
@staticmethod
|
||||
def random_key() -> bytearray: ...
|
||||
@staticmethod
|
||||
@@ -124,3 +127,35 @@ class Cipher(Protocol):
|
||||
def nonce_increment(nonce: "Buffer") -> None: ...
|
||||
@staticmethod
|
||||
def wipe(buffer: "Buffer") -> None: ...
|
||||
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf)
|
||||
RAF_ALG_ID: int
|
||||
|
||||
@staticmethod
|
||||
def new_raf_ctx(): ...
|
||||
@staticmethod
|
||||
def raf_scratch_size(chunk_size: int) -> int: ...
|
||||
@staticmethod
|
||||
def raf_create(key: "Buffer", ctx, io, rng, config) -> None: ...
|
||||
@staticmethod
|
||||
def raf_open(key: "Buffer", ctx, io, rng, config) -> None: ...
|
||||
@staticmethod
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: "Buffer | None" = None
|
||||
) -> bytearray | memoryview: ...
|
||||
@staticmethod
|
||||
def raf_write(ctx, data: "Buffer", offset: int) -> int: ...
|
||||
@staticmethod
|
||||
def raf_truncate(ctx, size: int) -> None: ...
|
||||
@staticmethod
|
||||
def raf_size(ctx) -> int: ...
|
||||
@staticmethod
|
||||
def raf_sync(ctx) -> None: ...
|
||||
@staticmethod
|
||||
def raf_close(ctx) -> None: ...
|
||||
@staticmethod
|
||||
def raf_merkle_rebuild(ctx) -> None: ...
|
||||
@staticmethod
|
||||
def raf_merkle_verify(ctx) -> "int | None": ...
|
||||
@staticmethod
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes: ...
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from ._loader import ffi
|
||||
from ._loader import lib as _lib
|
||||
from .random import Random
|
||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||
|
||||
NAME = "AEGIS-128L" #: Algorithm display name
|
||||
@@ -19,6 +21,15 @@ ALIGNMENT = 64 #: Required alignment for internal structures
|
||||
RATE = 64 #: Byte chunk size in internal processing
|
||||
|
||||
|
||||
def random() -> Random:
|
||||
"""Return a new AEGIS stream Random instance using this cipher module.
|
||||
|
||||
Seeded once from the OS, then deterministic: successive calls never
|
||||
repeat an output block. See aeg.random.Random for the API.
|
||||
"""
|
||||
return Random(sys.modules[__name__])
|
||||
|
||||
|
||||
def random_key() -> bytearray:
|
||||
"""
|
||||
Generate a secret key using cryptographically secure random bytes.
|
||||
@@ -868,6 +879,220 @@ def new_mac_state():
|
||||
return new_aligned_struct("aegis128l_mac_state", ALIGNMENT)
|
||||
|
||||
|
||||
RAF_ALG_ID = 1 #: Algorithm identifier in RAF file headers
|
||||
|
||||
|
||||
def _raise_errno(operation: str) -> None:
|
||||
err_num = ffi.errno
|
||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"{operation} failed: {err_name}")
|
||||
|
||||
|
||||
def new_raf_ctx():
|
||||
"""Allocate and return a new aegis128l_raf_ctx* with proper alignment."""
|
||||
return new_aligned_struct("aegis128l_raf_ctx", ALIGNMENT)
|
||||
|
||||
|
||||
def raf_scratch_size(chunk_size: int) -> int:
|
||||
"""Return the scratch buffer size required for the given chunk_size."""
|
||||
return _lib.aegis128l_raf_scratch_size(chunk_size)
|
||||
|
||||
|
||||
def raf_create(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Create a new encrypted file, writing the header through io.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileExistsError: If the file exists and AEGIS_RAF_TRUNCATE was not set.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis128l_raf_create(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
if ffi.errno == errno.EEXIST:
|
||||
raise FileExistsError("file exists (use truncate to overwrite)")
|
||||
_raise_errno("raf create")
|
||||
|
||||
|
||||
def raf_open(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Open an existing encrypted file, verifying the header MAC.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the header is invalid or authentication fails (wrong key).
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis128l_raf_open(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num == errno.ENOENT:
|
||||
raise FileNotFoundError("file not found")
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf open")
|
||||
|
||||
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: Buffer | None = None
|
||||
) -> bytearray | memoryview:
|
||||
"""Read and decrypt up to length bytes at offset.
|
||||
|
||||
Returns:
|
||||
The decrypted bytes, trimmed to the number actually read
|
||||
(fewer at EOF), as bytearray if into not provided,
|
||||
memoryview of into otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If into is smaller than length.
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
if into is None:
|
||||
out = bytearray(length)
|
||||
else:
|
||||
into = memoryview(into)
|
||||
if into.nbytes < length:
|
||||
raise TypeError("into length must be at least length")
|
||||
out = into
|
||||
bytes_read = ffi.new("size_t*")
|
||||
rc = _lib.aegis128l_raf_read(
|
||||
ctx.ptr, ffi.from_buffer(out), bytes_read, length, offset
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf read")
|
||||
n = bytes_read[0]
|
||||
return out[:n] if into is None else memoryview(out)[:n] # type: ignore
|
||||
|
||||
|
||||
def raf_write(ctx, data: Buffer, offset: int) -> int:
|
||||
"""Encrypt and write data at offset, extending the file as needed.
|
||||
|
||||
Returns:
|
||||
Number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the write fails (errno name included).
|
||||
"""
|
||||
data = memoryview(data)
|
||||
bytes_written = ffi.new("size_t*")
|
||||
rc = _lib.aegis128l_raf_write(
|
||||
ctx.ptr, bytes_written, _ptr(data), data.nbytes, offset
|
||||
)
|
||||
if rc != 0:
|
||||
_raise_errno("raf write")
|
||||
return bytes_written[0]
|
||||
|
||||
|
||||
def raf_truncate(ctx, size: int) -> None:
|
||||
"""Resize the file to size bytes (zero-filled when growing).
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the truncate fails.
|
||||
"""
|
||||
rc = _lib.aegis128l_raf_truncate(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf truncate")
|
||||
|
||||
|
||||
def raf_size(ctx) -> int:
|
||||
"""Return the logical plaintext file size."""
|
||||
size = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis128l_raf_get_size(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf get_size")
|
||||
return size[0]
|
||||
|
||||
|
||||
def raf_sync(ctx) -> None:
|
||||
"""Flush writes to the backing store.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the sync fails.
|
||||
"""
|
||||
rc = _lib.aegis128l_raf_sync(ctx.ptr)
|
||||
if rc != 0:
|
||||
_raise_errno("raf sync")
|
||||
|
||||
|
||||
def raf_close(ctx) -> None:
|
||||
"""Close the context, syncing and zeroizing key material."""
|
||||
_lib.aegis128l_raf_close(ctx.ptr)
|
||||
|
||||
|
||||
def raf_merkle_rebuild(ctx) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or rebuilding fails.
|
||||
ValueError: If a chunk fails authentication.
|
||||
"""
|
||||
rc = _lib.aegis128l_raf_merkle_rebuild(ctx.ptr)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf merkle_rebuild")
|
||||
|
||||
|
||||
def raf_merkle_verify(ctx) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or verification fails.
|
||||
"""
|
||||
corrupted = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis128l_raf_merkle_verify(ctx.ptr, corrupted)
|
||||
if rc == 0:
|
||||
return None
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
return corrupted[0]
|
||||
_raise_errno("raf merkle_verify")
|
||||
|
||||
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes:
|
||||
"""Compute the Merkle root commitment hash.
|
||||
|
||||
Returns:
|
||||
The commitment hash as bytes of length hash_len.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or the call fails.
|
||||
"""
|
||||
out = ffi.new(f"uint8_t[{hash_len}]")
|
||||
rc = _lib.aegis128l_raf_merkle_commitment(ctx.ptr, out, hash_len)
|
||||
if rc != 0:
|
||||
_raise_errno("raf merkle_commitment")
|
||||
return bytes(ffi.buffer(out, hash_len))
|
||||
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"NAME",
|
||||
@@ -877,7 +1102,9 @@ __all__ = [
|
||||
"MACBYTES_LONG",
|
||||
"ALIGNMENT",
|
||||
"RATE",
|
||||
"RAF_ALG_ID",
|
||||
# utility functions
|
||||
"random",
|
||||
"random_key",
|
||||
"random_nonce",
|
||||
"nonce_increment",
|
||||
@@ -895,4 +1122,18 @@ __all__ = [
|
||||
"Encryptor",
|
||||
"Decryptor",
|
||||
"Mac",
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf for the high-level API)
|
||||
"new_raf_ctx",
|
||||
"raf_scratch_size",
|
||||
"raf_create",
|
||||
"raf_open",
|
||||
"raf_read",
|
||||
"raf_write",
|
||||
"raf_truncate",
|
||||
"raf_size",
|
||||
"raf_sync",
|
||||
"raf_close",
|
||||
"raf_merkle_rebuild",
|
||||
"raf_merkle_verify",
|
||||
"raf_merkle_commitment",
|
||||
]
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from ._loader import ffi
|
||||
from ._loader import lib as _lib
|
||||
from .random import Random
|
||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||
|
||||
NAME = "AEGIS-128X2" #: Algorithm display name
|
||||
@@ -19,6 +21,15 @@ ALIGNMENT = 64 #: Required alignment for internal structures
|
||||
RATE = 64 #: Byte chunk size in internal processing
|
||||
|
||||
|
||||
def random() -> Random:
|
||||
"""Return a new AEGIS stream Random instance using this cipher module.
|
||||
|
||||
Seeded once from the OS, then deterministic: successive calls never
|
||||
repeat an output block. See aeg.random.Random for the API.
|
||||
"""
|
||||
return Random(sys.modules[__name__])
|
||||
|
||||
|
||||
def random_key() -> bytearray:
|
||||
"""
|
||||
Generate a secret key using cryptographically secure random bytes.
|
||||
@@ -868,6 +879,220 @@ def new_mac_state():
|
||||
return new_aligned_struct("aegis128x2_mac_state", ALIGNMENT)
|
||||
|
||||
|
||||
RAF_ALG_ID = 2 #: Algorithm identifier in RAF file headers
|
||||
|
||||
|
||||
def _raise_errno(operation: str) -> None:
|
||||
err_num = ffi.errno
|
||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"{operation} failed: {err_name}")
|
||||
|
||||
|
||||
def new_raf_ctx():
|
||||
"""Allocate and return a new aegis128x2_raf_ctx* with proper alignment."""
|
||||
return new_aligned_struct("aegis128x2_raf_ctx", ALIGNMENT)
|
||||
|
||||
|
||||
def raf_scratch_size(chunk_size: int) -> int:
|
||||
"""Return the scratch buffer size required for the given chunk_size."""
|
||||
return _lib.aegis128x2_raf_scratch_size(chunk_size)
|
||||
|
||||
|
||||
def raf_create(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Create a new encrypted file, writing the header through io.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileExistsError: If the file exists and AEGIS_RAF_TRUNCATE was not set.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis128x2_raf_create(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
if ffi.errno == errno.EEXIST:
|
||||
raise FileExistsError("file exists (use truncate to overwrite)")
|
||||
_raise_errno("raf create")
|
||||
|
||||
|
||||
def raf_open(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Open an existing encrypted file, verifying the header MAC.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the header is invalid or authentication fails (wrong key).
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis128x2_raf_open(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num == errno.ENOENT:
|
||||
raise FileNotFoundError("file not found")
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf open")
|
||||
|
||||
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: Buffer | None = None
|
||||
) -> bytearray | memoryview:
|
||||
"""Read and decrypt up to length bytes at offset.
|
||||
|
||||
Returns:
|
||||
The decrypted bytes, trimmed to the number actually read
|
||||
(fewer at EOF), as bytearray if into not provided,
|
||||
memoryview of into otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If into is smaller than length.
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
if into is None:
|
||||
out = bytearray(length)
|
||||
else:
|
||||
into = memoryview(into)
|
||||
if into.nbytes < length:
|
||||
raise TypeError("into length must be at least length")
|
||||
out = into
|
||||
bytes_read = ffi.new("size_t*")
|
||||
rc = _lib.aegis128x2_raf_read(
|
||||
ctx.ptr, ffi.from_buffer(out), bytes_read, length, offset
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf read")
|
||||
n = bytes_read[0]
|
||||
return out[:n] if into is None else memoryview(out)[:n] # type: ignore
|
||||
|
||||
|
||||
def raf_write(ctx, data: Buffer, offset: int) -> int:
|
||||
"""Encrypt and write data at offset, extending the file as needed.
|
||||
|
||||
Returns:
|
||||
Number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the write fails (errno name included).
|
||||
"""
|
||||
data = memoryview(data)
|
||||
bytes_written = ffi.new("size_t*")
|
||||
rc = _lib.aegis128x2_raf_write(
|
||||
ctx.ptr, bytes_written, _ptr(data), data.nbytes, offset
|
||||
)
|
||||
if rc != 0:
|
||||
_raise_errno("raf write")
|
||||
return bytes_written[0]
|
||||
|
||||
|
||||
def raf_truncate(ctx, size: int) -> None:
|
||||
"""Resize the file to size bytes (zero-filled when growing).
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the truncate fails.
|
||||
"""
|
||||
rc = _lib.aegis128x2_raf_truncate(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf truncate")
|
||||
|
||||
|
||||
def raf_size(ctx) -> int:
|
||||
"""Return the logical plaintext file size."""
|
||||
size = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis128x2_raf_get_size(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf get_size")
|
||||
return size[0]
|
||||
|
||||
|
||||
def raf_sync(ctx) -> None:
|
||||
"""Flush writes to the backing store.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the sync fails.
|
||||
"""
|
||||
rc = _lib.aegis128x2_raf_sync(ctx.ptr)
|
||||
if rc != 0:
|
||||
_raise_errno("raf sync")
|
||||
|
||||
|
||||
def raf_close(ctx) -> None:
|
||||
"""Close the context, syncing and zeroizing key material."""
|
||||
_lib.aegis128x2_raf_close(ctx.ptr)
|
||||
|
||||
|
||||
def raf_merkle_rebuild(ctx) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or rebuilding fails.
|
||||
ValueError: If a chunk fails authentication.
|
||||
"""
|
||||
rc = _lib.aegis128x2_raf_merkle_rebuild(ctx.ptr)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf merkle_rebuild")
|
||||
|
||||
|
||||
def raf_merkle_verify(ctx) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or verification fails.
|
||||
"""
|
||||
corrupted = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis128x2_raf_merkle_verify(ctx.ptr, corrupted)
|
||||
if rc == 0:
|
||||
return None
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
return corrupted[0]
|
||||
_raise_errno("raf merkle_verify")
|
||||
|
||||
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes:
|
||||
"""Compute the Merkle root commitment hash.
|
||||
|
||||
Returns:
|
||||
The commitment hash as bytes of length hash_len.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or the call fails.
|
||||
"""
|
||||
out = ffi.new(f"uint8_t[{hash_len}]")
|
||||
rc = _lib.aegis128x2_raf_merkle_commitment(ctx.ptr, out, hash_len)
|
||||
if rc != 0:
|
||||
_raise_errno("raf merkle_commitment")
|
||||
return bytes(ffi.buffer(out, hash_len))
|
||||
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"NAME",
|
||||
@@ -877,7 +1102,9 @@ __all__ = [
|
||||
"MACBYTES_LONG",
|
||||
"ALIGNMENT",
|
||||
"RATE",
|
||||
"RAF_ALG_ID",
|
||||
# utility functions
|
||||
"random",
|
||||
"random_key",
|
||||
"random_nonce",
|
||||
"nonce_increment",
|
||||
@@ -895,4 +1122,18 @@ __all__ = [
|
||||
"Encryptor",
|
||||
"Decryptor",
|
||||
"Mac",
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf for the high-level API)
|
||||
"new_raf_ctx",
|
||||
"raf_scratch_size",
|
||||
"raf_create",
|
||||
"raf_open",
|
||||
"raf_read",
|
||||
"raf_write",
|
||||
"raf_truncate",
|
||||
"raf_size",
|
||||
"raf_sync",
|
||||
"raf_close",
|
||||
"raf_merkle_rebuild",
|
||||
"raf_merkle_verify",
|
||||
"raf_merkle_commitment",
|
||||
]
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from ._loader import ffi
|
||||
from ._loader import lib as _lib
|
||||
from .random import Random
|
||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||
|
||||
NAME = "AEGIS-128X4" #: Algorithm display name
|
||||
@@ -19,6 +21,15 @@ ALIGNMENT = 64 #: Required alignment for internal structures
|
||||
RATE = 64 #: Byte chunk size in internal processing
|
||||
|
||||
|
||||
def random() -> Random:
|
||||
"""Return a new AEGIS stream Random instance using this cipher module.
|
||||
|
||||
Seeded once from the OS, then deterministic: successive calls never
|
||||
repeat an output block. See aeg.random.Random for the API.
|
||||
"""
|
||||
return Random(sys.modules[__name__])
|
||||
|
||||
|
||||
def random_key() -> bytearray:
|
||||
"""
|
||||
Generate a secret key using cryptographically secure random bytes.
|
||||
@@ -868,6 +879,220 @@ def new_mac_state():
|
||||
return new_aligned_struct("aegis128x4_mac_state", ALIGNMENT)
|
||||
|
||||
|
||||
RAF_ALG_ID = 3 #: Algorithm identifier in RAF file headers
|
||||
|
||||
|
||||
def _raise_errno(operation: str) -> None:
|
||||
err_num = ffi.errno
|
||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"{operation} failed: {err_name}")
|
||||
|
||||
|
||||
def new_raf_ctx():
|
||||
"""Allocate and return a new aegis128x4_raf_ctx* with proper alignment."""
|
||||
return new_aligned_struct("aegis128x4_raf_ctx", ALIGNMENT)
|
||||
|
||||
|
||||
def raf_scratch_size(chunk_size: int) -> int:
|
||||
"""Return the scratch buffer size required for the given chunk_size."""
|
||||
return _lib.aegis128x4_raf_scratch_size(chunk_size)
|
||||
|
||||
|
||||
def raf_create(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Create a new encrypted file, writing the header through io.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileExistsError: If the file exists and AEGIS_RAF_TRUNCATE was not set.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis128x4_raf_create(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
if ffi.errno == errno.EEXIST:
|
||||
raise FileExistsError("file exists (use truncate to overwrite)")
|
||||
_raise_errno("raf create")
|
||||
|
||||
|
||||
def raf_open(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Open an existing encrypted file, verifying the header MAC.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the header is invalid or authentication fails (wrong key).
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis128x4_raf_open(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num == errno.ENOENT:
|
||||
raise FileNotFoundError("file not found")
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf open")
|
||||
|
||||
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: Buffer | None = None
|
||||
) -> bytearray | memoryview:
|
||||
"""Read and decrypt up to length bytes at offset.
|
||||
|
||||
Returns:
|
||||
The decrypted bytes, trimmed to the number actually read
|
||||
(fewer at EOF), as bytearray if into not provided,
|
||||
memoryview of into otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If into is smaller than length.
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
if into is None:
|
||||
out = bytearray(length)
|
||||
else:
|
||||
into = memoryview(into)
|
||||
if into.nbytes < length:
|
||||
raise TypeError("into length must be at least length")
|
||||
out = into
|
||||
bytes_read = ffi.new("size_t*")
|
||||
rc = _lib.aegis128x4_raf_read(
|
||||
ctx.ptr, ffi.from_buffer(out), bytes_read, length, offset
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf read")
|
||||
n = bytes_read[0]
|
||||
return out[:n] if into is None else memoryview(out)[:n] # type: ignore
|
||||
|
||||
|
||||
def raf_write(ctx, data: Buffer, offset: int) -> int:
|
||||
"""Encrypt and write data at offset, extending the file as needed.
|
||||
|
||||
Returns:
|
||||
Number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the write fails (errno name included).
|
||||
"""
|
||||
data = memoryview(data)
|
||||
bytes_written = ffi.new("size_t*")
|
||||
rc = _lib.aegis128x4_raf_write(
|
||||
ctx.ptr, bytes_written, _ptr(data), data.nbytes, offset
|
||||
)
|
||||
if rc != 0:
|
||||
_raise_errno("raf write")
|
||||
return bytes_written[0]
|
||||
|
||||
|
||||
def raf_truncate(ctx, size: int) -> None:
|
||||
"""Resize the file to size bytes (zero-filled when growing).
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the truncate fails.
|
||||
"""
|
||||
rc = _lib.aegis128x4_raf_truncate(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf truncate")
|
||||
|
||||
|
||||
def raf_size(ctx) -> int:
|
||||
"""Return the logical plaintext file size."""
|
||||
size = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis128x4_raf_get_size(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf get_size")
|
||||
return size[0]
|
||||
|
||||
|
||||
def raf_sync(ctx) -> None:
|
||||
"""Flush writes to the backing store.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the sync fails.
|
||||
"""
|
||||
rc = _lib.aegis128x4_raf_sync(ctx.ptr)
|
||||
if rc != 0:
|
||||
_raise_errno("raf sync")
|
||||
|
||||
|
||||
def raf_close(ctx) -> None:
|
||||
"""Close the context, syncing and zeroizing key material."""
|
||||
_lib.aegis128x4_raf_close(ctx.ptr)
|
||||
|
||||
|
||||
def raf_merkle_rebuild(ctx) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or rebuilding fails.
|
||||
ValueError: If a chunk fails authentication.
|
||||
"""
|
||||
rc = _lib.aegis128x4_raf_merkle_rebuild(ctx.ptr)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf merkle_rebuild")
|
||||
|
||||
|
||||
def raf_merkle_verify(ctx) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or verification fails.
|
||||
"""
|
||||
corrupted = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis128x4_raf_merkle_verify(ctx.ptr, corrupted)
|
||||
if rc == 0:
|
||||
return None
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
return corrupted[0]
|
||||
_raise_errno("raf merkle_verify")
|
||||
|
||||
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes:
|
||||
"""Compute the Merkle root commitment hash.
|
||||
|
||||
Returns:
|
||||
The commitment hash as bytes of length hash_len.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or the call fails.
|
||||
"""
|
||||
out = ffi.new(f"uint8_t[{hash_len}]")
|
||||
rc = _lib.aegis128x4_raf_merkle_commitment(ctx.ptr, out, hash_len)
|
||||
if rc != 0:
|
||||
_raise_errno("raf merkle_commitment")
|
||||
return bytes(ffi.buffer(out, hash_len))
|
||||
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"NAME",
|
||||
@@ -877,7 +1102,9 @@ __all__ = [
|
||||
"MACBYTES_LONG",
|
||||
"ALIGNMENT",
|
||||
"RATE",
|
||||
"RAF_ALG_ID",
|
||||
# utility functions
|
||||
"random",
|
||||
"random_key",
|
||||
"random_nonce",
|
||||
"nonce_increment",
|
||||
@@ -895,4 +1122,18 @@ __all__ = [
|
||||
"Encryptor",
|
||||
"Decryptor",
|
||||
"Mac",
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf for the high-level API)
|
||||
"new_raf_ctx",
|
||||
"raf_scratch_size",
|
||||
"raf_create",
|
||||
"raf_open",
|
||||
"raf_read",
|
||||
"raf_write",
|
||||
"raf_truncate",
|
||||
"raf_size",
|
||||
"raf_sync",
|
||||
"raf_close",
|
||||
"raf_merkle_rebuild",
|
||||
"raf_merkle_verify",
|
||||
"raf_merkle_commitment",
|
||||
]
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from ._loader import ffi
|
||||
from ._loader import lib as _lib
|
||||
from .random import Random
|
||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||
|
||||
NAME = "AEGIS-256" #: Algorithm display name
|
||||
@@ -19,6 +21,15 @@ ALIGNMENT = 64 #: Required alignment for internal structures
|
||||
RATE = 64 #: Byte chunk size in internal processing
|
||||
|
||||
|
||||
def random() -> Random:
|
||||
"""Return a new AEGIS stream Random instance using this cipher module.
|
||||
|
||||
Seeded once from the OS, then deterministic: successive calls never
|
||||
repeat an output block. See aeg.random.Random for the API.
|
||||
"""
|
||||
return Random(sys.modules[__name__])
|
||||
|
||||
|
||||
def random_key() -> bytearray:
|
||||
"""
|
||||
Generate a secret key using cryptographically secure random bytes.
|
||||
@@ -868,6 +879,220 @@ def new_mac_state():
|
||||
return new_aligned_struct("aegis256_mac_state", ALIGNMENT)
|
||||
|
||||
|
||||
RAF_ALG_ID = 4 #: Algorithm identifier in RAF file headers
|
||||
|
||||
|
||||
def _raise_errno(operation: str) -> None:
|
||||
err_num = ffi.errno
|
||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"{operation} failed: {err_name}")
|
||||
|
||||
|
||||
def new_raf_ctx():
|
||||
"""Allocate and return a new aegis256_raf_ctx* with proper alignment."""
|
||||
return new_aligned_struct("aegis256_raf_ctx", ALIGNMENT)
|
||||
|
||||
|
||||
def raf_scratch_size(chunk_size: int) -> int:
|
||||
"""Return the scratch buffer size required for the given chunk_size."""
|
||||
return _lib.aegis256_raf_scratch_size(chunk_size)
|
||||
|
||||
|
||||
def raf_create(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Create a new encrypted file, writing the header through io.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileExistsError: If the file exists and AEGIS_RAF_TRUNCATE was not set.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis256_raf_create(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
if ffi.errno == errno.EEXIST:
|
||||
raise FileExistsError("file exists (use truncate to overwrite)")
|
||||
_raise_errno("raf create")
|
||||
|
||||
|
||||
def raf_open(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Open an existing encrypted file, verifying the header MAC.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the header is invalid or authentication fails (wrong key).
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis256_raf_open(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num == errno.ENOENT:
|
||||
raise FileNotFoundError("file not found")
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf open")
|
||||
|
||||
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: Buffer | None = None
|
||||
) -> bytearray | memoryview:
|
||||
"""Read and decrypt up to length bytes at offset.
|
||||
|
||||
Returns:
|
||||
The decrypted bytes, trimmed to the number actually read
|
||||
(fewer at EOF), as bytearray if into not provided,
|
||||
memoryview of into otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If into is smaller than length.
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
if into is None:
|
||||
out = bytearray(length)
|
||||
else:
|
||||
into = memoryview(into)
|
||||
if into.nbytes < length:
|
||||
raise TypeError("into length must be at least length")
|
||||
out = into
|
||||
bytes_read = ffi.new("size_t*")
|
||||
rc = _lib.aegis256_raf_read(
|
||||
ctx.ptr, ffi.from_buffer(out), bytes_read, length, offset
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf read")
|
||||
n = bytes_read[0]
|
||||
return out[:n] if into is None else memoryview(out)[:n] # type: ignore
|
||||
|
||||
|
||||
def raf_write(ctx, data: Buffer, offset: int) -> int:
|
||||
"""Encrypt and write data at offset, extending the file as needed.
|
||||
|
||||
Returns:
|
||||
Number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the write fails (errno name included).
|
||||
"""
|
||||
data = memoryview(data)
|
||||
bytes_written = ffi.new("size_t*")
|
||||
rc = _lib.aegis256_raf_write(
|
||||
ctx.ptr, bytes_written, _ptr(data), data.nbytes, offset
|
||||
)
|
||||
if rc != 0:
|
||||
_raise_errno("raf write")
|
||||
return bytes_written[0]
|
||||
|
||||
|
||||
def raf_truncate(ctx, size: int) -> None:
|
||||
"""Resize the file to size bytes (zero-filled when growing).
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the truncate fails.
|
||||
"""
|
||||
rc = _lib.aegis256_raf_truncate(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf truncate")
|
||||
|
||||
|
||||
def raf_size(ctx) -> int:
|
||||
"""Return the logical plaintext file size."""
|
||||
size = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis256_raf_get_size(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf get_size")
|
||||
return size[0]
|
||||
|
||||
|
||||
def raf_sync(ctx) -> None:
|
||||
"""Flush writes to the backing store.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the sync fails.
|
||||
"""
|
||||
rc = _lib.aegis256_raf_sync(ctx.ptr)
|
||||
if rc != 0:
|
||||
_raise_errno("raf sync")
|
||||
|
||||
|
||||
def raf_close(ctx) -> None:
|
||||
"""Close the context, syncing and zeroizing key material."""
|
||||
_lib.aegis256_raf_close(ctx.ptr)
|
||||
|
||||
|
||||
def raf_merkle_rebuild(ctx) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or rebuilding fails.
|
||||
ValueError: If a chunk fails authentication.
|
||||
"""
|
||||
rc = _lib.aegis256_raf_merkle_rebuild(ctx.ptr)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf merkle_rebuild")
|
||||
|
||||
|
||||
def raf_merkle_verify(ctx) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or verification fails.
|
||||
"""
|
||||
corrupted = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis256_raf_merkle_verify(ctx.ptr, corrupted)
|
||||
if rc == 0:
|
||||
return None
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
return corrupted[0]
|
||||
_raise_errno("raf merkle_verify")
|
||||
|
||||
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes:
|
||||
"""Compute the Merkle root commitment hash.
|
||||
|
||||
Returns:
|
||||
The commitment hash as bytes of length hash_len.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or the call fails.
|
||||
"""
|
||||
out = ffi.new(f"uint8_t[{hash_len}]")
|
||||
rc = _lib.aegis256_raf_merkle_commitment(ctx.ptr, out, hash_len)
|
||||
if rc != 0:
|
||||
_raise_errno("raf merkle_commitment")
|
||||
return bytes(ffi.buffer(out, hash_len))
|
||||
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"NAME",
|
||||
@@ -877,7 +1102,9 @@ __all__ = [
|
||||
"MACBYTES_LONG",
|
||||
"ALIGNMENT",
|
||||
"RATE",
|
||||
"RAF_ALG_ID",
|
||||
# utility functions
|
||||
"random",
|
||||
"random_key",
|
||||
"random_nonce",
|
||||
"nonce_increment",
|
||||
@@ -895,4 +1122,18 @@ __all__ = [
|
||||
"Encryptor",
|
||||
"Decryptor",
|
||||
"Mac",
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf for the high-level API)
|
||||
"new_raf_ctx",
|
||||
"raf_scratch_size",
|
||||
"raf_create",
|
||||
"raf_open",
|
||||
"raf_read",
|
||||
"raf_write",
|
||||
"raf_truncate",
|
||||
"raf_size",
|
||||
"raf_sync",
|
||||
"raf_close",
|
||||
"raf_merkle_rebuild",
|
||||
"raf_merkle_verify",
|
||||
"raf_merkle_commitment",
|
||||
]
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from ._loader import ffi
|
||||
from ._loader import lib as _lib
|
||||
from .random import Random
|
||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||
|
||||
NAME = "AEGIS-256X2" #: Algorithm display name
|
||||
@@ -19,6 +21,15 @@ ALIGNMENT = 64 #: Required alignment for internal structures
|
||||
RATE = 64 #: Byte chunk size in internal processing
|
||||
|
||||
|
||||
def random() -> Random:
|
||||
"""Return a new AEGIS stream Random instance using this cipher module.
|
||||
|
||||
Seeded once from the OS, then deterministic: successive calls never
|
||||
repeat an output block. See aeg.random.Random for the API.
|
||||
"""
|
||||
return Random(sys.modules[__name__])
|
||||
|
||||
|
||||
def random_key() -> bytearray:
|
||||
"""
|
||||
Generate a secret key using cryptographically secure random bytes.
|
||||
@@ -868,6 +879,220 @@ def new_mac_state():
|
||||
return new_aligned_struct("aegis256x2_mac_state", ALIGNMENT)
|
||||
|
||||
|
||||
RAF_ALG_ID = 5 #: Algorithm identifier in RAF file headers
|
||||
|
||||
|
||||
def _raise_errno(operation: str) -> None:
|
||||
err_num = ffi.errno
|
||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"{operation} failed: {err_name}")
|
||||
|
||||
|
||||
def new_raf_ctx():
|
||||
"""Allocate and return a new aegis256x2_raf_ctx* with proper alignment."""
|
||||
return new_aligned_struct("aegis256x2_raf_ctx", ALIGNMENT)
|
||||
|
||||
|
||||
def raf_scratch_size(chunk_size: int) -> int:
|
||||
"""Return the scratch buffer size required for the given chunk_size."""
|
||||
return _lib.aegis256x2_raf_scratch_size(chunk_size)
|
||||
|
||||
|
||||
def raf_create(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Create a new encrypted file, writing the header through io.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileExistsError: If the file exists and AEGIS_RAF_TRUNCATE was not set.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis256x2_raf_create(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
if ffi.errno == errno.EEXIST:
|
||||
raise FileExistsError("file exists (use truncate to overwrite)")
|
||||
_raise_errno("raf create")
|
||||
|
||||
|
||||
def raf_open(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Open an existing encrypted file, verifying the header MAC.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the header is invalid or authentication fails (wrong key).
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis256x2_raf_open(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num == errno.ENOENT:
|
||||
raise FileNotFoundError("file not found")
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf open")
|
||||
|
||||
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: Buffer | None = None
|
||||
) -> bytearray | memoryview:
|
||||
"""Read and decrypt up to length bytes at offset.
|
||||
|
||||
Returns:
|
||||
The decrypted bytes, trimmed to the number actually read
|
||||
(fewer at EOF), as bytearray if into not provided,
|
||||
memoryview of into otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If into is smaller than length.
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
if into is None:
|
||||
out = bytearray(length)
|
||||
else:
|
||||
into = memoryview(into)
|
||||
if into.nbytes < length:
|
||||
raise TypeError("into length must be at least length")
|
||||
out = into
|
||||
bytes_read = ffi.new("size_t*")
|
||||
rc = _lib.aegis256x2_raf_read(
|
||||
ctx.ptr, ffi.from_buffer(out), bytes_read, length, offset
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf read")
|
||||
n = bytes_read[0]
|
||||
return out[:n] if into is None else memoryview(out)[:n] # type: ignore
|
||||
|
||||
|
||||
def raf_write(ctx, data: Buffer, offset: int) -> int:
|
||||
"""Encrypt and write data at offset, extending the file as needed.
|
||||
|
||||
Returns:
|
||||
Number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the write fails (errno name included).
|
||||
"""
|
||||
data = memoryview(data)
|
||||
bytes_written = ffi.new("size_t*")
|
||||
rc = _lib.aegis256x2_raf_write(
|
||||
ctx.ptr, bytes_written, _ptr(data), data.nbytes, offset
|
||||
)
|
||||
if rc != 0:
|
||||
_raise_errno("raf write")
|
||||
return bytes_written[0]
|
||||
|
||||
|
||||
def raf_truncate(ctx, size: int) -> None:
|
||||
"""Resize the file to size bytes (zero-filled when growing).
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the truncate fails.
|
||||
"""
|
||||
rc = _lib.aegis256x2_raf_truncate(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf truncate")
|
||||
|
||||
|
||||
def raf_size(ctx) -> int:
|
||||
"""Return the logical plaintext file size."""
|
||||
size = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis256x2_raf_get_size(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf get_size")
|
||||
return size[0]
|
||||
|
||||
|
||||
def raf_sync(ctx) -> None:
|
||||
"""Flush writes to the backing store.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the sync fails.
|
||||
"""
|
||||
rc = _lib.aegis256x2_raf_sync(ctx.ptr)
|
||||
if rc != 0:
|
||||
_raise_errno("raf sync")
|
||||
|
||||
|
||||
def raf_close(ctx) -> None:
|
||||
"""Close the context, syncing and zeroizing key material."""
|
||||
_lib.aegis256x2_raf_close(ctx.ptr)
|
||||
|
||||
|
||||
def raf_merkle_rebuild(ctx) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or rebuilding fails.
|
||||
ValueError: If a chunk fails authentication.
|
||||
"""
|
||||
rc = _lib.aegis256x2_raf_merkle_rebuild(ctx.ptr)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf merkle_rebuild")
|
||||
|
||||
|
||||
def raf_merkle_verify(ctx) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or verification fails.
|
||||
"""
|
||||
corrupted = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis256x2_raf_merkle_verify(ctx.ptr, corrupted)
|
||||
if rc == 0:
|
||||
return None
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
return corrupted[0]
|
||||
_raise_errno("raf merkle_verify")
|
||||
|
||||
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes:
|
||||
"""Compute the Merkle root commitment hash.
|
||||
|
||||
Returns:
|
||||
The commitment hash as bytes of length hash_len.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or the call fails.
|
||||
"""
|
||||
out = ffi.new(f"uint8_t[{hash_len}]")
|
||||
rc = _lib.aegis256x2_raf_merkle_commitment(ctx.ptr, out, hash_len)
|
||||
if rc != 0:
|
||||
_raise_errno("raf merkle_commitment")
|
||||
return bytes(ffi.buffer(out, hash_len))
|
||||
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"NAME",
|
||||
@@ -877,7 +1102,9 @@ __all__ = [
|
||||
"MACBYTES_LONG",
|
||||
"ALIGNMENT",
|
||||
"RATE",
|
||||
"RAF_ALG_ID",
|
||||
# utility functions
|
||||
"random",
|
||||
"random_key",
|
||||
"random_nonce",
|
||||
"nonce_increment",
|
||||
@@ -895,4 +1122,18 @@ __all__ = [
|
||||
"Encryptor",
|
||||
"Decryptor",
|
||||
"Mac",
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf for the high-level API)
|
||||
"new_raf_ctx",
|
||||
"raf_scratch_size",
|
||||
"raf_create",
|
||||
"raf_open",
|
||||
"raf_read",
|
||||
"raf_write",
|
||||
"raf_truncate",
|
||||
"raf_size",
|
||||
"raf_sync",
|
||||
"raf_close",
|
||||
"raf_merkle_rebuild",
|
||||
"raf_merkle_verify",
|
||||
"raf_merkle_commitment",
|
||||
]
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from ._loader import ffi
|
||||
from ._loader import lib as _lib
|
||||
from .random import Random
|
||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||
|
||||
NAME = "AEGIS-256X4" #: Algorithm display name
|
||||
@@ -19,6 +21,15 @@ ALIGNMENT = 64 #: Required alignment for internal structures
|
||||
RATE = 64 #: Byte chunk size in internal processing
|
||||
|
||||
|
||||
def random() -> Random:
|
||||
"""Return a new AEGIS stream Random instance using this cipher module.
|
||||
|
||||
Seeded once from the OS, then deterministic: successive calls never
|
||||
repeat an output block. See aeg.random.Random for the API.
|
||||
"""
|
||||
return Random(sys.modules[__name__])
|
||||
|
||||
|
||||
def random_key() -> bytearray:
|
||||
"""
|
||||
Generate a secret key using cryptographically secure random bytes.
|
||||
@@ -868,6 +879,220 @@ def new_mac_state():
|
||||
return new_aligned_struct("aegis256x4_mac_state", ALIGNMENT)
|
||||
|
||||
|
||||
RAF_ALG_ID = 6 #: Algorithm identifier in RAF file headers
|
||||
|
||||
|
||||
def _raise_errno(operation: str) -> None:
|
||||
err_num = ffi.errno
|
||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"{operation} failed: {err_name}")
|
||||
|
||||
|
||||
def new_raf_ctx():
|
||||
"""Allocate and return a new aegis256x4_raf_ctx* with proper alignment."""
|
||||
return new_aligned_struct("aegis256x4_raf_ctx", ALIGNMENT)
|
||||
|
||||
|
||||
def raf_scratch_size(chunk_size: int) -> int:
|
||||
"""Return the scratch buffer size required for the given chunk_size."""
|
||||
return _lib.aegis256x4_raf_scratch_size(chunk_size)
|
||||
|
||||
|
||||
def raf_create(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Create a new encrypted file, writing the header through io.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileExistsError: If the file exists and AEGIS_RAF_TRUNCATE was not set.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis256x4_raf_create(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
if ffi.errno == errno.EEXIST:
|
||||
raise FileExistsError("file exists (use truncate to overwrite)")
|
||||
_raise_errno("raf create")
|
||||
|
||||
|
||||
def raf_open(key: Buffer, ctx, io, rng, config) -> None:
|
||||
"""Open an existing encrypted file, verifying the header MAC.
|
||||
|
||||
Args:
|
||||
key: Master key (KEYBYTES bytes).
|
||||
ctx: Context from new_raf_ctx().
|
||||
io: aegis_raf_io* cdata (see aeg.raf).
|
||||
rng: aegis_raf_rng* cdata (see aeg.random).
|
||||
config: aegis_raf_config* cdata (see aeg.raf).
|
||||
|
||||
Raises:
|
||||
TypeError: If key length is invalid.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the header is invalid or authentication fails (wrong key).
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
key = memoryview(key)
|
||||
if key.nbytes != KEYBYTES:
|
||||
raise TypeError(f"key length must be {KEYBYTES}")
|
||||
rc = _lib.aegis256x4_raf_open(ctx.ptr, io, rng, config, _ptr(key))
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num == errno.ENOENT:
|
||||
raise FileNotFoundError("file not found")
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf open")
|
||||
|
||||
|
||||
def raf_read(
|
||||
ctx, length: int, offset: int, *, into: Buffer | None = None
|
||||
) -> bytearray | memoryview:
|
||||
"""Read and decrypt up to length bytes at offset.
|
||||
|
||||
Returns:
|
||||
The decrypted bytes, trimmed to the number actually read
|
||||
(fewer at EOF), as bytearray if into not provided,
|
||||
memoryview of into otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If into is smaller than length.
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
if into is None:
|
||||
out = bytearray(length)
|
||||
else:
|
||||
into = memoryview(into)
|
||||
if into.nbytes < length:
|
||||
raise TypeError("into length must be at least length")
|
||||
out = into
|
||||
bytes_read = ffi.new("size_t*")
|
||||
rc = _lib.aegis256x4_raf_read(
|
||||
ctx.ptr, ffi.from_buffer(out), bytes_read, length, offset
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf read")
|
||||
n = bytes_read[0]
|
||||
return out[:n] if into is None else memoryview(out)[:n] # type: ignore
|
||||
|
||||
|
||||
def raf_write(ctx, data: Buffer, offset: int) -> int:
|
||||
"""Encrypt and write data at offset, extending the file as needed.
|
||||
|
||||
Returns:
|
||||
Number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the write fails (errno name included).
|
||||
"""
|
||||
data = memoryview(data)
|
||||
bytes_written = ffi.new("size_t*")
|
||||
rc = _lib.aegis256x4_raf_write(
|
||||
ctx.ptr, bytes_written, _ptr(data), data.nbytes, offset
|
||||
)
|
||||
if rc != 0:
|
||||
_raise_errno("raf write")
|
||||
return bytes_written[0]
|
||||
|
||||
|
||||
def raf_truncate(ctx, size: int) -> None:
|
||||
"""Resize the file to size bytes (zero-filled when growing).
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the truncate fails.
|
||||
"""
|
||||
rc = _lib.aegis256x4_raf_truncate(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf truncate")
|
||||
|
||||
|
||||
def raf_size(ctx) -> int:
|
||||
"""Return the logical plaintext file size."""
|
||||
size = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis256x4_raf_get_size(ctx.ptr, size)
|
||||
if rc != 0:
|
||||
_raise_errno("raf get_size")
|
||||
return size[0]
|
||||
|
||||
|
||||
def raf_sync(ctx) -> None:
|
||||
"""Flush writes to the backing store.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the sync fails.
|
||||
"""
|
||||
rc = _lib.aegis256x4_raf_sync(ctx.ptr)
|
||||
if rc != 0:
|
||||
_raise_errno("raf sync")
|
||||
|
||||
|
||||
def raf_close(ctx) -> None:
|
||||
"""Close the context, syncing and zeroizing key material."""
|
||||
_lib.aegis256x4_raf_close(ctx.ptr)
|
||||
|
||||
|
||||
def raf_merkle_rebuild(ctx) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or rebuilding fails.
|
||||
ValueError: If a chunk fails authentication.
|
||||
"""
|
||||
rc = _lib.aegis256x4_raf_merkle_rebuild(ctx.ptr)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
raise ValueError("authentication failed")
|
||||
_raise_errno("raf merkle_rebuild")
|
||||
|
||||
|
||||
def raf_merkle_verify(ctx) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or verification fails.
|
||||
"""
|
||||
corrupted = ffi.new("uint64_t*")
|
||||
rc = _lib.aegis256x4_raf_merkle_verify(ctx.ptr, corrupted)
|
||||
if rc == 0:
|
||||
return None
|
||||
err_num = ffi.errno
|
||||
if err_num in (errno.EINVAL, getattr(errno, "EBADMSG", -1)):
|
||||
return corrupted[0]
|
||||
_raise_errno("raf merkle_verify")
|
||||
|
||||
|
||||
def raf_merkle_commitment(ctx, hash_len: int) -> bytes:
|
||||
"""Compute the Merkle root commitment hash.
|
||||
|
||||
Returns:
|
||||
The commitment hash as bytes of length hash_len.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Merkle is not enabled or the call fails.
|
||||
"""
|
||||
out = ffi.new(f"uint8_t[{hash_len}]")
|
||||
rc = _lib.aegis256x4_raf_merkle_commitment(ctx.ptr, out, hash_len)
|
||||
if rc != 0:
|
||||
_raise_errno("raf merkle_commitment")
|
||||
return bytes(ffi.buffer(out, hash_len))
|
||||
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"NAME",
|
||||
@@ -877,7 +1102,9 @@ __all__ = [
|
||||
"MACBYTES_LONG",
|
||||
"ALIGNMENT",
|
||||
"RATE",
|
||||
"RAF_ALG_ID",
|
||||
# utility functions
|
||||
"random",
|
||||
"random_key",
|
||||
"random_nonce",
|
||||
"nonce_increment",
|
||||
@@ -895,4 +1122,18 @@ __all__ = [
|
||||
"Encryptor",
|
||||
"Decryptor",
|
||||
"Mac",
|
||||
# random-access encrypted files (thin wrappers; see aeg.raf for the high-level API)
|
||||
"new_raf_ctx",
|
||||
"raf_scratch_size",
|
||||
"raf_create",
|
||||
"raf_open",
|
||||
"raf_read",
|
||||
"raf_write",
|
||||
"raf_truncate",
|
||||
"raf_size",
|
||||
"raf_sync",
|
||||
"raf_close",
|
||||
"raf_merkle_rebuild",
|
||||
"raf_merkle_verify",
|
||||
"raf_merkle_commitment",
|
||||
]
|
||||
@@ -475,3 +475,232 @@ 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);
|
||||
|
||||
/* aegis_raf.h */
|
||||
typedef struct aegis_raf_scratch { uint8_t *buf; size_t len; } aegis_raf_scratch;
|
||||
typedef struct aegis_raf_io { void *user; int (*read_at)(void *user,
|
||||
uint8_t *buf,
|
||||
size_t len,
|
||||
uint64_t off); int (*write_at)(void *user,
|
||||
const uint8_t *buf,
|
||||
size_t len,
|
||||
uint64_t off); int (*get_size)(void *user,
|
||||
uint64_t *size); int (*set_size)(void *user,
|
||||
uint64_t size); int (*sync)(void *user); } aegis_raf_io;
|
||||
typedef struct aegis_raf_rng { void *user; int (*random)(void *user, uint8_t *out, size_t len); } aegis_raf_rng;
|
||||
typedef struct aegis_raf_merkle_config { int (*hash_leaf)(void *user,
|
||||
uint8_t *out,
|
||||
size_t out_len,
|
||||
const uint8_t *chunk,
|
||||
size_t chunk_len,
|
||||
uint64_t chunk_idx); int (*hash_parent)(void *user,
|
||||
uint8_t *out,
|
||||
size_t out_len,
|
||||
const uint8_t *left,
|
||||
const uint8_t *right,
|
||||
uint32_t level,
|
||||
uint64_t node_idx); int (*hash_empty)(void *user,
|
||||
uint8_t *out,
|
||||
size_t out_len,
|
||||
uint32_t level,
|
||||
uint64_t node_idx); int (*hash_commitment)(void *user,
|
||||
uint8_t *out,
|
||||
size_t out_len,
|
||||
const uint8_t *structural_root,
|
||||
const uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
uint64_t file_size); void *user; uint8_t *buf; size_t len; uint64_t max_chunks; uint32_t hash_len; } aegis_raf_merkle_config;
|
||||
typedef struct aegis_raf_config { const aegis_raf_scratch *scratch; const aegis_raf_merkle_config *merkle; uint32_t chunk_size; uint8_t flags; } aegis_raf_config;
|
||||
typedef struct aegis_raf_info { uint64_t file_size; uint32_t chunk_size; uint8_t alg_id; } aegis_raf_info;
|
||||
typedef struct aegis128l_raf_ctx { uint8_t opaque[512]; } aegis128l_raf_ctx;
|
||||
typedef struct aegis128x2_raf_ctx { uint8_t opaque[512]; } aegis128x2_raf_ctx;
|
||||
typedef struct aegis128x4_raf_ctx { uint8_t opaque[512]; } aegis128x4_raf_ctx;
|
||||
typedef struct aegis256_raf_ctx { uint8_t opaque[512]; } aegis256_raf_ctx;
|
||||
typedef struct aegis256x2_raf_ctx { uint8_t opaque[512]; } aegis256x2_raf_ctx;
|
||||
typedef struct aegis256x4_raf_ctx { uint8_t opaque[512]; } aegis256x4_raf_ctx;
|
||||
size_t aegis_raf_chunk_min(void);
|
||||
size_t aegis_raf_chunk_max(void);
|
||||
size_t aegis_raf_header_size(void);
|
||||
size_t aegis_raf_scratch_align(void);
|
||||
int aegis_raf_probe(const aegis_raf_io *io, aegis_raf_info *info);
|
||||
size_t aegis128l_raf_scratch_size(uint32_t chunk_size);
|
||||
int aegis128l_raf_scratch_validate(const aegis_raf_scratch *scratch, uint32_t chunk_size);
|
||||
size_t aegis128x2_raf_scratch_size(uint32_t chunk_size);
|
||||
int aegis128x2_raf_scratch_validate(const aegis_raf_scratch *scratch, uint32_t chunk_size);
|
||||
size_t aegis128x4_raf_scratch_size(uint32_t chunk_size);
|
||||
int aegis128x4_raf_scratch_validate(const aegis_raf_scratch *scratch, uint32_t chunk_size);
|
||||
size_t aegis256_raf_scratch_size(uint32_t chunk_size);
|
||||
int aegis256_raf_scratch_validate(const aegis_raf_scratch *scratch, uint32_t chunk_size);
|
||||
size_t aegis256x2_raf_scratch_size(uint32_t chunk_size);
|
||||
int aegis256x2_raf_scratch_validate(const aegis_raf_scratch *scratch, uint32_t chunk_size);
|
||||
size_t aegis256x4_raf_scratch_size(uint32_t chunk_size);
|
||||
int aegis256x4_raf_scratch_validate(const aegis_raf_scratch *scratch, uint32_t chunk_size);
|
||||
size_t aegis_raf_merkle_buffer_size(const aegis_raf_merkle_config *cfg);
|
||||
int aegis_raf_derive_master_key(uint8_t *out,
|
||||
size_t out_len,
|
||||
const uint8_t *master_key,
|
||||
size_t master_key_len,
|
||||
const uint8_t *context,
|
||||
size_t context_len);
|
||||
int aegis128l_raf_create(aegis128l_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis128l_raf_open(aegis128l_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis128l_raf_read(aegis128l_raf_ctx *ctx,
|
||||
uint8_t *out,
|
||||
size_t *bytes_read,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis128l_raf_write(aegis128l_raf_ctx *ctx,
|
||||
size_t *bytes_written,
|
||||
const uint8_t *in,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis128l_raf_truncate(aegis128l_raf_ctx *ctx, uint64_t size);
|
||||
int aegis128l_raf_get_size(const aegis128l_raf_ctx *ctx, uint64_t *size);
|
||||
int aegis128l_raf_sync(aegis128l_raf_ctx *ctx);
|
||||
void aegis128l_raf_close(aegis128l_raf_ctx *ctx);
|
||||
int aegis128l_raf_merkle_rebuild(aegis128l_raf_ctx *ctx);
|
||||
int aegis128l_raf_merkle_verify(aegis128l_raf_ctx *ctx, uint64_t *corrupted_chunk);
|
||||
int aegis128l_raf_merkle_commitment(const aegis128l_raf_ctx *ctx, uint8_t *out, size_t out_len);
|
||||
int aegis128x2_raf_create(aegis128x2_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis128x2_raf_open(aegis128x2_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis128x2_raf_read(aegis128x2_raf_ctx *ctx,
|
||||
uint8_t *out,
|
||||
size_t *bytes_read,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis128x2_raf_write(aegis128x2_raf_ctx *ctx,
|
||||
size_t *bytes_written,
|
||||
const uint8_t *in,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis128x2_raf_truncate(aegis128x2_raf_ctx *ctx, uint64_t size);
|
||||
int aegis128x2_raf_get_size(const aegis128x2_raf_ctx *ctx, uint64_t *size);
|
||||
int aegis128x2_raf_sync(aegis128x2_raf_ctx *ctx);
|
||||
void aegis128x2_raf_close(aegis128x2_raf_ctx *ctx);
|
||||
int aegis128x2_raf_merkle_rebuild(aegis128x2_raf_ctx *ctx);
|
||||
int aegis128x2_raf_merkle_verify(aegis128x2_raf_ctx *ctx, uint64_t *corrupted_chunk);
|
||||
int aegis128x2_raf_merkle_commitment(const aegis128x2_raf_ctx *ctx, uint8_t *out, size_t out_len);
|
||||
int aegis128x4_raf_create(aegis128x4_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis128x4_raf_open(aegis128x4_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis128x4_raf_read(aegis128x4_raf_ctx *ctx,
|
||||
uint8_t *out,
|
||||
size_t *bytes_read,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis128x4_raf_write(aegis128x4_raf_ctx *ctx,
|
||||
size_t *bytes_written,
|
||||
const uint8_t *in,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis128x4_raf_truncate(aegis128x4_raf_ctx *ctx, uint64_t size);
|
||||
int aegis128x4_raf_get_size(const aegis128x4_raf_ctx *ctx, uint64_t *size);
|
||||
int aegis128x4_raf_sync(aegis128x4_raf_ctx *ctx);
|
||||
void aegis128x4_raf_close(aegis128x4_raf_ctx *ctx);
|
||||
int aegis128x4_raf_merkle_rebuild(aegis128x4_raf_ctx *ctx);
|
||||
int aegis128x4_raf_merkle_verify(aegis128x4_raf_ctx *ctx, uint64_t *corrupted_chunk);
|
||||
int aegis128x4_raf_merkle_commitment(const aegis128x4_raf_ctx *ctx, uint8_t *out, size_t out_len);
|
||||
int aegis256_raf_create(aegis256_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis256_raf_open(aegis256_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis256_raf_read(aegis256_raf_ctx *ctx,
|
||||
uint8_t *out,
|
||||
size_t *bytes_read,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis256_raf_write(aegis256_raf_ctx *ctx,
|
||||
size_t *bytes_written,
|
||||
const uint8_t *in,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis256_raf_truncate(aegis256_raf_ctx *ctx, uint64_t size);
|
||||
int aegis256_raf_get_size(const aegis256_raf_ctx *ctx, uint64_t *size);
|
||||
int aegis256_raf_sync(aegis256_raf_ctx *ctx);
|
||||
void aegis256_raf_close(aegis256_raf_ctx *ctx);
|
||||
int aegis256_raf_merkle_rebuild(aegis256_raf_ctx *ctx);
|
||||
int aegis256_raf_merkle_verify(aegis256_raf_ctx *ctx, uint64_t *corrupted_chunk);
|
||||
int aegis256_raf_merkle_commitment(const aegis256_raf_ctx *ctx, uint8_t *out, size_t out_len);
|
||||
int aegis256x2_raf_create(aegis256x2_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis256x2_raf_open(aegis256x2_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis256x2_raf_read(aegis256x2_raf_ctx *ctx,
|
||||
uint8_t *out,
|
||||
size_t *bytes_read,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis256x2_raf_write(aegis256x2_raf_ctx *ctx,
|
||||
size_t *bytes_written,
|
||||
const uint8_t *in,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis256x2_raf_truncate(aegis256x2_raf_ctx *ctx, uint64_t size);
|
||||
int aegis256x2_raf_get_size(const aegis256x2_raf_ctx *ctx, uint64_t *size);
|
||||
int aegis256x2_raf_sync(aegis256x2_raf_ctx *ctx);
|
||||
void aegis256x2_raf_close(aegis256x2_raf_ctx *ctx);
|
||||
int aegis256x2_raf_merkle_rebuild(aegis256x2_raf_ctx *ctx);
|
||||
int aegis256x2_raf_merkle_verify(aegis256x2_raf_ctx *ctx, uint64_t *corrupted_chunk);
|
||||
int aegis256x2_raf_merkle_commitment(const aegis256x2_raf_ctx *ctx, uint8_t *out, size_t out_len);
|
||||
int aegis256x4_raf_create(aegis256x4_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis256x4_raf_open(aegis256x4_raf_ctx *ctx,
|
||||
const aegis_raf_io *io,
|
||||
const aegis_raf_rng *rng,
|
||||
const aegis_raf_config *cfg,
|
||||
const uint8_t *master_key);
|
||||
int aegis256x4_raf_read(aegis256x4_raf_ctx *ctx,
|
||||
uint8_t *out,
|
||||
size_t *bytes_read,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis256x4_raf_write(aegis256x4_raf_ctx *ctx,
|
||||
size_t *bytes_written,
|
||||
const uint8_t *in,
|
||||
size_t len,
|
||||
uint64_t offset);
|
||||
int aegis256x4_raf_truncate(aegis256x4_raf_ctx *ctx, uint64_t size);
|
||||
int aegis256x4_raf_get_size(const aegis256x4_raf_ctx *ctx, uint64_t *size);
|
||||
int aegis256x4_raf_sync(aegis256x4_raf_ctx *ctx);
|
||||
void aegis256x4_raf_close(aegis256x4_raf_ctx *ctx);
|
||||
int aegis256x4_raf_merkle_rebuild(aegis256x4_raf_ctx *ctx);
|
||||
int aegis256x4_raf_merkle_verify(aegis256x4_raf_ctx *ctx, uint64_t *corrupted_chunk);
|
||||
int aegis256x4_raf_merkle_commitment(const aegis256x4_raf_ctx *ctx, uint8_t *out, size_t out_len);
|
||||
+917
@@ -0,0 +1,917 @@
|
||||
"""RAF (random-access encrypted file) high-level API.
|
||||
|
||||
Provides pread/pwrite-style access to encrypted files. Files are divided into
|
||||
fixed-size chunks, each independently encrypted with a fresh nonce, enabling
|
||||
efficient random access without decrypting the whole file.
|
||||
|
||||
Use create() / open() with a cipher module (from aeg.cipher()) and either a
|
||||
path or a Storage instance.
|
||||
|
||||
Portions of the storage/callback/Merkle/file-like machinery are adapted from
|
||||
pyaegis (https://github.com/jedisct1/pyaegis) by Frank Denis, MIT license.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno as errno_module
|
||||
import hashlib
|
||||
import os
|
||||
import threading
|
||||
from typing import NamedTuple, Protocol, runtime_checkable
|
||||
|
||||
from . import cipher as _cipher_for_name
|
||||
from . import random as _random
|
||||
from ._loader import ffi, lib
|
||||
from ._typing import Cipher
|
||||
from .util import Buffer
|
||||
|
||||
__all__ = [
|
||||
"CHUNK_MIN",
|
||||
"CHUNK_MAX",
|
||||
"HEADER_SIZE",
|
||||
"SCRATCH_ALIGN",
|
||||
"Storage",
|
||||
"FileStorage",
|
||||
"BytesIOStorage",
|
||||
"MerkleHasher",
|
||||
"SHA256MerkleHasher",
|
||||
"RafInfo",
|
||||
"Raf",
|
||||
"create",
|
||||
"open",
|
||||
"probe",
|
||||
"derive_master_key",
|
||||
]
|
||||
|
||||
CHUNK_MIN = int(lib.aegis_raf_chunk_min()) #: Minimum chunk size in bytes
|
||||
CHUNK_MAX = int(lib.aegis_raf_chunk_max()) #: Maximum chunk size in bytes
|
||||
HEADER_SIZE = int(lib.aegis_raf_header_size()) #: Encrypted file header size
|
||||
SCRATCH_ALIGN = int(lib.aegis_raf_scratch_align()) #: Required scratch buffer alignment
|
||||
|
||||
# From aegis_raf.h (preprocessor macros, not available through cdef)
|
||||
_FLAG_CREATE = 0x01
|
||||
_FLAG_TRUNCATE = 0x02
|
||||
_MERKLE_HASH_MIN = 8
|
||||
_MERKLE_HASH_MAX = 64
|
||||
|
||||
@runtime_checkable
|
||||
class Storage(Protocol):
|
||||
"""Backing storage for RAF files. Operations must complete fully or raise OSError."""
|
||||
|
||||
def read_at(self, buf: bytearray, offset: int) -> None:
|
||||
"""Read exactly len(buf) bytes at offset into buf."""
|
||||
...
|
||||
|
||||
def write_at(self, data: bytes, offset: int) -> None:
|
||||
"""Write exactly len(data) bytes at offset."""
|
||||
...
|
||||
|
||||
def get_size(self) -> int:
|
||||
"""Return current storage size in bytes."""
|
||||
...
|
||||
|
||||
def set_size(self, size: int) -> None:
|
||||
"""Resize storage (truncate or extend)."""
|
||||
...
|
||||
|
||||
def sync(self) -> None:
|
||||
"""Flush writes to durable storage. May be a no-op."""
|
||||
...
|
||||
|
||||
|
||||
class FileStorage:
|
||||
"""File-based storage.
|
||||
|
||||
Uses os.pread/os.pwrite where available (Unix); otherwise falls back to
|
||||
a lock plus os.lseek/os.read/os.write (e.g. on Windows).
|
||||
"""
|
||||
|
||||
def __init__(self, path: str | os.PathLike, mode: str = "r+b"):
|
||||
"""Open a file for RAF storage.
|
||||
|
||||
Args:
|
||||
path: Path to the file.
|
||||
mode: "r+b" to open existing, "w+b" or "x+b" to create.
|
||||
"""
|
||||
if mode == "r+b":
|
||||
flags = os.O_RDWR
|
||||
elif mode == "w+b":
|
||||
flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC
|
||||
elif mode == "x+b":
|
||||
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||
else:
|
||||
raise ValueError(f"Unsupported mode: {mode}")
|
||||
self._fd = os.open(path, flags, 0o644)
|
||||
self._closed = False
|
||||
self._lock = threading.Lock()
|
||||
self._positional = hasattr(os, "pread") and hasattr(os, "pwrite")
|
||||
|
||||
def read_at(self, buf: bytearray, offset: int) -> None:
|
||||
"""Read exactly len(buf) bytes. Raises OSError on short read."""
|
||||
view = memoryview(buf)
|
||||
if self._positional:
|
||||
done = 0
|
||||
while done < len(view):
|
||||
chunk = os.pread(self._fd, len(view) - done, offset + done)
|
||||
if not chunk:
|
||||
raise OSError(f"Short read: expected {len(view)}, got {done}")
|
||||
view[done : done + len(chunk)] = chunk
|
||||
done += len(chunk)
|
||||
else:
|
||||
with self._lock:
|
||||
os.lseek(self._fd, offset, os.SEEK_SET)
|
||||
done = 0
|
||||
while done < len(view):
|
||||
chunk = os.read(self._fd, len(view) - done)
|
||||
if not chunk:
|
||||
raise OSError(f"Short read: expected {len(view)}, got {done}")
|
||||
view[done : done + len(chunk)] = chunk
|
||||
done += len(chunk)
|
||||
|
||||
def write_at(self, data: bytes, offset: int) -> None:
|
||||
"""Write exactly len(data) bytes."""
|
||||
view = memoryview(data)
|
||||
if self._positional:
|
||||
done = 0
|
||||
while done < len(view):
|
||||
done += os.pwrite(self._fd, view[done:], offset + done)
|
||||
else:
|
||||
with self._lock:
|
||||
os.lseek(self._fd, offset, os.SEEK_SET)
|
||||
done = 0
|
||||
while done < len(view):
|
||||
done += os.write(self._fd, view[done:])
|
||||
|
||||
def get_size(self) -> int:
|
||||
"""Return current file size."""
|
||||
return os.fstat(self._fd).st_size
|
||||
|
||||
def set_size(self, size: int) -> None:
|
||||
"""Truncate or extend the file to the given size."""
|
||||
os.ftruncate(self._fd, size)
|
||||
|
||||
def sync(self) -> None:
|
||||
"""Flush file data to disk."""
|
||||
os.fsync(self._fd)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the underlying file descriptor."""
|
||||
if not self._closed:
|
||||
os.close(self._fd)
|
||||
self._closed = True
|
||||
|
||||
def __enter__(self) -> FileStorage:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
self.close()
|
||||
|
||||
|
||||
class BytesIOStorage:
|
||||
"""In-memory storage backed by a bytearray. Useful for tests."""
|
||||
|
||||
def __init__(self, initial: bytes = b""):
|
||||
self._data = bytearray(initial)
|
||||
|
||||
def read_at(self, buf: bytearray, offset: int) -> None:
|
||||
"""Read exactly len(buf) bytes at offset into buf."""
|
||||
end = offset + len(buf)
|
||||
if end > len(self._data):
|
||||
raise OSError(
|
||||
f"Read past EOF: offset={offset}, len={len(buf)}, size={len(self._data)}"
|
||||
)
|
||||
buf[:] = self._data[offset:end]
|
||||
|
||||
def write_at(self, data: bytes, offset: int) -> None:
|
||||
"""Write exactly len(data) bytes at offset."""
|
||||
if not data:
|
||||
return
|
||||
end = offset + len(data)
|
||||
if end > len(self._data):
|
||||
self._data.extend(b"\x00" * (end - len(self._data)))
|
||||
self._data[offset:end] = data
|
||||
|
||||
def get_size(self) -> int:
|
||||
"""Return current buffer size."""
|
||||
return len(self._data)
|
||||
|
||||
def set_size(self, size: int) -> None:
|
||||
"""Resize the buffer (truncate or extend with zeros)."""
|
||||
current = len(self._data)
|
||||
if size < current:
|
||||
del self._data[size:]
|
||||
elif size > current:
|
||||
self._data.extend(b"\x00" * (size - current))
|
||||
|
||||
def sync(self) -> None:
|
||||
"""No-op for in-memory storage."""
|
||||
|
||||
def getvalue(self) -> bytes:
|
||||
"""Return the entire buffer contents."""
|
||||
return bytes(self._data)
|
||||
|
||||
def __enter__(self) -> BytesIOStorage:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class MerkleHasher(Protocol):
|
||||
"""Protocol for Merkle tree hash functions."""
|
||||
|
||||
@property
|
||||
def hash_len(self) -> int: ...
|
||||
|
||||
def hash_leaf(self, chunk: bytes, chunk_len: int, chunk_idx: int) -> bytes: ...
|
||||
def hash_parent(
|
||||
self, left: bytes, right: bytes, level: int, node_idx: int
|
||||
) -> bytes: ...
|
||||
def hash_empty(self, level: int, node_idx: int) -> bytes: ...
|
||||
def hash_commitment(
|
||||
self, structural_root: bytes, ctx: bytes, file_size: int
|
||||
) -> bytes: ...
|
||||
|
||||
|
||||
class SHA256MerkleHasher:
|
||||
"""Default Merkle hasher using SHA-256 with domain-separated prefixes."""
|
||||
|
||||
hash_len = 32
|
||||
|
||||
def hash_leaf(self, chunk: bytes, chunk_len: int, chunk_idx: int) -> bytes:
|
||||
h = hashlib.sha256()
|
||||
h.update(b"\x00")
|
||||
h.update(chunk_idx.to_bytes(8, "little"))
|
||||
h.update(chunk[:chunk_len])
|
||||
return h.digest()
|
||||
|
||||
def hash_parent(
|
||||
self, left: bytes, right: bytes, level: int, node_idx: int
|
||||
) -> bytes:
|
||||
h = hashlib.sha256()
|
||||
h.update(b"\x01")
|
||||
h.update(level.to_bytes(4, "little"))
|
||||
h.update(node_idx.to_bytes(8, "little"))
|
||||
h.update(left)
|
||||
h.update(right)
|
||||
return h.digest()
|
||||
|
||||
def hash_empty(self, level: int, node_idx: int) -> bytes:
|
||||
h = hashlib.sha256()
|
||||
h.update(b"\x02")
|
||||
h.update(level.to_bytes(4, "little"))
|
||||
h.update(node_idx.to_bytes(8, "little"))
|
||||
return h.digest()
|
||||
|
||||
def hash_commitment(
|
||||
self, structural_root: bytes, ctx: bytes, file_size: int
|
||||
) -> bytes:
|
||||
h = hashlib.sha256()
|
||||
h.update(b"\x03")
|
||||
h.update(structural_root)
|
||||
h.update(ctx)
|
||||
h.update(file_size.to_bytes(8, "little"))
|
||||
return h.digest()
|
||||
|
||||
|
||||
# CFFI callbacks (module-level so they stay alive)
|
||||
|
||||
|
||||
@ffi.callback("int(void*, uint8_t*, size_t, uint64_t)")
|
||||
def _read_at_callback(user, buf, length, offset):
|
||||
try:
|
||||
storage = ffi.from_handle(user)
|
||||
temp = bytearray(length)
|
||||
storage.read_at(temp, offset)
|
||||
ffi.buffer(buf, length)[:] = temp
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EIO
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback("int(void*, const uint8_t*, size_t, uint64_t)")
|
||||
def _write_at_callback(user, buf, length, offset):
|
||||
try:
|
||||
storage = ffi.from_handle(user)
|
||||
storage.write_at(bytes(ffi.buffer(buf, length)), offset)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EIO
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback("int(void*, uint64_t*)")
|
||||
def _get_size_callback(user, size_ptr):
|
||||
try:
|
||||
size_ptr[0] = ffi.from_handle(user).get_size()
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EIO
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback("int(void*, uint64_t)")
|
||||
def _set_size_callback(user, size):
|
||||
try:
|
||||
ffi.from_handle(user).set_size(size)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EIO
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback("int(void*)")
|
||||
def _sync_callback(user):
|
||||
try:
|
||||
ffi.from_handle(user).sync()
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EIO
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback("int(void*, uint8_t*, size_t, const uint8_t*, size_t, uint64_t)")
|
||||
def _hash_leaf_callback(user, out, out_len, chunk, chunk_len, chunk_idx):
|
||||
try:
|
||||
hasher = ffi.from_handle(user)
|
||||
digest = hasher.hash_leaf(
|
||||
bytes(ffi.buffer(chunk, chunk_len)), chunk_len, chunk_idx
|
||||
)
|
||||
if len(digest) != out_len:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
ffi.memmove(out, digest, out_len)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback(
|
||||
"int(void*, uint8_t*, size_t, const uint8_t*, const uint8_t*, uint32_t, uint64_t)"
|
||||
)
|
||||
def _hash_parent_callback(user, out, out_len, left, right, level, node_idx):
|
||||
try:
|
||||
hasher = ffi.from_handle(user)
|
||||
digest = hasher.hash_parent(
|
||||
bytes(ffi.buffer(left, out_len)),
|
||||
bytes(ffi.buffer(right, out_len)),
|
||||
level,
|
||||
node_idx,
|
||||
)
|
||||
if len(digest) != out_len:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
ffi.memmove(out, digest, out_len)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback("int(void*, uint8_t*, size_t, uint32_t, uint64_t)")
|
||||
def _hash_empty_callback(user, out, out_len, level, node_idx):
|
||||
try:
|
||||
hasher = ffi.from_handle(user)
|
||||
digest = hasher.hash_empty(level, node_idx)
|
||||
if len(digest) != out_len:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
ffi.memmove(out, digest, out_len)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
|
||||
|
||||
@ffi.callback(
|
||||
"int(void*, uint8_t*, size_t, const uint8_t*, const uint8_t*, size_t, uint64_t)"
|
||||
)
|
||||
def _hash_commitment_callback(
|
||||
user, out, out_len, structural_root, ctx, ctx_len, file_size
|
||||
):
|
||||
try:
|
||||
hasher = ffi.from_handle(user)
|
||||
root_bytes = bytes(ffi.buffer(structural_root, out_len))
|
||||
ctx_bytes = bytes(ffi.buffer(ctx, ctx_len)) if ctx != ffi.NULL else b""
|
||||
digest = hasher.hash_commitment(root_bytes, ctx_bytes, file_size)
|
||||
if len(digest) != out_len:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
ffi.memmove(out, digest, out_len)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno_module.EINVAL
|
||||
return -1
|
||||
|
||||
|
||||
def _make_io(storage: Storage):
|
||||
"""Build an aegis_raf_io* cdata for a Storage. Returns (io, handle)."""
|
||||
handle = ffi.new_handle(storage)
|
||||
io = ffi.new("aegis_raf_io*")
|
||||
io.user = handle
|
||||
io.read_at = _read_at_callback
|
||||
io.write_at = _write_at_callback
|
||||
io.get_size = _get_size_callback
|
||||
io.set_size = _set_size_callback
|
||||
io.sync = _sync_callback
|
||||
return io, handle
|
||||
|
||||
|
||||
def _allocate_scratch(cipher: Cipher, chunk_size: int):
|
||||
"""Allocate an aligned aegis_raf_scratch*. Returns (raw, scratch)."""
|
||||
size = cipher.raf_scratch_size(chunk_size)
|
||||
raw = bytearray(size + SCRATCH_ALIGN)
|
||||
raw_ptr = ffi.from_buffer(raw)
|
||||
offset = (-int(ffi.cast("uintptr_t", raw_ptr))) & (SCRATCH_ALIGN - 1)
|
||||
scratch = ffi.new("aegis_raf_scratch*")
|
||||
scratch.buf = ffi.cast("uint8_t*", raw_ptr) + offset
|
||||
scratch.len = size
|
||||
return raw, scratch
|
||||
|
||||
|
||||
def derive_master_key(master_key: Buffer, context: Buffer = b"") -> bytes:
|
||||
"""Derive a context-bound RAF master key from an application master key.
|
||||
|
||||
Args:
|
||||
master_key: A 16-byte or 32-byte application master key.
|
||||
context: A public identifier for a file or file family
|
||||
(at most 120 bytes for 16-byte keys, 72 for 32-byte keys).
|
||||
|
||||
Returns:
|
||||
A RAF-scoped key of the same length as master_key.
|
||||
|
||||
Raises:
|
||||
ValueError: If the key or context length is invalid.
|
||||
RuntimeError: If the C KDF rejects validated inputs.
|
||||
"""
|
||||
master_key = memoryview(master_key)
|
||||
context = memoryview(context)
|
||||
key_size = master_key.nbytes
|
||||
if key_size not in (16, 32):
|
||||
raise ValueError(f"master_key must be 16 or 32 bytes, got {key_size}")
|
||||
max_context = 120 if key_size == 16 else 72
|
||||
if context.nbytes > max_context:
|
||||
raise ValueError(
|
||||
f"context must be at most {max_context} bytes for a {key_size}-byte key"
|
||||
)
|
||||
out = bytearray(key_size)
|
||||
rc = lib.aegis_raf_derive_master_key(
|
||||
ffi.from_buffer(out),
|
||||
key_size,
|
||||
bytes(master_key),
|
||||
key_size,
|
||||
bytes(context),
|
||||
context.nbytes,
|
||||
)
|
||||
if rc != 0:
|
||||
err_num = ffi.errno
|
||||
err_name = errno_module.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"derive master key failed: {err_name}")
|
||||
return bytes(out)
|
||||
|
||||
|
||||
class RafInfo(NamedTuple):
|
||||
"""Unverified file metadata from probe()."""
|
||||
|
||||
alg_id: int
|
||||
chunk_size: int
|
||||
file_size: int
|
||||
|
||||
|
||||
def probe(storage: Storage) -> RafInfo:
|
||||
"""Probe an encrypted file to determine its parameters.
|
||||
|
||||
WARNING: the returned values are read from the header WITHOUT
|
||||
cryptographic verification. Only use them for scratch sizing and
|
||||
algorithm selection; open() verifies the header MAC with the key.
|
||||
|
||||
Returns:
|
||||
RafInfo(alg_id, chunk_size, file_size).
|
||||
|
||||
Raises:
|
||||
RuntimeError: Invalid header or I/O failure.
|
||||
"""
|
||||
io, _handle = _make_io(storage)
|
||||
info = ffi.new("aegis_raf_info*")
|
||||
if lib.aegis_raf_probe(io, info) != 0:
|
||||
err_num = ffi.errno
|
||||
err_name = errno_module.errorcode.get(err_num, f"errno_{err_num}")
|
||||
raise RuntimeError(f"probe failed: {err_name}")
|
||||
return RafInfo(int(info.alg_id), int(info.chunk_size), int(info.file_size))
|
||||
|
||||
|
||||
def _as_storage(storage_or_path, mode: str) -> tuple[Storage, bool]:
|
||||
if isinstance(storage_or_path, (str, bytes, os.PathLike)):
|
||||
return FileStorage(storage_or_path, mode), True
|
||||
return storage_or_path, False
|
||||
|
||||
|
||||
def _resolve_cipher(cipher) -> Cipher:
|
||||
if isinstance(cipher, str):
|
||||
return _cipher_for_name(cipher)
|
||||
return cipher
|
||||
|
||||
|
||||
def create(
|
||||
storage_or_path,
|
||||
key: Buffer,
|
||||
cipher,
|
||||
*,
|
||||
chunk_size: int = 65536,
|
||||
truncate: bool = False,
|
||||
merkle: bool | MerkleHasher = False,
|
||||
merkle_max_chunks: int = 16384,
|
||||
rng: "_random.RandomSource | None" = None,
|
||||
) -> "Raf":
|
||||
"""Create a new encrypted file.
|
||||
|
||||
Args:
|
||||
storage_or_path: A Storage instance or a path (opened "x+b", or "w+b"
|
||||
when truncate=True).
|
||||
key: Master key (cipher.KEYBYTES bytes).
|
||||
cipher: A cipher module (from aeg.cipher()) or algorithm name.
|
||||
chunk_size: Plaintext bytes per chunk (CHUNK_MIN..CHUNK_MAX, multiple of 16).
|
||||
truncate: Overwrite an existing file.
|
||||
merkle: Enable Merkle tree. True uses SHA256MerkleHasher, or pass a
|
||||
custom MerkleHasher instance.
|
||||
merkle_max_chunks: Maximum number of chunks the Merkle tree can track.
|
||||
rng: Random source (see aeg.random). Default: a fresh aeg.random.Random
|
||||
seeded for the same cipher module passed as cipher=; any callable(n)
|
||||
returning bytes overrides.
|
||||
|
||||
Raises:
|
||||
TypeError: If the key length is invalid.
|
||||
ValueError: If chunk_size or merkle parameters are invalid.
|
||||
FileExistsError: If the file exists and truncate is False.
|
||||
RuntimeError: If creation fails.
|
||||
"""
|
||||
cipher = _resolve_cipher(cipher)
|
||||
if not (CHUNK_MIN <= chunk_size <= CHUNK_MAX):
|
||||
raise ValueError(
|
||||
f"chunk_size must be {CHUNK_MIN}-{CHUNK_MAX}, got {chunk_size}"
|
||||
)
|
||||
if chunk_size % 16 != 0:
|
||||
raise ValueError(f"chunk_size must be a multiple of 16, got {chunk_size}")
|
||||
storage, owns = _as_storage(storage_or_path, "w+b" if truncate else "x+b")
|
||||
return Raf(
|
||||
storage,
|
||||
key,
|
||||
cipher,
|
||||
owns_storage=owns,
|
||||
creating=True,
|
||||
chunk_size=chunk_size,
|
||||
truncate=truncate,
|
||||
merkle=merkle,
|
||||
merkle_max_chunks=merkle_max_chunks,
|
||||
rng=rng,
|
||||
)
|
||||
|
||||
|
||||
def open(
|
||||
storage_or_path,
|
||||
key: Buffer,
|
||||
cipher,
|
||||
*,
|
||||
merkle: bool | MerkleHasher = False,
|
||||
merkle_max_chunks: int = 16384,
|
||||
rng: "_random.RandomSource | None" = None,
|
||||
) -> "Raf":
|
||||
"""Open an existing encrypted file.
|
||||
|
||||
The header is probed to size internal buffers and its alg_id is checked
|
||||
against cipher.RAF_ALG_ID; a mismatch raises ValueError (the algorithm is
|
||||
never switched silently).
|
||||
|
||||
Args:
|
||||
storage_or_path: A Storage instance or a path (opened "r+b").
|
||||
key: Master key (cipher.KEYBYTES bytes).
|
||||
cipher: A cipher module (from aeg.cipher()) or algorithm name.
|
||||
merkle: Enable Merkle tree (call merkle_rebuild() before use).
|
||||
merkle_max_chunks: Maximum number of chunks the Merkle tree can track.
|
||||
rng: Random source (see aeg.random). Default: a fresh aeg.random.Random
|
||||
seeded for the same cipher module passed as cipher=; any callable(n)
|
||||
returning bytes overrides.
|
||||
|
||||
Raises:
|
||||
TypeError: If the key length is invalid.
|
||||
ValueError: If the algorithm doesn't match cipher or authentication fails.
|
||||
FileNotFoundError: If the file does not exist.
|
||||
RuntimeError: If opening fails.
|
||||
"""
|
||||
cipher = _resolve_cipher(cipher)
|
||||
storage, owns = _as_storage(storage_or_path, "r+b")
|
||||
info = probe(storage)
|
||||
if info.alg_id != cipher.RAF_ALG_ID:
|
||||
raise ValueError(
|
||||
f"file uses algorithm id {info.alg_id}, "
|
||||
f"expected {cipher.RAF_ALG_ID} ({cipher.NAME})"
|
||||
)
|
||||
return Raf(
|
||||
storage,
|
||||
key,
|
||||
cipher,
|
||||
owns_storage=owns,
|
||||
creating=False,
|
||||
chunk_size=info.chunk_size,
|
||||
truncate=False,
|
||||
merkle=merkle,
|
||||
merkle_max_chunks=merkle_max_chunks,
|
||||
rng=rng,
|
||||
)
|
||||
|
||||
|
||||
class Raf:
|
||||
"""File-like random-access encrypted file. Use raf.create() / raf.open()."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
storage: Storage,
|
||||
key: Buffer,
|
||||
cipher: Cipher,
|
||||
*,
|
||||
owns_storage: bool,
|
||||
creating: bool,
|
||||
chunk_size: int,
|
||||
truncate: bool,
|
||||
merkle: bool | MerkleHasher,
|
||||
merkle_max_chunks: int,
|
||||
rng,
|
||||
):
|
||||
self._cipher = cipher
|
||||
self._storage = storage
|
||||
self._owns_storage = owns_storage
|
||||
self._closed = False
|
||||
self._position = 0
|
||||
|
||||
self._ctx = cipher.new_raf_ctx()
|
||||
self._io, self._storage_handle = _make_io(storage)
|
||||
if rng is None:
|
||||
rng = _random.Random(cipher)
|
||||
self._rng, self._rng_keepalive = _random.as_raf_rng(rng)
|
||||
self._raw_scratch, self._scratch = _allocate_scratch(cipher, chunk_size)
|
||||
|
||||
self._config = ffi.new("aegis_raf_config*")
|
||||
self._config.scratch = self._scratch
|
||||
self._config.chunk_size = chunk_size
|
||||
flags = 0
|
||||
if creating:
|
||||
flags |= _FLAG_CREATE
|
||||
if truncate:
|
||||
flags |= _FLAG_TRUNCATE
|
||||
self._config.flags = flags
|
||||
self._config.merkle = ffi.NULL
|
||||
|
||||
self._merkle_hasher = None
|
||||
self._merkle_cfg = None
|
||||
self._merkle_buf = None
|
||||
self._merkle_hasher_handle = None
|
||||
if merkle:
|
||||
hasher = SHA256MerkleHasher() if merkle is True else merkle
|
||||
if merkle_max_chunks <= 0:
|
||||
raise ValueError("merkle_max_chunks must be > 0")
|
||||
if not (_MERKLE_HASH_MIN <= hasher.hash_len <= _MERKLE_HASH_MAX):
|
||||
raise ValueError(
|
||||
f"hash_len must be between {_MERKLE_HASH_MIN} and "
|
||||
f"{_MERKLE_HASH_MAX}, got {hasher.hash_len}"
|
||||
)
|
||||
mcfg = ffi.new("aegis_raf_merkle_config*")
|
||||
mcfg.hash_len = hasher.hash_len
|
||||
mcfg.max_chunks = merkle_max_chunks
|
||||
buf_size = lib.aegis_raf_merkle_buffer_size(mcfg)
|
||||
if buf_size == 0 or buf_size >= int(ffi.cast("size_t", -1)):
|
||||
raise ValueError("Merkle tree buffer size overflow")
|
||||
self._merkle_buf = bytearray(buf_size)
|
||||
self._merkle_hasher_handle = ffi.new_handle(hasher)
|
||||
mcfg.hash_leaf = _hash_leaf_callback
|
||||
mcfg.hash_parent = _hash_parent_callback
|
||||
mcfg.hash_empty = _hash_empty_callback
|
||||
mcfg.hash_commitment = _hash_commitment_callback
|
||||
mcfg.user = self._merkle_hasher_handle
|
||||
mcfg.buf = ffi.from_buffer(self._merkle_buf)
|
||||
mcfg.len = buf_size
|
||||
self._merkle_hasher = hasher
|
||||
self._merkle_cfg = mcfg
|
||||
self._config.merkle = mcfg
|
||||
|
||||
if creating:
|
||||
cipher.raf_create(key, self._ctx, self._io, self._rng, self._config)
|
||||
else:
|
||||
cipher.raf_open(key, self._ctx, self._io, self._rng, self._config)
|
||||
|
||||
def _check_open(self) -> None:
|
||||
if self._closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
|
||||
def read(self, size: int = -1, offset: int | None = None) -> bytes:
|
||||
"""Read and decrypt bytes, advancing the current position.
|
||||
|
||||
Args:
|
||||
size: Number of bytes to read (-1 for all remaining).
|
||||
offset: Position to read from (None uses current position).
|
||||
|
||||
Returns:
|
||||
Decrypted bytes (fewer than requested at EOF).
|
||||
|
||||
Raises:
|
||||
ValueError: If authentication fails (corruption or tampering).
|
||||
RuntimeError: If the read fails.
|
||||
"""
|
||||
self._check_open()
|
||||
if offset is None:
|
||||
offset = self._position
|
||||
elif offset < 0:
|
||||
raise ValueError(f"offset must be non-negative, got {offset}")
|
||||
if size < 0:
|
||||
size = max(0, self.size - offset)
|
||||
if size == 0:
|
||||
return b""
|
||||
out = self._cipher.raf_read(self._ctx, size, offset)
|
||||
self._position = offset + len(out)
|
||||
return bytes(out)
|
||||
|
||||
def pread(self, size: int, offset: int) -> bytes:
|
||||
"""Read at offset without updating the position (like os.pread)."""
|
||||
self._check_open()
|
||||
if offset < 0:
|
||||
raise ValueError(f"offset must be non-negative, got {offset}")
|
||||
if size < 0:
|
||||
raise ValueError(f"size must be non-negative, got {size}")
|
||||
if size == 0:
|
||||
return b""
|
||||
return bytes(self._cipher.raf_read(self._ctx, size, offset))
|
||||
|
||||
def read_into(self, buf: Buffer, offset: int | None = None) -> int:
|
||||
"""Read and decrypt into buf, advancing the position.
|
||||
|
||||
Returns:
|
||||
Number of bytes actually read.
|
||||
"""
|
||||
self._check_open()
|
||||
buf = memoryview(buf)
|
||||
if offset is None:
|
||||
offset = self._position
|
||||
elif offset < 0:
|
||||
raise ValueError(f"offset must be non-negative, got {offset}")
|
||||
if buf.nbytes == 0:
|
||||
return 0
|
||||
out = self._cipher.raf_read(self._ctx, buf.nbytes, offset, into=buf)
|
||||
n = len(out)
|
||||
self._position = offset + n
|
||||
return n
|
||||
|
||||
def write(self, data: Buffer, offset: int | None = None) -> int:
|
||||
"""Encrypt and write bytes, advancing the current position.
|
||||
|
||||
Returns:
|
||||
Number of bytes written (always len(data) on success).
|
||||
|
||||
Raises:
|
||||
ValueError: If the write exceeds merkle_max_chunks.
|
||||
RuntimeError: If the write fails.
|
||||
"""
|
||||
self._check_open()
|
||||
data = memoryview(data)
|
||||
if offset is None:
|
||||
offset = self._position
|
||||
elif offset < 0:
|
||||
raise ValueError(f"offset must be non-negative, got {offset}")
|
||||
if data.nbytes == 0:
|
||||
return 0
|
||||
try:
|
||||
n = self._cipher.raf_write(self._ctx, data, offset)
|
||||
except RuntimeError as e:
|
||||
if "EOVERFLOW" in str(e) and self._merkle_cfg is not None:
|
||||
new_end = offset + data.nbytes
|
||||
if new_end <= 0xFFFFFFFFFFFFFFFF:
|
||||
chunk_size = self._config.chunk_size
|
||||
new_chunks = (new_end + chunk_size - 1) // chunk_size
|
||||
if new_chunks > self._merkle_cfg.max_chunks:
|
||||
raise ValueError(
|
||||
f"write exceeds merkle_max_chunks "
|
||||
f"({self._merkle_cfg.max_chunks}); "
|
||||
"open the file with a larger merkle_max_chunks"
|
||||
) from e
|
||||
raise
|
||||
self._position = offset + n
|
||||
return n
|
||||
|
||||
def pwrite(self, data: Buffer, offset: int) -> int:
|
||||
"""Write at offset without updating the position (like os.pwrite)."""
|
||||
pos = self._position
|
||||
try:
|
||||
return self.write(data, offset)
|
||||
finally:
|
||||
self._position = pos
|
||||
|
||||
def truncate(self, size: int | None = None) -> int:
|
||||
"""Resize the file (default: current position). Returns the new size."""
|
||||
self._check_open()
|
||||
if size is None:
|
||||
size = self._position
|
||||
self._cipher.raf_truncate(self._ctx, size)
|
||||
return size
|
||||
|
||||
def seek(self, offset: int, whence: int = 0) -> int:
|
||||
"""Move the file position (whence: 0=absolute, 1=relative, 2=from end)."""
|
||||
self._check_open()
|
||||
if whence == 0:
|
||||
new_pos = offset
|
||||
elif whence == 1:
|
||||
new_pos = self._position + offset
|
||||
elif whence == 2:
|
||||
new_pos = self.size + offset
|
||||
else:
|
||||
raise ValueError(f"invalid whence: {whence}")
|
||||
if new_pos < 0:
|
||||
raise ValueError(f"negative seek position: {new_pos}")
|
||||
self._position = new_pos
|
||||
return new_pos
|
||||
|
||||
def tell(self) -> int:
|
||||
"""Return the current position."""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
"""Logical plaintext file size."""
|
||||
self._check_open()
|
||||
return self._cipher.raf_size(self._ctx)
|
||||
|
||||
def sync(self) -> None:
|
||||
"""Flush writes to backing storage."""
|
||||
self._check_open()
|
||||
self._cipher.raf_sync(self._ctx)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the file, syncing and zeroizing key material."""
|
||||
if not self._closed:
|
||||
self._closed = True
|
||||
self._cipher.raf_close(self._ctx)
|
||||
if self._owns_storage and hasattr(self._storage, "close"):
|
||||
self._storage.close()
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
"""True if the file has been closed."""
|
||||
return self._closed
|
||||
|
||||
def __enter__(self) -> "Raf":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
self.close()
|
||||
|
||||
def _check_merkle_enabled(self) -> None:
|
||||
if self._merkle_cfg is None:
|
||||
raise ValueError("Merkle tree is not enabled on this file")
|
||||
|
||||
def merkle_rebuild(self) -> None:
|
||||
"""Rebuild the Merkle tree by reading and re-hashing every chunk.
|
||||
|
||||
Must be called after opening an existing file with merkle enabled.
|
||||
"""
|
||||
self._check_open()
|
||||
self._check_merkle_enabled()
|
||||
self._cipher.raf_merkle_rebuild(self._ctx)
|
||||
|
||||
def merkle_verify(self) -> int | None:
|
||||
"""Verify every chunk's hash against the Merkle tree.
|
||||
|
||||
Returns:
|
||||
None if all chunks match, otherwise the index of the first
|
||||
corrupted chunk.
|
||||
"""
|
||||
self._check_open()
|
||||
self._check_merkle_enabled()
|
||||
return self._cipher.raf_merkle_verify(self._ctx)
|
||||
|
||||
@property
|
||||
def root_hash(self) -> bytes | None:
|
||||
"""Current Merkle root commitment hash, or None if Merkle is not enabled."""
|
||||
self._check_open()
|
||||
if self._merkle_cfg is None:
|
||||
return None
|
||||
return self._cipher.raf_merkle_commitment(
|
||||
self._ctx, self._merkle_hasher.hash_len
|
||||
)
|
||||
|
||||
def verify_root(self, expected: Buffer) -> None:
|
||||
"""Rebuild the Merkle tree and verify the root matches expected.
|
||||
|
||||
Raises:
|
||||
ValueError: If the root mismatches or expected has the wrong length.
|
||||
"""
|
||||
self._check_open()
|
||||
self._check_merkle_enabled()
|
||||
expected = memoryview(expected)
|
||||
if expected.nbytes != self._merkle_hasher.hash_len:
|
||||
raise ValueError(
|
||||
f"expected must be {self._merkle_hasher.hash_len} bytes, "
|
||||
f"got {expected.nbytes}"
|
||||
)
|
||||
self.merkle_rebuild()
|
||||
if self.root_hash != bytes(expected):
|
||||
raise ValueError("authentication failed: merkle root mismatch")
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Pluggable random sources for aeg, built on AEGIS streams.
|
||||
|
||||
``Random`` is seeded once from the OS (secrets), then deterministic: each call
|
||||
produces AEGIS keystream under the current key/nonce and increments the nonce
|
||||
afterwards (little-endian), so no output block is ever produced twice.
|
||||
Modeled on quantumpipe's ChaCha20-based Random class.
|
||||
|
||||
Note: within the ``aeg`` package namespace this submodule shadows the stdlib
|
||||
``random`` module; import it as ``from aeg import random`` (or ``aeg.random``).
|
||||
"""
|
||||
|
||||
import errno
|
||||
import secrets
|
||||
from typing import Protocol
|
||||
|
||||
from ._loader import ffi
|
||||
from .util import Buffer, nonce_increment
|
||||
|
||||
__all__ = ["RandomSource", "Random", "system_random", "as_raf_rng"]
|
||||
|
||||
|
||||
class RandomSource(Protocol):
|
||||
"""Callable returning n cryptographically secure random bytes."""
|
||||
|
||||
def __call__(self, n: int) -> bytes: ...
|
||||
|
||||
|
||||
class Random:
|
||||
"""AEGIS stream RNG: seeded once from the OS, then deterministic.
|
||||
|
||||
Each call produces keystream under the current key/nonce and increments
|
||||
the nonce afterwards, so no output block is ever produced twice.
|
||||
|
||||
The primary way to create one is the cipher module's own random()
|
||||
helper, e.g. ``aeg.cipher("AEGIS-128X2").random()``.
|
||||
"""
|
||||
|
||||
def __init__(self, cipher, *, seed: Buffer | None = None):
|
||||
"""Create a Random using the given cipher module.
|
||||
|
||||
Args:
|
||||
cipher: Cipher module (from aeg.cipher()).
|
||||
seed: Optional nonce || key bytes (NONCEBYTES + KEYBYTES long).
|
||||
Primarily for tests; default seeds from the OS via secrets.
|
||||
|
||||
Raises:
|
||||
TypeError: If seed length is invalid.
|
||||
"""
|
||||
self._cipher = cipher
|
||||
if seed is None:
|
||||
seed = secrets.token_bytes(cipher.NONCEBYTES + cipher.KEYBYTES)
|
||||
seed = memoryview(seed)
|
||||
if seed.nbytes != cipher.NONCEBYTES + cipher.KEYBYTES:
|
||||
raise TypeError(
|
||||
f"seed length must be {cipher.NONCEBYTES + cipher.KEYBYTES} "
|
||||
f"(nonce || key) for {cipher.NAME}"
|
||||
)
|
||||
self._nonce = bytearray(seed[: cipher.NONCEBYTES])
|
||||
self._key = seed[cipher.NONCEBYTES :]
|
||||
|
||||
def bytes(self, n: int) -> bytearray:
|
||||
"""Return n random bytes as a bytearray."""
|
||||
out = self._cipher.stream(self._key, self._nonce, n)
|
||||
nonce_increment(self._nonce)
|
||||
return out
|
||||
|
||||
def into(self, buf: Buffer) -> None:
|
||||
"""Fill buf in place with random bytes."""
|
||||
mv = memoryview(buf).cast("B")
|
||||
self._cipher.stream(self._key, self._nonce, into=mv)
|
||||
nonce_increment(self._nonce)
|
||||
|
||||
def __call__(self, n: int) -> bytearray:
|
||||
"""Alias for bytes(n); satisfies the RandomSource protocol."""
|
||||
return self.bytes(n)
|
||||
|
||||
|
||||
def system_random(n: int) -> bytes:
|
||||
"""Random source backed directly by secrets.token_bytes."""
|
||||
return secrets.token_bytes(n)
|
||||
|
||||
|
||||
@ffi.callback("int(void*, uint8_t*, size_t)")
|
||||
def _random_callback(user, out, length):
|
||||
try:
|
||||
source = ffi.from_handle(user)
|
||||
data = source(length)
|
||||
if len(data) != length:
|
||||
ffi.errno = errno.EINVAL
|
||||
return -1
|
||||
ffi.memmove(out, data, length)
|
||||
return 0
|
||||
except Exception:
|
||||
ffi.errno = errno.EIO
|
||||
return -1
|
||||
|
||||
|
||||
def as_raf_rng(source: RandomSource):
|
||||
"""Build an aegis_raf_rng* cdata wrapping a Python random source.
|
||||
|
||||
Args:
|
||||
source: Callable returning n random bytes, e.g. a Random instance
|
||||
or system_random.
|
||||
|
||||
Returns:
|
||||
Tuple of (rng, keepalive). Keep ``keepalive`` alive for as long as
|
||||
the rng may be called by C code.
|
||||
"""
|
||||
handle = ffi.new_handle(source)
|
||||
rng = ffi.new("aegis_raf_rng*")
|
||||
rng.user = handle
|
||||
rng.random = _random_callback
|
||||
return rng, (handle, _random_callback)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,358 @@
|
||||
import pytest
|
||||
|
||||
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
||||
from aeg import raf
|
||||
from aeg import random as aeg_random
|
||||
|
||||
CIPHERS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
|
||||
|
||||
CHUNK = 1024
|
||||
|
||||
|
||||
def cipher_id(c):
|
||||
return c.NAME
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_roundtrip_bytesio(c):
|
||||
key = c.random_key()
|
||||
data = bytes(range(256)) * 20 # spans multiple chunks
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
assert f.write(data) == len(data)
|
||||
assert f.size == len(data)
|
||||
with raf.open(st, key, c) as f:
|
||||
assert f.read() == data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_roundtrip_filestorage(c, tmp_path):
|
||||
key = c.random_key()
|
||||
data = b"file-backed" * 500
|
||||
path = tmp_path / "test.raf"
|
||||
with raf.create(path, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(data)
|
||||
with raf.open(path, key, c) as f:
|
||||
assert f.read() == data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_random_access(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
data = bytearray(4097)
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(data)
|
||||
# unaligned writes across chunk boundaries
|
||||
f.pwrite(b"ABCDEF", 1000) # crosses chunk 0/1 boundary at 1024
|
||||
f.pwrite(b"z", 4096) # last byte
|
||||
data[1000:1006] = b"ABCDEF"
|
||||
data[4096:4097] = b"z"
|
||||
assert f.pread(6, 1000) == b"ABCDEF"
|
||||
assert f.pread(4097, 0) == bytes(data)
|
||||
assert f.tell() == len(data) # pwrite/pread don't move the position
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_seek_tell_read_into(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
data = b"0123456789" * 100
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(data)
|
||||
assert f.tell() == len(data)
|
||||
assert f.seek(10) == 10
|
||||
assert f.read(5) == data[10:15]
|
||||
assert f.tell() == 15
|
||||
assert f.seek(-5, 1) == 10
|
||||
assert f.seek(0, 2) == len(data)
|
||||
buf = bytearray(20)
|
||||
assert f.read_into(buf, 100) == 20
|
||||
assert bytes(buf) == data[100:120]
|
||||
with pytest.raises(ValueError):
|
||||
f.seek(-1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_truncate(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(b"x" * 3000)
|
||||
assert f.truncate(1500) == 1500
|
||||
assert f.size == 1500
|
||||
assert f.pread(1500, 0) == b"x" * 1500
|
||||
f.truncate(2500) # grow, zero-filled
|
||||
assert f.size == 2500
|
||||
assert f.pread(1000, 1500) == b"\x00" * 1000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_wrong_key(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(b"secret")
|
||||
with pytest.raises(ValueError, match="authentication failed"):
|
||||
raf.open(st, c.random_key(), c)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_tampered_chunk(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(b"y" * 2048)
|
||||
# flip a ciphertext byte inside the first chunk record (after the nonce)
|
||||
st._data[raf.HEADER_SIZE + c.NONCEBYTES + 10] ^= 1
|
||||
with raf.open(st, key, c) as f:
|
||||
with pytest.raises(ValueError, match="authentication failed"):
|
||||
f.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_probe_and_cipher_mismatch(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(b"data")
|
||||
info = raf.probe(st)
|
||||
assert info.alg_id == c.RAF_ALG_ID
|
||||
assert info.chunk_size == CHUNK
|
||||
assert info.file_size == 4
|
||||
other = aegis256 if c is not aegis256 else aegis128l
|
||||
with pytest.raises(ValueError, match="algorithm id"):
|
||||
raf.open(st, key, other)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_create_existing(c, tmp_path):
|
||||
key = c.random_key()
|
||||
path = tmp_path / "exists.raf"
|
||||
raf.create(path, key, c, chunk_size=CHUNK).close()
|
||||
with pytest.raises(FileExistsError):
|
||||
raf.create(path, key, c, chunk_size=CHUNK)
|
||||
with raf.create(path, key, c, chunk_size=CHUNK, truncate=True) as f:
|
||||
assert f.size == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_open_missing(c, tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
raf.open(tmp_path / "missing.raf", c.random_key(), c)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_merkle(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
data = b"m" * 5000
|
||||
with raf.create(st, key, c, chunk_size=CHUNK, merkle=True) as f:
|
||||
f.write(data)
|
||||
root1 = f.root_hash
|
||||
assert root1 is not None and len(root1) == 32
|
||||
assert f.merkle_verify() is None
|
||||
f.pwrite(b"X", 0)
|
||||
root2 = f.root_hash
|
||||
assert root1 != root2
|
||||
assert f.merkle_verify() is None
|
||||
with raf.open(st, key, c, merkle=True) as f:
|
||||
f.verify_root(root2) # rebuilds and compares
|
||||
assert f.root_hash == root2
|
||||
with pytest.raises(ValueError, match="authentication failed"):
|
||||
f.verify_root(root1)
|
||||
with pytest.raises(ValueError):
|
||||
f.verify_root(b"short")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_merkle_detects_corruption(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK, merkle=True) as f:
|
||||
f.write(b"z" * 3000)
|
||||
f.merkle_rebuild()
|
||||
record = c.NONCEBYTES + CHUNK + 16
|
||||
# corrupt chunk 2's ciphertext in the backing store
|
||||
st._data[raf.HEADER_SIZE + 2 * record + c.NONCEBYTES + 5] ^= 1
|
||||
assert f.merkle_verify() == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_merkle_overflow(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(
|
||||
st, key, c, chunk_size=CHUNK, merkle=True, merkle_max_chunks=2
|
||||
) as f:
|
||||
f.write(b"a" * 2048)
|
||||
with pytest.raises(ValueError, match="merkle_max_chunks"):
|
||||
f.write(b"b")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_merkle_disabled(c):
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
assert f.root_hash is None
|
||||
with pytest.raises(ValueError, match="not enabled"):
|
||||
f.merkle_verify()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_custom_rng(c):
|
||||
counter = 0
|
||||
|
||||
def deterministic(n: int) -> bytes:
|
||||
nonlocal counter
|
||||
out = bytes((counter + i) % 256 for i in range(n))
|
||||
counter += n
|
||||
return out
|
||||
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK, rng=deterministic) as f:
|
||||
f.write(b"rng test")
|
||||
with raf.open(st, key, c) as f:
|
||||
assert f.read() == b"rng test"
|
||||
assert counter > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_default_rng_matches_cipher(c):
|
||||
"""raf.create/open without rng use a Random seeded for the same cipher."""
|
||||
key = c.random_key()
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
assert f._rng_keepalive[0] # handle alive
|
||||
f.write(b"default rng")
|
||||
with raf.open(st, key, c) as f:
|
||||
assert f.read() == b"default rng"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_random_per_cipher(c):
|
||||
"""Random can be constructed with each cipher module."""
|
||||
r1 = aeg_random.Random(c)
|
||||
r2 = c.random()
|
||||
out = r1.bytes(100)
|
||||
assert len(out) == 100
|
||||
assert any(out) # not all zeros
|
||||
assert len(r2.bytes(50)) == 50
|
||||
# Random instance satisfies the RandomSource callable protocol
|
||||
assert callable(r1)
|
||||
rng_cdata, keepalive = aeg_random.as_raf_rng(r1)
|
||||
assert rng_cdata.random is not None and keepalive
|
||||
|
||||
|
||||
def test_random_determinism_with_seed():
|
||||
"""Two Random instances seeded identically produce identical output."""
|
||||
c = aegis256x4
|
||||
seed = bytes(range(c.NONCEBYTES + c.KEYBYTES))
|
||||
r1 = aeg_random.Random(c, seed=seed)
|
||||
r2 = aeg_random.Random(c, seed=seed)
|
||||
assert r1.bytes(64) == r2.bytes(64)
|
||||
assert r1.bytes(1000) == r2.bytes(1000)
|
||||
with pytest.raises(TypeError, match="seed length"):
|
||||
aeg_random.Random(c, seed=b"short")
|
||||
|
||||
|
||||
def test_random_into_equivalence():
|
||||
"""into() fills in place and matches bytes() from an identically-seeded RNG."""
|
||||
c = aegis256x4
|
||||
seed = bytes(c.NONCEBYTES + c.KEYBYTES)
|
||||
r1 = aeg_random.Random(c, seed=seed)
|
||||
r2 = aeg_random.Random(c, seed=seed)
|
||||
buf = bytearray(128)
|
||||
r1.into(buf)
|
||||
assert bytes(buf) == bytes(r2.bytes(128))
|
||||
# buffers of other buffer-protocol types work too
|
||||
import array
|
||||
|
||||
arr = array.array("b", bytes(32))
|
||||
r1.into(arr)
|
||||
assert any(arr.tobytes())
|
||||
|
||||
|
||||
def test_random_no_repeat():
|
||||
"""Successive calls never repeat a block (nonce increments)."""
|
||||
r = aeg_random.Random(aegis128x2)
|
||||
blocks = {bytes(r.bytes(64)) for _ in range(1000)}
|
||||
assert len(blocks) == 1000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id)
|
||||
def test_cipher_module_random_helper(c):
|
||||
"""Each cipher module provides a random() helper bound to itself."""
|
||||
r = c.random()
|
||||
assert isinstance(r, aeg_random.Random)
|
||||
assert r._cipher is c
|
||||
assert len(r.bytes(32)) == 32
|
||||
buf = bytearray(48)
|
||||
r.into(buf)
|
||||
assert any(buf)
|
||||
|
||||
|
||||
def test_random_module():
|
||||
assert len(aeg_random.system_random(16)) == 16
|
||||
# any callable source can be bridged to the C RNG struct
|
||||
rng, keepalive = aeg_random.as_raf_rng(aeg_random.system_random)
|
||||
assert rng.random is not None and keepalive
|
||||
|
||||
def bad(n: int) -> bytes:
|
||||
return b"too short"
|
||||
|
||||
rng2, _ = aeg_random.as_raf_rng(bad)
|
||||
out = aeg_random.ffi.new("uint8_t[8]")
|
||||
assert rng2.random(rng2.user, out, 8) == -1
|
||||
|
||||
|
||||
def test_derive_master_key():
|
||||
mk16 = bytes(range(16))
|
||||
mk32 = bytes(range(32))
|
||||
assert len(raf.derive_master_key(mk16)) == 16
|
||||
assert len(raf.derive_master_key(mk32)) == 32
|
||||
# context-bound: different contexts give different keys
|
||||
assert raf.derive_master_key(mk32, b"a") != raf.derive_master_key(mk32, b"b")
|
||||
# empty context still derives a scoped subkey
|
||||
assert raf.derive_master_key(mk32) != mk32
|
||||
with pytest.raises(ValueError):
|
||||
raf.derive_master_key(b"\x00" * 24)
|
||||
with pytest.raises(ValueError):
|
||||
raf.derive_master_key(mk16, b"c" * 121)
|
||||
with pytest.raises(ValueError):
|
||||
raf.derive_master_key(mk32, b"c" * 73)
|
||||
# derived keys work end-to-end
|
||||
c = aegis256x4
|
||||
key = raf.derive_master_key(mk32, b"my-file")
|
||||
st = raf.BytesIOStorage()
|
||||
with raf.create(st, key, c, chunk_size=CHUNK) as f:
|
||||
f.write(b"derived")
|
||||
with raf.open(st, key, c) as f:
|
||||
assert f.read() == b"derived"
|
||||
|
||||
|
||||
def test_invalid_chunk_size():
|
||||
c = aegis256x4
|
||||
with pytest.raises(ValueError, match="chunk_size"):
|
||||
raf.create(raf.BytesIOStorage(), c.random_key(), c, chunk_size=512)
|
||||
with pytest.raises(ValueError, match="multiple of 16"):
|
||||
raf.create(raf.BytesIOStorage(), c.random_key(), c, chunk_size=1032)
|
||||
|
||||
|
||||
def test_bad_key_length():
|
||||
c = aegis256x4
|
||||
with pytest.raises(TypeError, match="key length"):
|
||||
raf.create(raf.BytesIOStorage(), b"\x00" * 16, c, chunk_size=CHUNK)
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
c = aegis256x4
|
||||
st = raf.BytesIOStorage()
|
||||
f = raf.create(st, c.random_key(), c, chunk_size=CHUNK)
|
||||
f.close()
|
||||
assert f.closed
|
||||
with pytest.raises(ValueError, match="closed"):
|
||||
f.read()
|
||||
f.close() # idempotent
|
||||
File diff suppressed because it is too large.
Load diff
+23
-3
@@ -88,6 +88,7 @@ def generate_cdef(include_dir: pathlib.Path) -> str:
|
||||
"aegis256.h",
|
||||
"aegis256x2.h",
|
||||
"aegis256x4.h",
|
||||
"aegis_raf.h",
|
||||
]
|
||||
|
||||
for header_name in headers:
|
||||
@@ -105,9 +106,9 @@ def generate_cdef(include_dir: pathlib.Path) -> str:
|
||||
|
||||
|
||||
def extract_constants(
|
||||
common_h_path: pathlib.Path, header_path: pathlib.Path
|
||||
common_h_path: pathlib.Path, header_path: pathlib.Path, raf_h_path: pathlib.Path
|
||||
) -> Dict[str, int]:
|
||||
"""Extract constants from common.h (ALIGNMENT, RATE) and main header (KEYBYTES, NPUBBYTES, ABYTES_*)."""
|
||||
"""Extract constants from common.h (ALIGNMENT, RATE), main header (KEYBYTES, NPUBBYTES, ABYTES_*), and aegis_raf.h (RAF_ALG_ID)."""
|
||||
constants = {}
|
||||
|
||||
# Extract from common.h
|
||||
@@ -136,6 +137,14 @@ def extract_constants(
|
||||
raise ValueError(f"Could not extract {const_name} from {header_path}")
|
||||
constants[const_name] = int(match.group(1))
|
||||
|
||||
# Extract RAF algorithm id from aegis_raf.h
|
||||
raf_content = raf_h_path.read_text(encoding="utf-8")
|
||||
raf_pattern = rf"^\s*#define\s+AEGIS_RAF_ALG_{variant[5:].upper()}\s+(\d+)"
|
||||
raf_match = re.search(raf_pattern, raf_content, re.MULTILINE)
|
||||
if not raf_match:
|
||||
raise ValueError(f"Could not extract RAF_ALG_ID from {raf_h_path}")
|
||||
constants["RAF_ALG_ID"] = int(raf_match.group(1))
|
||||
|
||||
return constants
|
||||
|
||||
|
||||
@@ -155,6 +164,7 @@ def extract_all_constants(
|
||||
for variant in variants:
|
||||
common_h = libaegis_src_dir / variant / f"{variant}_common.h"
|
||||
header_h = include_dir / f"{variant}.h"
|
||||
raf_h = include_dir / "aegis_raf.h"
|
||||
|
||||
if not common_h.exists():
|
||||
print(f"Warning: {common_h} not found, skipping {variant}", file=sys.stderr)
|
||||
@@ -165,7 +175,7 @@ def extract_all_constants(
|
||||
continue
|
||||
|
||||
try:
|
||||
constants[variant] = extract_constants(common_h, header_h)
|
||||
constants[variant] = extract_constants(common_h, header_h, raf_h)
|
||||
except Exception as e:
|
||||
print(f"Error extracting constants from {variant}: {e}", file=sys.stderr)
|
||||
|
||||
@@ -215,6 +225,11 @@ def generate_variant(template_src: str, variant: str, constants: Dict[str, int])
|
||||
f"MACBYTES_LONG = {constants['ABYTES_MAX']}",
|
||||
s,
|
||||
)
|
||||
s = re.sub(
|
||||
r"RAF_ALG_ID = \d+",
|
||||
f"RAF_ALG_ID = {constants['RAF_ALG_ID']}",
|
||||
s,
|
||||
)
|
||||
|
||||
return s
|
||||
|
||||
@@ -262,6 +277,11 @@ def generate_python_modules(
|
||||
f"MACBYTES_LONG = {const_dict['ABYTES_MAX']}",
|
||||
new_content,
|
||||
)
|
||||
new_content = re.sub(
|
||||
r"RAF_ALG_ID = \d+",
|
||||
f"RAF_ALG_ID = {const_dict['RAF_ALG_ID']}",
|
||||
new_content,
|
||||
)
|
||||
else:
|
||||
new_content = generate_variant(template_src, variant, const_dict)
|
||||
|
||||
|
||||
Reference in new issue
Block a user