Repository: randombit/botan
Pull request #1907 (2019-04-20 00:22:05)

Add Python APIs
A few items from #1906


Patch:
diff --git a/src/python/botan2.py b/src/python/botan2.py
index 82eebe576..dc8376f65 100755
--- a/src/python/botan2.py
+++ b/src/python/botan2.py
@@ -92,9 +92,6 @@ def _set_prototypes(dll):
 
     # These are generated using src/scripts/ffi_decls.py:
     ffi_api(dll.botan_constant_time_compare, [c_char_p, c_char_p, c_size_t])
-
-    ffi_api(dll.botan_same_mem, [c_char_p, c_char_p, c_size_t])
-
     ffi_api(dll.botan_scrub_mem, [c_void_p, c_size_t])
 
     ffi_api(dll.botan_hex_encode, [c_char_p, c_size_t, c_char_p, c_uint32])
@@ -396,6 +393,11 @@ _DLL = _set_prototypes(_load_botan_dll(BOTAN_FFI_VERSION))
 #
 # Internal utilities
 #
+def _call_fn_returning_sz(fn):
+    sz = c_size_t(0)
+    fn(byref(sz))
+    return int(sz.value)
+
 def _call_fn_returning_vec(guess, fn):
 
     buf = create_string_buffer(guess)
@@ -495,22 +497,94 @@ class RandomNumberGenerator(object):
         _DLL.botan_rng_get(self.__obj, out, l)
         return _ctype_bufout(out)
 
