diff --git a/src/python/botan3.py b/src/python/botan3.py index df5e1bb1cb4..a76e12ce47a 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -28,8 +28,9 @@ # This Python module requires the FFI API version introduced in Botan 3.10.0 # +# 3.11.0 - XOF API # 3.10.0 - introduced botan_pubkey_load_ec*_sec1() -BOTAN_FFI_VERSION = 20250829 +BOTAN_FFI_VERSION = 20260203 # # Base exception for all exceptions raised from this module @@ -163,6 +164,17 @@ def ffi_api(fn, args, allowed_errors=None): ffi_api(dll.botan_hash_destroy, [c_void_p]) ffi_api(dll.botan_hash_name, [c_void_p, c_char_p, POINTER(c_size_t)]) + # XOF + ffi_api(dll.botan_xof_init, [c_void_p, c_char_p, c_uint32]) + ffi_api(dll.botan_xof_copy_state, [c_void_p, c_void_p]) + ffi_api(dll.botan_xof_block_size, [c_void_p, POINTER(c_size_t)]) + ffi_api(dll.botan_xof_name, [c_void_p, c_char_p, POINTER(c_size_t)]) + ffi_api(dll.botan_xof_accepts_input, [c_void_p]) + ffi_api(dll.botan_xof_clear, [c_void_p]) + ffi_api(dll.botan_xof_update, [c_void_p, c_char_p, c_size_t]) + ffi_api(dll.botan_xof_output, [c_void_p, c_char_p, c_size_t]) + ffi_api(dll.botan_xof_destroy, [c_void_p]) + # MAC ffi_api(dll.botan_mac_init, [c_void_p, c_char_p, c_uint32]) ffi_api(dll.botan_mac_output_length, [c_void_p, POINTER(c_size_t)]) @@ -586,6 +598,10 @@ def _call_fn_returning_sz(fn) -> int: fn(byref(sz)) return int(sz.value) +def _call_fn_returning_bool(fn) -> bool: + res = fn() + return res > 0 + def _call_fn_returning_vec(guess, fn) -> bytes: buf = create_string_buffer(guess) @@ -983,6 +999,57 @@ def final(self) -> bytes: _DLL.botan_hash_final(self.__obj, out) return _ctype_bufout(out) +# +# eXtensible Output Functions +# +class XOF: + """eXtensible Output Function (XOF). The ``algo`` param is a string (e.g 'SHAKE-256', 'Ascon-XOF128')""" + + def __init__(self, algo: str | c_void_p): + if isinstance(algo, c_void_p): + self.__obj = algo + else: + flags = c_uint32(0) # always zero in this API version + self.__obj = c_void_p(0) + _DLL.botan_xof_init(byref(self.__obj), _ctype_str(algo), flags) + + def __del__(self): + _DLL.botan_xof_destroy(self.__obj) + + def copy_state(self) -> XOF: + copy = c_void_p(0) + _DLL.botan_xof_copy_state(byref(copy), self.__obj) + return XOF(copy) + + def clear(self): + """Clear state""" + _DLL.botan_xof_clear(self.__obj) + + def update(self, x: str | bytes): + """Add some input""" + bits = _ctype_bits(x) + _DLL.botan_xof_update(self.__obj, bits, len(bits)) + + def output(self, length: int) -> bytes: + """Returns `length` bytes of output from the XOF after all input was provided""" + if length <= 0: + return b'' + buf = create_string_buffer(length) + _DLL.botan_xof_output(self.__obj, buf, c_size_t(length)) + return _ctype_bufout(buf) + + def accepts_input(self) -> bool: + """Returns True if the XOF can accept more input, False if it is in output-only mode.""" + return _call_fn_returning_bool(lambda: _DLL.botan_xof_accepts_input(self.__obj)) + + def algo_name(self) -> str: + """Returns the name of this algorithm""" + return _call_fn_returning_str(32, lambda b, bl: _DLL.botan_xof_name(self.__obj, b, bl)) + + def block_size(self) -> int: + """Return block size in bytes""" + return _call_fn_returning_sz(lambda length: _DLL.botan_xof_block_size(self.__obj, length)) + # # Message authentication codes # diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index dcb6a070bca..5a67f927cf0 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -293,6 +293,37 @@ def test_hash(self): self.assertEqual(hex_encode(sha256.final()), "08bfce15fd2406114825ee6f770a06b1b00c129cb48fcddc54ef58b5de48bdf5") + def test_xof(self): + try: + _h = botan.XOF('NoSuchXof') + except botan.BotanException as e: + self.assertEqual(str(e), "botan_xof_init failed: -40 (Not implemented)") + + shake128 = botan.XOF('SHAKE-128') + self.assertEqual(shake128.algo_name(), 'SHAKE-128') + self.assertEqual(shake128.block_size(), 168) + self.assertTrue(shake128.accepts_input()) + + shake128.update('ignore this please') + shake128.clear() + shake128.update(hex_decode("32a36452a646beba4bf611e0bf2cfcb6")) + + shake128_2 = shake128.copy_state() + self.assertTrue(shake128_2.accepts_input()) + + self.assertEqual(hex_encode(shake128.output(8)), "3df0ccef456072f3") + self.assertFalse(shake128.accepts_input()) + self.assertEqual(hex_encode(shake128.output(8)), "daa5642d4b02bd5f") + + self.assertEqual(hex_encode(shake128_2.output(4)), "3df0ccef") + self.assertFalse(shake128_2.accepts_input()) + shake128_3 = shake128_2.copy_state() + self.assertFalse(shake128_3.accepts_input()) + self.assertEqual(hex_encode(shake128_3.output(12)), "456072f3daa5642d4b02bd5f") + + with self.assertRaises(botan.BotanException): + shake128.update('no more input accepted') + def test_cipher(self): for mode in ['AES-128/CTR-BE', 'Serpent/GCM', 'ChaCha20Poly1305', 'AES-128/CBC/PKCS7']: try: