Repository: privacyidea/privacyidea

Issue:
Allow to choose padding in default security module
The padding of the defaul security module and the aeshsm.py is different.
Since the new encryptkey.py inherits the default security module, it will also have a different padding, than the aeshsm.py.

It would make sense to allow encryptkey.py to have the same padding like the aeshsm.py --- or make the padding configurable.

The `lib/security/default.py` would have to be changed this way to allow for the same padding like aeshsm.py:

~~~~python
 
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives import padding 
 
 
def encrypt(self, data, iv, key_id=SecurityModule.TOKEN_KEY):
        if self.is_ready is False:
            raise HSMException('setup of security module incomplete')

        key = self._get_secret(key_id)

        # convert input to ascii, so we can securely append bin data for padding
        input_data = binascii.b2a_hex(data)
        padder = padding.PKCS7(algorithms.AES.block_size).padder()
        padded_data = padder.update(data) + padder.finalize()
        res = aes_cbc_encrypt(key, iv, padded_data)

        if self.crypted is False:
            zerome(key)
            del key
        return res

def decrypt(self, enc_data, iv, key_id=SecurityModule.TOKEN_KEY):
        if self.is_ready is False:
            raise HSMException('setup of security module incomplete')

        key = self._get_secret(key_id)
        output = aes_cbc_decrypt(key, iv, enc_data)
        # remove padding
        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
        data = unpadder.update(output) + unpadder.finalize()

        if self.crypted is False:
            zerome(key)
            del key

        return data
~~~~

To chose this padding over the old standard padding, we can add a config key:

    PI_HSM_PADDING = "PKCS7"

This might be related to #3005 



Discussion:

Resolving patch:
diff --git a/privacyidea/lib/security/default.py b/privacyidea/lib/security/default.py
--- a/privacyidea/lib/security/default.py
+++ b/privacyidea/lib/security/default.py
@@ -51,6 +51,9 @@
                                    hexlify_and_unicode)
 
 from .password import PASSWORD
+from ..framework import get_app_config_value
+from cryptography.hazmat.primitives.ciphers import algorithms
+from cryptography.hazmat.primitives import padding
 
 log = logging.getLogger(__name__)
 
@@ -398,13 +401,20 @@ def encrypt(self, data, iv, key_id=SecurityModule.TOKEN_KEY):
 
         key = self._get_secret(key_id)
 
-        # convert input to ascii, so we can securely append bin data for padding
-        input_data = binascii.b2a_hex(data)
-        input_data += b"\x01\x02"
-        padding = (16 - len(input_data) % 16) % 16
-        input_data += padding * b"\0"
+        if get_app_config_value("PI_HSM_PADDING") == "PKCS7":
+            padder = padding.PKCS7(algorithms.AES.block_size).padder()
+            padded_data = padder.update(data) + padder.finalize()
+
+        else:
+            # convert input to ascii, so we can securely append bin data for padding
+            input_data = binascii.b2a_hex(data)
+
+            input_data += b"\x01\x02"
+            default_padding = (16 - len(input_data) % 16) % 16
+            input_data += default_padding * b"\0"
+            padded_data = input_data
 
-        res = aes_cbc_encrypt(key, iv, input_data)
+        res = aes_cbc_encrypt(key, iv, padded_data)
 
         if self.crypted is False:
             zerome(key)
@@ -486,13 +496,18 @@ def decrypt(self, enc_data, iv, key_id=SecurityModule.TOKEN_KEY):
 
         key = self._get_secret(key_id)
         output = aes_cbc_decrypt(key, iv, enc_data)
-        # remove padding
-        eof = output.rfind(b"\x01\x02")
-        if eof >= 0:
-            output = output[:eof]
 
-        # convert output from ascii, back to bin data
-        data = binascii.unhexlify(output)
+        if get_app_config_value("PI_HSM_PADDING") == "PKCS7":
+            unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
+            data = unpadder.update(output) + unpadder.finalize()
+        else:
+            # remove padding
+            eof = output.rfind(b"\x01\x02")
+            if eof >= 0:
+                output = output[:eof]
+
+            # convert output from ascii, back to bin data
+            data = binascii.unhexlify(output)
 
         if self.crypted is False:
             zerome(key)


Test patch:
diff --git a/tests/test_lib_crypto.py b/tests/test_lib_crypto.py
--- a/tests/test_lib_crypto.py
+++ b/tests/test_lib_crypto.py
@@ -67,6 +67,7 @@ def test_05_encrypt_decrypt(self):
         hsm = DefaultSecurityModule({"file": config.get("PI_ENCFILE")})
 
         cipher = hsm.encrypt(b"data", b"iv12345678901234")
+        self.assertNotEqual(binascii.b2a_hex(cipher), b"ac17c4a5aa8703d7129c09158adc5fd4")
         text = hsm.decrypt(cipher, b"iv12345678901234")
         self.assertEqual(text, b"data")
 
@@ -758,3 +759,27 @@ def test_01_custom_hash_algorithm_list(self):
         self.assertRaises(passlib.exc.UnknownHashError, verify_pass_hash, password, 'password')
         # Checks if a faulty hash is failing.
         self.assertFalse(verify_pass_hash(password, argon2_fail_hash))
+
+
+class PKCS7PaddingTestCase(OverrideConfigTestCase):
+
+    class Config(TestingConfig):
+        # Set custom parameter for hsm padding in pi.cfg.
+        PI_HSM_PADDING = "PKCS7"
+
+    def test_01_encrypt_decrypt(self):
+        config = current_app.config
+        hsm = DefaultSecurityModule({"file": config.get("PI_ENCFILE")})
+
+        cipher = hsm.encrypt(b"data", b"iv12345678901234")
+        self.assertEqual(binascii.b2a_hex(cipher), b"ac17c4a5aa8703d7129c09158adc5fd4")
+        text = hsm.decrypt(cipher, b"iv12345678901234")
+        self.assertEqual(text, b"data")
+
+        cipher = hsm.encrypt_pin(u"pin")
+        text = hsm.decrypt_pin(cipher)
+        self.assertEqual(text, u"pin")
+
+        cipher = hsm.encrypt_password(u"password")
+        text = hsm.decrypt_password(cipher)
+        self.assertEqual(text, u"password")