+#
+# Block cipher
+#
+class BlockCipher(object):
+    def __init__(self, algo):
+
+        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_block_cipher_init(byref(self.__obj), _ctype_str(algo), flags)
+
+        min_keylen = c_size_t(0)
+        max_keylen = c_size_t(0)
+        mod_keylen = c_size_t(0)
+        _DLL.botan_block_cipher_get_keyspec(self.__obj, byref(min_keylen), byref(max_keylen), byref(mod_keylen))
+
+        self.__min_keylen = min_keylen.value
+        self.__max_keylen = max_keylen.value
+        self.__mod_keylen = mod_keylen.value
+
+        self.__block_size = _DLL.botan_block_cipher_block_size(self.__obj)
+
+    def __del__(self):
+        _DLL.botan_block_cipher_destroy(self.__obj)
+
+    def set_key(self, key):
+        _DLL.botan_block_cipher_set_key(self.__obj, key, len(key))
+
+    def encrypt(self, pt):
+        if len(pt) % self.block_size() != 0:
+            raise Exception("Invalid input must be multiple of block size")
+
+        blocks = c_size_t(len(pt) // self.block_size())
+        output = create_string_buffer(len(pt))
+        _DLL.botan_block_cipher_encrypt_blocks(self.__obj, pt, output, blocks)
+        return output
+
+    def decrypt(self, ct):
+        if len(ct) % self.block_size() != 0:
+            raise Exception("Invalid input must be multiple of block size")
+
+        blocks = c_size_t(len(ct) // self.block_size())
+        output = create_string_buffer(len(ct))
+        _DLL.botan_block_cipher_decrypt_blocks(self.__obj, ct, output, blocks)
+        return output
+
+    def algo_name(self):
+        return _call_fn_returning_str(32, lambda b, bl: _DLL.botan_block_cipher_name(self.__obj, b, bl))
+
+    def clear(self):
+        _DLL.botan_block_cipher_clear(self.__obj)
+
+    def block_size(self):
+        return self.__block_size
+
+    def minimum_keylength(self):
+        return self.__min_keylen
+
+    def maximum_keylength(self):
+        return self.__max_keylen
+
+
 #
 # Hash function
 #
 class HashFunction(object):
     def __init__(self, algo):
-        flags = c_uint32(0) # always zero in this API version
-        self.__obj = c_void_p(0)
-        _DLL.botan_hash_init(byref(self.__obj), _ctype_str(algo), flags)
 
-        output_length = c_size_t(0)
-        _DLL.botan_hash_output_length(self.__obj, byref(output_length))
-        self.__output_length = output_length.value
+        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_hash_init(byref(self.__obj), _ctype_str(algo), flags)
+
+        self.__output_length = _call_fn_returning_sz(lambda l: _DLL.botan_hash_output_length(self.__obj, l))
+        self.__block_size = _call_fn_returning_sz(lambda l: _DLL.botan_hash_block_size(self.__obj, l))
 
     def __del__(self):
         _DLL.botan_hash_destroy(self.__obj)
 
+    def copy_state(self):
+        copy = c_void_p(0)
+        _DLL.botan_hash_copy_state(byref(copy), self.__obj)
+        return HashFunction(copy)
+
     def algo_name(self):
         return _call_fn_returning_str(32, lambda b, bl: _DLL.botan_hash_name(self.__obj, b, bl))
 
@@ -520,6 +594,9 @@ class HashFunction(object):
     def output_length(self):
         return self.__output_length
 
+    def block_size(self):
+        return self.__block_size
+
     def update(self, x):
         _DLL.botan_hash_update(self.__obj, _ctype_bits(x), len(x))
 
@@ -1066,6 +1143,18 @@ class MPI(object):
             # For int or long (or whatever else), try converting to string:
             _DLL.botan_mp_set_from_str(self.__obj, _ctype_str(str(initial_value)))
 
+    @classmethod
+    def random(cls, rng_obj, bits):
+        bn = MPI()
+        _DLL.botan_mp_rand_bits(bn.handle_(), rng_obj.handle_(), c_size_t(bits))
+        return bn
+
+    @classmethod
+    def random_range(cls, rng_obj, lower, upper):
+        bn = MPI()
+        _DLL.botan_mp_rand_range(bn.handle_(), rng_obj.handle_(), lower.handle_(), upper.handle_())
+        return bn
+
     def __del__(self):
         _DLL.botan_mp_destroy(self.__obj)
 
@@ -1101,6 +1190,20 @@ class MPI(object):
         rc = _DLL.botan_mp_is_negative(self.__obj)
         return rc == 1
 
+    def is_positive(self):
+        rc = _DLL.botan_mp_is_positive(self.__obj)
+        return rc == 1
+
+    def is_zero(self):
+        rc = _DLL.botan_mp_is_zero(self.__obj)
+        return rc == 1
+
+    def is_odd(self):
+        return self.get_bit(0) == 1
+
+    def is_even(self):
+        return self.get_bit(0) == 0
+
     def flip_sign(self):
         _DLL.botan_mp_flip_sign(self.__obj)
 
@@ -1261,6 +1364,18 @@ class HOTP(object):
         else:
             return (False, counter)
 
+def nist_key_wrap(kek, key):
+    output = create_string_buffer(len(key) + 8)
+    out_len = c_size_t(len(output))
+    _DLL.botan_key_wrap3394(key, len(key), kek, len(kek), output, byref(out_len))
+    return output[0:int(out_len.value)]
+
+def nist_key_unwrap(kek, wrapped):
+    output = create_string_buffer(len(wrapped))
+    out_len = c_size_t(len(output))
+    _DLL.botan_key_unwrap3394(wrapped, len(wrapped), kek, len(kek), output, byref(out_len))
+    return output[0:int(out_len.value)]
+
 # Typedefs for compat with older versions
 # Will be removed in a future major release
 cipher = SymmetricCipher                  # pylint: disable=invalid-name


Test patch:
diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py
index b6e8ca9d6..1ba21f570 100644
--- a/src/scripts/test_python.py
+++ b/src/scripts/test_python.py
@@ -25,6 +25,22 @@ class BotanPythonTests(unittest.TestCase):
         self.assertEqual(botan2.version_major(), 2)
         self.assertTrue(botan2.version_minor() >= 8)
 
+    def test_block_cipher(self):
+        aes = botan2.BlockCipher("AES-128")
+        self.assertEqual(aes.algo_name(), "AES-128")
+        self.assertEqual(aes.block_size(), 16)
+        self.assertEqual(aes.minimum_keylength(), 16)
+        self.assertEqual(aes.maximum_keylength(), 16)
+
+        aes.set_key(hex_decode("000102030405060708090a0b0c0d0e0f"))
+        ct = aes.encrypt(hex_decode("00112233445566778899aabbccddeeff"))
+
+        self.assertEqual(hex_encode(ct), "69c4e0d86a7b0430d8cdb78070b4c55a")
+
+        pt = aes.decrypt(ct)
+
+        self.assertEqual(hex_encode(pt), "00112233445566778899aabbccddeeff")
+
     def test_kdf(self):
 
         secret = hex_decode('6FD4C3C0F38E5C7A6F83E99CD9BD')
@@ -104,24 +120,29 @@ class BotanPythonTests(unittest.TestCase):
     def test_hash(self):
 
         try:
-            h = botan2.HashFunction('NoSuchHash')
+            _h = botan2.HashFunction('NoSuchHash')
         except botan2.BotanException as e:
             self.assertEqual(str(e), "botan_hash_init failed: -40 (Not implemented)")
 
-        h = botan2.HashFunction('SHA-256')
-        self.assertEqual(h.algo_name(), 'SHA-256')
-        assert h.output_length() == 32
-        h.update('ignore this please')
-        h.clear()
-        h.update('a')
-        h1 = h.final()
-
-        self.assertEqual(hex_encode(h1), "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb")
-
-        h.update(hex_decode('616263'))
-        h2 = h.final()
+        sha256 = botan2.HashFunction('SHA-256')
+        self.assertEqual(sha256.algo_name(), 'SHA-256')
+        self.assertEqual(sha256.output_length(), 32)
+        self.assertEqual(sha256.block_size(), 64)
+        sha256.update('ignore this please')
+        sha256.clear()
+        sha256.update('a')
+        hash1 = sha256.final()
+
+        self.assertEqual(hex_encode(hash1), "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb")
+
+        sha256.update(hex_decode('61'))
+        sha256_2 = sha256.copy_state()
+        sha256.update(hex_decode('6263'))
+        h2 = sha256.final()
         self.assertEqual(hex_encode(h2), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
 
+        self.assertEqual(hex_encode(sha256_2.final()), hex_encode(hash1))
+
     def test_cipher(self):
         for mode in ['AES-128/CTR-BE', 'Serpent/GCM', 'ChaCha20Poly1305']:
             enc = botan2.SymmetricCipher(mode, encrypt=True)
@@ -381,6 +402,20 @@ ofvkP1EDmpx50fHLawIDAQAB
         p = inv.pow_mod(botan2.MPI(46), mod)
         self.assertEqual(int(p), 42)
 
+    def test_mpi_random(self):
+        rng = botan2.RandomNumberGenerator()
+
+        u = botan2.MPI.random(rng, 512)
+        self.assertEqual(u.bit_count(), 512)
+
+        l = u >> 32
+        self.assertEqual(l.bit_count(), 512-32)
+
+        for _i in range(10):
+            x = botan2.MPI.random_range(rng, l, u)
+            self.assertTrue(x < u)
+            self.assertTrue(x > l)
+
     def test_fpe(self):
 
         modulus = botan2.MPI('1000000000')
@@ -397,6 +432,17 @@ ofvkP1EDmpx50fHLawIDAQAB
 
         self.assertEqual(value, ptext)
 
+    def test_keywrap(self):
+        key = hex_decode('00112233445566778899aabbccddeeff')
+        kek = hex_decode('000102030405060708090a0b0c0d0e0f')
+
+        wrapped = botan2.nist_key_wrap(kek, key)
+        self.assertEqual(hex_encode(wrapped), '1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5')
+
+        self.assertEqual(len(wrapped), 16+8)
+        unwrapped = botan2.nist_key_unwrap(kek, wrapped)
+        self.assertEqual(hex_encode(unwrapped), '00112233445566778899aabbccddeeff')
+
     def test_hotp(self):
 
         hotp = botan2.HOTP(b'12345678901234567890')