Compare commits
7
Commits
f7f3a450c9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ae37a03d | ||
|
|
e29035f768 | ||
|
|
3ff145a0f0 | ||
|
|
503ce8e491 | ||
|
|
59dd4d922f | ||
|
|
9583a1f9a8 | ||
|
|
c3c4d66ea8 |
No files matched your search
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -8,5 +8,6 @@
|
||||
/src/aeg/_aegis*.pyd
|
||||
__pycache__
|
||||
!.gitignore
|
||||
!.gitattributes
|
||||
!.gitea/
|
||||
!.gitmodules
|
||||
@@ -123,12 +123,12 @@ Constants (per module): NAME, KEYBYTES, NONCEBYTES, MACBYTES, MACBYTES_LONG, RAT
|
||||
- nonce_increment(nonce)
|
||||
- wipe(buffer)
|
||||
|
||||
### CSPRNG (aeg.random)
|
||||
### Random Generator (CSPRNG)
|
||||
|
||||
Seeded once from the OS, then a deterministic AEGIS keystream: each call increments the nonce, so no output block ever repeats. random_key() and random_nonce() draw from it.
|
||||
Seeded once from the OS, then a cryptographically secure AEGIS keystream with thread local key/nonce making it thread safe. `random_key()` and `random_nonce()` draw from it. The into variant can directly fill Numpy integer arrays and other structures that support Buffer API, making it far faster than any alternatives (e.g. Numpy's own random module).
|
||||
|
||||
- ciph.random() -> per-thread aeg.random.Random singleton for the cipher module
|
||||
- rng.bytes(n) -> bytearray, rng.into(buf), rng(n)
|
||||
- random().bytes(n) -> bytearray
|
||||
- random().into(buffer) -> None
|
||||
|
||||
### Random-access encrypted files (aeg.raf)
|
||||
|
||||
|
||||
@@ -9,8 +9,15 @@ from setuptools import setup
|
||||
|
||||
ROOT = Path(__file__).parent.resolve()
|
||||
|
||||
libaegis_static = ROOT / "libaegis/zig-out/lib" / (
|
||||
"aegis.lib" if sys.platform == "win32" else "libaegis.a"
|
||||
if sys.maxsize <= 2**32:
|
||||
raise RuntimeError(
|
||||
"aeg does not support 32-bit platforms; build with a 64-bit Python"
|
||||
)
|
||||
|
||||
libaegis_static = (
|
||||
ROOT
|
||||
/ "libaegis/zig-out/lib"
|
||||
/ ("aegis.lib" if sys.platform == "win32" else "libaegis.a")
|
||||
)
|
||||
|
||||
ffibuilder = FFI()
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
/* This file is generated with tools/generate.py. Do not edit. */
|
||||
|
||||
typedef unsigned char uint8_t;
|
||||
typedef unsigned long size_t;
|
||||
|
||||
/* aegis.h */
|
||||
int aegis_init(void);
|
||||
int aegis_verify_16(const uint8_t *x, const uint8_t *y);
|
||||
|
||||
@@ -104,6 +104,9 @@ class FileStorage:
|
||||
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||
else:
|
||||
raise ValueError(f"Unsupported mode: {mode}")
|
||||
# O_BINARY (Windows only): keep the CRT from translating \r\n in
|
||||
# binary data, which corrupts ciphertext and shifts offsets
|
||||
flags |= getattr(os, "O_BINARY", 0)
|
||||
self._fd = os.open(path, flags, 0o644)
|
||||
self._closed = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
+72
-29
@@ -73,13 +73,49 @@ def format_declaration(decl: str, max_width: int = 100) -> str:
|
||||
return decl
|
||||
|
||||
|
||||
def _ruff_format_source(content: str, filename: str) -> str:
|
||||
"""Format Python source in memory (ruff format via stdin, no file writes)."""
|
||||
result = subprocess.run(
|
||||
["uv", "run", "ruff", "format", "--stdin-filename", filename, "-"],
|
||||
input=content,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def _is_up_to_date(path: pathlib.Path, content: str, check: bool) -> bool:
|
||||
"""True if writing content to path would leave it unchanged. In check mode,
|
||||
Python content is compared in its ruff-formatted form, matching what a
|
||||
write + format pass would produce."""
|
||||
if not path.exists():
|
||||
return False
|
||||
if check and path.suffix == ".py":
|
||||
content = _ruff_format_source(content, path.name)
|
||||
return path.read_text(encoding="utf-8") == content
|
||||
|
||||
|
||||
def _sync_file(
|
||||
path: pathlib.Path, content: str, check: bool, stale: list[pathlib.Path]
|
||||
) -> None:
|
||||
"""Write content to path, or in check mode record it as stale instead."""
|
||||
if _is_up_to_date(path, content, check):
|
||||
print(f" - No changes to {path.name}", file=sys.stderr)
|
||||
elif check:
|
||||
stale.append(path)
|
||||
else:
|
||||
path.write_bytes(content.encode())
|
||||
print(f" - Updated {path}", file=sys.stderr)
|
||||
|
||||
|
||||
def generate_cdef(include_dir: pathlib.Path) -> str:
|
||||
# cffi pre-defines uint8_t, size_t, etc. with the correct platform sizes.
|
||||
# Do not typedef them here: "unsigned long" matches size_t on LP64 but is
|
||||
# 4 bytes vs 8 on Win64 (LLP64), breaking struct layout verification.
|
||||
lines = [
|
||||
"/* This file is generated with tools/generate.py. Do not edit. */",
|
||||
"",
|
||||
"typedef unsigned char uint8_t;",
|
||||
"typedef unsigned long size_t;",
|
||||
"",
|
||||
]
|
||||
|
||||
headers = [
|
||||
@@ -240,6 +276,7 @@ def generate_python_modules(
|
||||
template_path: pathlib.Path,
|
||||
output_dir: pathlib.Path,
|
||||
constants: dict[str, dict[str, int]],
|
||||
check: bool = False,
|
||||
) -> tuple[list[pathlib.Path], list[pathlib.Path]]:
|
||||
if not template_path.exists():
|
||||
raise FileNotFoundError(f"Template not found: {template_path}")
|
||||
@@ -287,10 +324,11 @@ def generate_python_modules(
|
||||
else:
|
||||
new_content = generate_variant(template_src, variant, const_dict)
|
||||
|
||||
if dst.exists() and dst.read_text(encoding="utf-8") == new_content:
|
||||
if _is_up_to_date(dst, new_content, check):
|
||||
unchanged.append(dst)
|
||||
else:
|
||||
dst.write_bytes(new_content.encode())
|
||||
if not check:
|
||||
dst.write_bytes(new_content.encode())
|
||||
updated.append(dst)
|
||||
|
||||
return updated, unchanged
|
||||
@@ -313,7 +351,9 @@ def generate_ciphers_module(constants: dict[str, dict[str, int]]) -> str:
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
def main(check: bool = False) -> int:
|
||||
"""Regenerate files from the C sources. With check=True, only verify the
|
||||
committed files are up to date; nothing is written."""
|
||||
root = pathlib.Path(__file__).parent.parent
|
||||
libaegis_src_dir = root / "libaegis" / "src"
|
||||
include_dir = libaegis_src_dir / "include"
|
||||
@@ -333,36 +373,26 @@ def main() -> int:
|
||||
print("Error: No constants extracted", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("Step 2: Generating CFFI cdef header...", file=sys.stderr)
|
||||
pyaegis_dir.mkdir(exist_ok=True)
|
||||
cdef_path = pyaegis_dir / "aegis_cdef.h"
|
||||
cdef_content = generate_cdef(include_dir)
|
||||
stale: list[pathlib.Path] = []
|
||||
|
||||
if cdef_path.exists() and cdef_path.read_text(encoding="utf-8") == cdef_content:
|
||||
print(f" - No changes to {cdef_path}", file=sys.stderr)
|
||||
else:
|
||||
cdef_path.write_bytes(cdef_content.encode())
|
||||
print(f" - Updated {cdef_path}", file=sys.stderr)
|
||||
print("Step 2: Generating CFFI cdef header...", file=sys.stderr)
|
||||
if not check:
|
||||
pyaegis_dir.mkdir(exist_ok=True)
|
||||
_sync_file(pyaegis_dir / "aegis_cdef.h", generate_cdef(include_dir), check, stale)
|
||||
|
||||
print("Step 3: Generating _ciphers.py...", file=sys.stderr)
|
||||
ciphers_path = pyaegis_dir / "_ciphers.py"
|
||||
ciphers_content = generate_ciphers_module(constants)
|
||||
|
||||
if (
|
||||
ciphers_path.exists()
|
||||
and ciphers_path.read_text(encoding="utf-8") == ciphers_content
|
||||
):
|
||||
print(f" - No changes to {ciphers_path.name}", file=sys.stderr)
|
||||
else:
|
||||
ciphers_path.write_bytes(ciphers_content.encode())
|
||||
print(f" - Updated {ciphers_path.name}", file=sys.stderr)
|
||||
_sync_file(
|
||||
pyaegis_dir / "_ciphers.py", generate_ciphers_module(constants), check, stale
|
||||
)
|
||||
|
||||
print("Step 4: Generating Python modules...", file=sys.stderr)
|
||||
try:
|
||||
updated, unchanged = generate_python_modules(
|
||||
pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants
|
||||
pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants, check=check
|
||||
)
|
||||
if updated:
|
||||
if check:
|
||||
stale.extend(updated)
|
||||
else:
|
||||
for p in updated:
|
||||
print(f" - {p.relative_to(root)}", file=sys.stderr)
|
||||
if unchanged:
|
||||
@@ -377,6 +407,19 @@ def main() -> int:
|
||||
print(f"Error generating Python modules: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if check:
|
||||
for p in stale:
|
||||
print(f" - Out of date: {p.relative_to(root)}", file=sys.stderr)
|
||||
if stale:
|
||||
print(
|
||||
"✗ Generated files are out of date; "
|
||||
"run tools/generate.py and commit the result",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
print("All generated files are up to date.", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
print("Step 5: Formatting generated files with ruff...", file=sys.stderr)
|
||||
result = subprocess.run(
|
||||
["uv", "run", "ruff", "format", str(pyaegis_dir)],
|
||||
@@ -392,4 +435,4 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(main(check="--check" in sys.argv))
|
||||
+76
-33
@@ -129,27 +129,29 @@ def get_build_env():
|
||||
return env
|
||||
|
||||
|
||||
def normalize_line_endings(repo_root: Path):
|
||||
"""Normalize all text files to LF line endings."""
|
||||
# Patterns for files to normalize
|
||||
patterns = [
|
||||
"src/aeg/**/*.py",
|
||||
"src/aeg/**/*.h",
|
||||
"tests/**/*.py",
|
||||
"tools/**/*.py",
|
||||
"*.py",
|
||||
"*.md",
|
||||
"*.txt",
|
||||
"*.toml",
|
||||
"*.in",
|
||||
]
|
||||
for pattern in patterns:
|
||||
# Text files that must have LF line endings (.gitattributes enforces this on
|
||||
# checkout; release validation fails instead of rewriting)
|
||||
LINE_ENDING_PATTERNS = [
|
||||
"src/aeg/**/*.py",
|
||||
"src/aeg/**/*.h",
|
||||
"tests/**/*.py",
|
||||
"tools/**/*.py",
|
||||
"*.py",
|
||||
"*.md",
|
||||
"*.txt",
|
||||
"*.toml",
|
||||
"*.in",
|
||||
]
|
||||
|
||||
|
||||
def find_crlf_files(repo_root: Path) -> list[Path]:
|
||||
"""Return text files containing CRLF line endings."""
|
||||
bad = []
|
||||
for pattern in LINE_ENDING_PATTERNS:
|
||||
for file_path in repo_root.glob(pattern):
|
||||
if file_path.is_file():
|
||||
content = file_path.read_bytes()
|
||||
if b"\r\n" in content:
|
||||
content = content.replace(b"\r\n", b"\n")
|
||||
file_path.write_bytes(content)
|
||||
if file_path.is_file() and b"\r\n" in file_path.read_bytes():
|
||||
bad.append(file_path)
|
||||
return bad
|
||||
|
||||
|
||||
def get_wheel_pattern(py_version: str, abi3: bool = False) -> str:
|
||||
@@ -305,6 +307,11 @@ def run_benchmark(wheel: Path, py_version: str) -> bool:
|
||||
|
||||
def main():
|
||||
"""Build wheels for all supported Python versions."""
|
||||
# Windows CI consoles default to cp1252; emit ✓/✗/⚠ as UTF-8
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(stream, "reconfigure"):
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
# CI matrix jobs build per-platform wheels but never upload; a separate
|
||||
# publish job uploads everything once all platforms have succeeded.
|
||||
no_upload = "--no-upload" in sys.argv
|
||||
@@ -312,29 +319,38 @@ def main():
|
||||
repo_root = Path(__file__).parent.parent
|
||||
dist_dir = repo_root / "dist"
|
||||
|
||||
# Generate CFFI definitions and Python modules
|
||||
# The repository must already contain up-to-date generated code and
|
||||
# lint-clean sources: a release build never writes to tracked files.
|
||||
# Line endings must be LF (.gitattributes enforces this on checkout).
|
||||
print(f"\n{'=' * 70}")
|
||||
print("Code generation from C headers (tools/generate.py)")
|
||||
print("Validating generated files (tools/generate.py --check)")
|
||||
print(f"{'=' * 70}")
|
||||
if generate.main() != 0:
|
||||
print("✗ Code generation failed", file=sys.stderr)
|
||||
if generate.main(check=True) != 0:
|
||||
return 1
|
||||
|
||||
# Run ruff to check and fix any issues
|
||||
print(f"\n{'=' * 70}")
|
||||
print("Linting and formatting")
|
||||
print("Lint and format validation")
|
||||
print(f"{'=' * 70}")
|
||||
if not run_command(["uv", "run", "ruff", "check", "--fix", "."]):
|
||||
print("✗ Ruff check failed", file=sys.stderr)
|
||||
if not run_command(["uv", "run", "ruff", "check", "."]):
|
||||
print("✗ Ruff check failed; fix and commit before releasing", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Run ruff format
|
||||
if not run_command(["uv", "run", "ruff", "format", "."]):
|
||||
print("✗ Ruff format failed", file=sys.stderr)
|
||||
if not run_command(["uv", "run", "ruff", "format", "--check", "."]):
|
||||
print(
|
||||
"✗ Ruff format check failed; run 'uv run ruff format .' and commit",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Normalize all line endings to LF (important for consistent builds)
|
||||
normalize_line_endings(repo_root)
|
||||
if crlf_files := find_crlf_files(repo_root):
|
||||
print("✗ CRLF line endings found in:", file=sys.stderr)
|
||||
for p in crlf_files:
|
||||
print(f" - {p.relative_to(repo_root)}", file=sys.stderr)
|
||||
print(
|
||||
"Convert to LF and commit (.gitattributes enforces eol=lf).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Get version from git repo
|
||||
version = get_version_from_scm()
|
||||
@@ -342,6 +358,33 @@ def main():
|
||||
return 1
|
||||
is_release = is_release_version(version)
|
||||
|
||||
# On a tagged commit the version must be a clean release (e.g. 0.10.6).
|
||||
# A dev/dirty version means the checkout itself is dirty (PyPI rejects
|
||||
# such versions), so fail before building.
|
||||
head_tags = subprocess.run(
|
||||
["git", "tag", "--points-at", "HEAD", "--list", "v*"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
).stdout.split()
|
||||
if head_tags and not is_release:
|
||||
print(
|
||||
f"✗ HEAD is tagged ({', '.join(head_tags)}) but the version resolved "
|
||||
f"to {version}, not a clean release.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
dirty = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
).stdout
|
||||
if dirty.strip():
|
||||
print("Working copy is dirty:", file=sys.stderr)
|
||||
print(dirty, file=sys.stderr, end="")
|
||||
print("Commit the changes, re-tag, and run again.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Main header for the packaging process
|
||||
print(f"\n{'=' * 70}")
|
||||
print(
|
||||
|
||||
Reference in new issue
Block a user