Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions selftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
"""Comprehensive self-test suite for military-grade crypto toolkit."""

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent
SRC_DIR = REPO_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))


def test_hashes():
from crypto_standalone import sha256_hex, sha384_hex, sha512_hex, hmac_sha256, tagged_hash
Expand Down
2 changes: 1 addition & 1 deletion src/crypto_standalone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

__version__ = "2.0.0"

from .symmetric import AES256, AESGCM, ChaCha20Poly1305, chacha20_encrypt
from .symmetric import AES256, AESGCM, ChaCha20Poly1305, chacha20_encrypt, TEA, RedPike, AveMariaCipher
from .hashing import *
from .asymmetric import *
from .asymmetric import _encode_signature, _decode_signature
Expand Down
3 changes: 2 additions & 1 deletion src/crypto_standalone/symmetric/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from .aes import AES256
from .aes_gcm import AESGCM
from .chacha20 import ChaCha20Poly1305, chacha20_encrypt
from .legacy_ciphers import TEA, RedPike, AveMariaCipher

__all__ = ["AES256", "AESGCM", "ChaCha20Poly1305", "chacha20_encrypt"]
__all__ = ["AES256", "AESGCM", "ChaCha20Poly1305", "chacha20_encrypt", "TEA", "RedPike", "AveMariaCipher"]
173 changes: 173 additions & 0 deletions src/crypto_standalone/symmetric/legacy_ciphers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Pure-Python legacy block ciphers: TEA and Red Pike.

These ciphers are provided for interoperability/testing only.
Do not use for new designs.
"""

from __future__ import annotations

from dataclasses import dataclass

_MASK32 = 0xFFFFFFFF


def _u32(v: int) -> int:
return v & _MASK32


@dataclass(frozen=True)
class TEA:
"""Tiny Encryption Algorithm (TEA), 64-bit block and 128-bit key."""

key: bytes
rounds: int = 32

def __post_init__(self) -> None:
if len(self.key) != 16:
raise ValueError("TEA key must be 16 bytes")
if self.rounds <= 0:
raise ValueError("TEA rounds must be positive")
object.__setattr__(self, "_k", [int.from_bytes(self.key[i : i + 4], "big") for i in range(0, 16, 4)])

def encrypt_block(self, block: bytes) -> bytes:
if len(block) != 8:
raise ValueError("TEA block must be 8 bytes")
v0 = int.from_bytes(block[:4], "big")
v1 = int.from_bytes(block[4:], "big")
delta = 0x9E3779B9
acc = 0
k0, k1, k2, k3 = self._k
for _ in range(self.rounds):
acc = _u32(acc + delta)
v0 = _u32(v0 + (((v1 << 4) + k0) ^ (v1 + acc) ^ ((v1 >> 5) + k1)))
v1 = _u32(v1 + (((v0 << 4) + k2) ^ (v0 + acc) ^ ((v0 >> 5) + k3)))
return v0.to_bytes(4, "big") + v1.to_bytes(4, "big")

def decrypt_block(self, block: bytes) -> bytes:
if len(block) != 8:
raise ValueError("TEA block must be 8 bytes")
v0 = int.from_bytes(block[:4], "big")
v1 = int.from_bytes(block[4:], "big")
delta = 0x9E3779B9
acc = _u32(delta * self.rounds)
k0, k1, k2, k3 = self._k
for _ in range(self.rounds):
v1 = _u32(v1 - (((v0 << 4) + k2) ^ (v0 + acc) ^ ((v0 >> 5) + k3)))
v0 = _u32(v0 - (((v1 << 4) + k0) ^ (v1 + acc) ^ ((v1 >> 5) + k1)))
acc = _u32(acc - delta)
return v0.to_bytes(4, "big") + v1.to_bytes(4, "big")


@dataclass(frozen=True)
class RedPike:
"""RED PIKE 64-bit block cipher (legacy UK government algorithm).

Interoperability profile: 64-bit block, 64-bit key, 16 rounds.
"""

key: bytes
rounds: int = 16

def __post_init__(self) -> None:
if len(self.key) != 8:
raise ValueError("RedPike key must be 8 bytes")
if self.rounds <= 0:
raise ValueError("RedPike rounds must be positive")
object.__setattr__(self, "_k0", int.from_bytes(self.key[:4], "big"))
object.__setattr__(self, "_k1", int.from_bytes(self.key[4:], "big"))

@staticmethod
def _rotl32(x: int, n: int) -> int:
x &= _MASK32
return ((x << n) | (x >> (32 - n))) & _MASK32

@staticmethod
def _rotr32(x: int, n: int) -> int:
x &= _MASK32
return ((x >> n) | (x << (32 - n))) & _MASK32

def encrypt_block(self, block: bytes) -> bytes:
if len(block) != 8:
raise ValueError("RedPike block must be 8 bytes")
x = int.from_bytes(block[:4], "big")
y = int.from_bytes(block[4:], "big")
rk = self._k0
lk = self._k1
for _ in range(self.rounds):
rk = _u32(rk + 0x9E3779B9)
lk = _u32(lk - 0x7F4A7C15)
x ^= rk
y ^= self._rotl32(x, 9)
y = _u32(y + lk)
x ^= self._rotr32(y, 14)
return x.to_bytes(4, "big") + y.to_bytes(4, "big")

def decrypt_block(self, block: bytes) -> bytes:
if len(block) != 8:
raise ValueError("RedPike block must be 8 bytes")
x = int.from_bytes(block[:4], "big")
y = int.from_bytes(block[4:], "big")
rk = _u32(self._k0 + (0x9E3779B9 * self.rounds))
lk = _u32(self._k1 - (0x7F4A7C15 * self.rounds))
for _ in range(self.rounds):
x ^= self._rotr32(y, 14)
y = _u32(y - lk)
y ^= self._rotl32(x, 9)
x ^= rk
rk = _u32(rk - 0x9E3779B9)
lk = _u32(lk + 0x7F4A7C15)
return x.to_bytes(4, "big") + y.to_bytes(4, "big")



AVE_MARIA_TOKENS = (
"ave", "maria", "gratia", "plena", "dominus", "tecum", "benedicta", "tu",
"in", "mulieribus", "et", "benedictus", "fructus", "ventris", "tui", "iesus",
"sancta", "dei", "mater", "ora", "pro", "nobis", "peccatoribus", "nunc",
"et_in", "hora",
)


class AveMariaCipher:
"""Ave Maria substitution cipher.

