Repository: privacyidea/privacyidea

Issue:
Completely switch to PKCS7 padding in the default security module
In #3115 the PKCS7 padding was added as an option to the encryption routine of the default security module.
We should always use the PKCS7 padding for encryption.
To avoid failures when decrypting values with the legacy padding, we can wrap the `un-padding` in a `try...catch` and use the legacy un-padding in case of a `ValueError`. This would always be correct since the legacy padding would never produce a valid PKCS7 padding.

Completely switch to PKCS7 padding in the default security module
In #3115 the PKCS7 padding was added as an option to the encryption routine of the default security module.
We should always use the PKCS7 padding for encryption.
To avoid failures when decrypting values with the legacy padding, we can wrap the `un-padding` in a `try...catch` and use the legacy un-padding in case of a `ValueError`. This would always be correct since the legacy padding would never produce a valid PKCS7 padding.



Discussion:

Resolving patch:
diff --git a/privacyidea/lib/crypto.py b/privacyidea/lib/crypto.py
--- a/privacyidea/lib/crypto.py
+++ b/privacyidea/lib/crypto.py
@@ -415,8 +415,7 @@ def aes_cbc_decrypt(key, iv, enc_data):
     :param iv: The initialization vector
     :type iv: bytes
     :param enc_data: The cipher text
-    :type enc_data: binary string
-    :param mode: The AES MODE
+    :type enc_data: bytes
     :return: plain text in binary data
     :rtype: bytes
     """
@@ -436,12 +435,11 @@ def aes_cbc_encrypt(key, iv, data):
     AES block size). The calling function must take care of this!
 
     :param key: The encryption key
-    :type key: binary string
+    :type key: bytes
     :param iv: The initialization vector
-    :type iv: binary string
+    :type iv: bytes
     :param data: The cipher text
     :type data: bytes
-    :param mode: The AES MODE
     :return: plain text in binary data
     :rtype: bytes
     """
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
@@ -401,18 +401,8 @@ def encrypt(self, data, iv, key_id=SecurityModule.TOKEN_KEY):
 
         key = self._get_secret(key_id)
 
-        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
+        padder = padding.PKCS7(algorithms.AES.block_size).padder()
+        padded_data = padder.update(data) + padder.finalize()
 
         res = aes_cbc_encrypt(key, iv, padded_data)
 
@@ -497,10 +487,11 @@ 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)
 
-        if get_app_config_value("PI_HSM_PADDING") == "PKCS7":
+        try:
             unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
             data = unpadder.update(output) + unpadder.finalize()
-        else:
+        except ValueError as _e:
+            # try legacy padding
             # remove padding
             eof = output.rfind(b"\x01\x02")
             if eof >= 0:


Test patch:
diff --git a/tests/test_db_model.py b/tests/test_db_model.py
--- a/tests/test_db_model.py
+++ b/tests/test_db_model.py
@@ -230,7 +230,7 @@ def test_01_create_a_token_with_a_realm(self):
         self.assertEqual(u'Hellö', t2.get_otpkey().getKey().decode('utf8'), t2)
 
         # key too long
-        k = os.urandom(256)
+        k = os.urandom(512)
         t2.set_otpkey(k)
         self.assertGreater(len(t2.key_enc), Token.key_enc.property.columns[0].type.length)
 
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,10 +67,19 @@ 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")
+        self.assertEqual(binascii.b2a_hex(cipher), b"ac17c4a5aa8703d7129c09158adc5fd4")
         text = hsm.decrypt(cipher, b"iv12345678901234")
         self.assertEqual(text, b"data")
 
+        # check that we can decrypt values with default PKCS7 padding
+        iv = b'1234567890abcdef'
+        pkcs7_cipher = '98d005d6f87c01f1719199bc3df1beb8'
+        legacy_cipher = 'bbcaff52640f9dc90be1c4e1df8a70b55a1194cc67d155722054317901e3646a'
+        self.assertEqual(b'Hallo Welt', hsm.decrypt(binascii.unhexlify(pkcs7_cipher), iv))
+
+        # check that we can decrypt values with legacy padding
+        self.assertEqual(b'Hallo Welt', hsm.decrypt(binascii.unhexlify(legacy_cipher), iv))
+
         cipher = hsm.encrypt_pin(u"pin")
         text = hsm.decrypt_pin(cipher)
         self.assertEqual(text, u"pin")
@@ -759,27 +768,3 @@ 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")
diff --git a/tests/test_lib_tokens_daplug.py b/tests/test_lib_tokens_daplug.py
--- a/tests/test_lib_tokens_daplug.py
+++ b/tests/test_lib_tokens_daplug.py
@@ -149,8 +149,7 @@ def test_04_base_methods(self):
                         token.token.so_pin)
         self.assertTrue(len(token.token.user_pin) == 32,
                         token.token.user_pin)
-        self.assertTrue(len(token.token.key_enc) == 192,
-                        token.token.key_enc)
+        self.assertEqual(len(token.token.key_enc), 96, token.token.key_enc)
         self.assertTrue(token.get_otplen() == 8)
         self.assertTrue(token.token.count == 1000,
                         token.token.count)
diff --git a/tests/test_lib_tokens_hotp.py b/tests/test_lib_tokens_hotp.py
--- a/tests/test_lib_tokens_hotp.py
+++ b/tests/test_lib_tokens_hotp.py
@@ -160,8 +160,7 @@ def test_04_base_methods(self):
                         token.token.so_pin)
         self.assertTrue(len(token.token.user_pin) == 32,
                         token.token.user_pin)
-        self.assertTrue(len(token.token.key_enc) == 192,
-                        token.token.key_enc)
+        self.assertEqual(len(token.token.key_enc), 96, token.token.key_enc)
         self.assertTrue(token.get_otplen() == 8)
         self.assertTrue(token.token.count == 1000,
                         token.token.count)
diff --git a/tests/test_lib_tokens_sms.py b/tests/test_lib_tokens_sms.py
--- a/tests/test_lib_tokens_sms.py
+++ b/tests/test_lib_tokens_sms.py
@@ -189,8 +189,7 @@ def test_04_base_methods(self):
                         token.token.so_pin)
         self.assertTrue(len(token.token.user_pin) == 32,
                         token.token.user_pin)
-        self.assertTrue(len(token.token.key_enc) == 192,
-                        token.token.key_enc)
+        self.assertEqual(len(token.token.key_enc), 96, token.token.key_enc)
         self.assertTrue(token.get_otplen() == 8)
         self.assertTrue(token.token.count == 1000,
                         token.token.count)
diff --git a/tests/test_lib_tokens_totp.py b/tests/test_lib_tokens_totp.py
--- a/tests/test_lib_tokens_totp.py
+++ b/tests/test_lib_tokens_totp.py
@@ -159,8 +159,7 @@ def test_04_base_methods(self):
                         token.token.so_pin)
         self.assertTrue(len(token.token.user_pin) == 32,
                         token.token.user_pin)
-        self.assertTrue(len(token.token.key_enc) == 192,
-                        token.token.key_enc)
+        self.assertEqual(len(token.token.key_enc), 96, token.token.key_enc)
         self.assertTrue(token.get_otplen() == 8)
         self.assertTrue(token.token.count == 1000,
                         token.token.count)