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
69 changes: 68 additions & 1 deletion src/python/botan3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)])
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
#
Expand Down
31 changes: 31 additions & 0 deletions src/scripts/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,37 @@ def test_hash(self):
self.assertEqual(hex_encode(sha256.final()),
"08bfce15fd2406114825ee6f770a06b1b00c129cb48fcddc54ef58b5de48bdf5")

def test_xof(self):
try:
_h = botan.XOF('NoSuchXof')
Comment thread
reneme marked this conversation as resolved.
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:
Expand Down
Loading