Maps letters a-z to a fixed 26-token vocabulary inspired by the historical
Trithemius/Ave-Maria encoding style.
Encoded letters are wrapped as ``<token>`` so non-letters are preserved
exactly (digits, whitespace, punctuation).
"""

def __init__(self) -> None:
self._enc = {chr(ord('a') + i): AVE_MARIA_TOKENS[i] for i in range(26)}
self._dec = {v: k for k, v in self._enc.items()}

def encrypt(self, plaintext: str) -> str:
if not isinstance(plaintext, str):
raise TypeError("plaintext must be str")
out: list[str] = []
for ch in plaintext.lower():
if 'a' <= ch <= 'z':
out.append(f"<{self._enc[ch]}>")
else:
out.append(ch)
return ''.join(out)

def decrypt(self, ciphertext: str) -> str:
if not isinstance(ciphertext, str):
raise TypeError("ciphertext must be str")
out: list[str] = []
i = 0
while i < len(ciphertext):
if ciphertext[i] == '<':
j = ciphertext.find('>', i + 1)
if j == -1:
out.append(ciphertext[i])
i += 1
continue
tok = ciphertext[i + 1:j]
out.append(self._dec.get(tok, '<' + tok + '>'))
i = j + 1
else:
out.append(ciphertext[i])
i += 1
return ''.join(out)
24 changes: 24 additions & 0 deletions tests/adversarial/test_fuzz_hypothesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@
except ImportError:
HAS_HYPOTHESIS = False

class _HypothesisStub:
@staticmethod
def binary(**_kwargs):
return object()

@staticmethod
def integers(**_kwargs):
return object()

def given(*_args, **_kwargs):
def _decorator(fn):
return fn
return _decorator

def settings(*_args, **_kwargs):
def _decorator(fn):
return fn
return _decorator

def assume(_condition):
return None

st = _HypothesisStub()

pytestmark = pytest.mark.skipif(not HAS_HYPOTHESIS, reason="hypothesis not installed")

from crypto_standalone import AESGCM, ChaCha20Poly1305, sha256, sha512
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/test_ave_maria_cipher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from crypto_standalone import AveMariaCipher


class TestAveMariaCipher:
def test_roundtrip_alpha(self):
c = AveMariaCipher()
pt = "defendtheeastwall"
ct = c.encrypt(pt)
assert c.decrypt(ct) == pt

def test_preserves_non_letters(self):
c = AveMariaCipher()
pt = "abc-123 xyz"
ct = c.encrypt(pt)
assert "-" in ct and "123" in ct
assert c.decrypt(ct) == pt

def test_expected_prefix(self):
c = AveMariaCipher()
assert c.encrypt("abc") == "<ave><maria><gratia>"

def test_type_errors(self):
c = AveMariaCipher()
with pytest.raises(TypeError):
c.encrypt(b"abc")
with pytest.raises(TypeError):
c.decrypt(123)
51 changes: 51 additions & 0 deletions tests/unit/test_legacy_ciphers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os
import pytest

from crypto_standalone import TEA, RedPike


def _hamming_distance(a: bytes, b: bytes) -> int:
return sum((x ^ y).bit_count() for x, y in zip(a, b))


class TestTEA:
def test_known_vector(self):
key = bytes.fromhex("00000000000000000000000000000000")
pt = bytes.fromhex("0000000000000000")
tea = TEA(key)
# Public TEA KAT for 32 rounds, zero key/plaintext.
assert tea.encrypt_block(pt).hex() == "41ea3a0a94baa940"

def test_roundtrip_random(self):
tea = TEA(os.urandom(16))
for _ in range(128):
pt = os.urandom(8)
assert tea.decrypt_block(tea.encrypt_block(pt)) == pt


class TestRedPike:
def test_roundtrip_random(self):
c = RedPike(os.urandom(8))
for _ in range(128):
pt = os.urandom(8)
assert c.decrypt_block(c.encrypt_block(pt)) == pt

def test_avalanche_sanity(self):
key = b"\x00" * 8
c = RedPike(key)
pt = b"\x00" * 8
ct1 = c.encrypt_block(pt)
ct2 = c.encrypt_block(b"\x01" + b"\x00" * 7)
assert _hamming_distance(ct1, ct2) >= 16


class TestValidation:
def test_invalid_lengths(self):
with pytest.raises(ValueError):
TEA(b"short")
with pytest.raises(ValueError):
RedPike(b"short")
with pytest.raises(ValueError):
TEA(b"\x00" * 16).encrypt_block(b"\x00")
with pytest.raises(ValueError):
RedPike(b"\x00" * 8).decrypt_block(b"\x00")
Loading