Repository: privacyidea/privacyidea

Issue:
The AES security module should also encrypt empty strings
Currently the aeshms module does not encrypt empty strings.
It is not clear, if this has any relevance or any impact on functionality.
Check it and fix it.


Discussion:
According to the current PKCS#11 spec, the `C_Encrypt` function with the `CKM_AES_CBC_PAD` method should allow any input length: https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/os/pkcs11-curr-v3.0-os.html#_Toc30061242
However tests with [SoftHSM](https://www.opendnssec.org/softhsm/) (due to the lack of a real HSM with `AES` capabilities) produce an error.
It is unclear, if the function would ever be called with empty data so i guess we could live with an error here.
We need a working error handling, since in some cases we might e.g. have an empty password and we need to save "something", so that later the empty password (e.g. of an LDAP resolver) can be used.
> We need a working error handling, since in some cases we might e.g. have an empty password and we need to save "something", so that later the empty password (e.g. of an LDAP resolver) can be used.

We could get rid of the weird and not very intuitive password settings in case of an error:
https://github.com/privacyidea/privacyidea/blob/2a66d4792289424e0643178bd902a2e6e5f96482/privacyidea/lib/crypto.py#L350
https://github.com/privacyidea/privacyidea/blob/2a66d4792289424e0643178bd902a2e6e5f96482/privacyidea/lib/crypto.py#L319

This way the admin gets a direct response when something went wrong while saving the password.

Resolving patch:
diff --git a/privacyidea/lib/crypto.py b/privacyidea/lib/crypto.py
--- a/privacyidea/lib/crypto.py
+++ b/privacyidea/lib/crypto.py
@@ -316,7 +316,8 @@ def encryptPassword(password):
         ret = hsm.encrypt_password(to_bytes(password))
     except Exception as exx:  # pragma: no cover
         log.warning(exx)
-        ret = "FAILED TO ENCRYPT PASSWORD!"
+        log.info(traceback.format_exc())
+        raise HSMException("Failed to encrypt password!")
     return ret
 
 
diff --git a/privacyidea/lib/security/aeshsm.py b/privacyidea/lib/security/aeshsm.py
--- a/privacyidea/lib/security/aeshsm.py
+++ b/privacyidea/lib/security/aeshsm.py
@@ -187,12 +187,17 @@ def random(self, length):
                 break
             except PyKCS11.PyKCS11Error as exx:
                 log.warning(u"Generate Random failed: {0!s}".format(exx))
-                # If something goes wrong in this process, we free memory, session and handles
-                self.pkcs11.lib.C_Finalize()
-                self.initialize_hsm()
-                retries += 1
-                if retries > self.max_retries:
-                    raise HSMException("Failed to generate random number after multiple retries.")
+                # If we get an CKR_SESSION_HANDLE_INVALID error code, we free
+                # memory, session and handles and retry
+                if exx.value == PyKCS11.CKR_SESSION_HANDLE_INVALID:
+                    self.pkcs11.lib.C_Finalize()
+                    self.initialize_hsm()
+                    retries += 1
+                    if retries > self.max_retries:
+                        raise HSMException("Failed to generate random number after multiple retries.")
+                else:
+                    raise HSMException("HSM random number generation failed "
+                                       "with {0!s}".format(exx))
 
         # convert the array of the random integers to a string
         return int_list_to_bytestring(r_integers)
@@ -202,8 +207,6 @@ def encrypt(self, data, iv, key_id=SecurityModule.TOKEN_KEY):
 
         :rtype: bytes
         """
-        if len(data) == 0:
-            return bytes()
         log.debug("Encrypting {} bytes with key {}".format(len(data), key_id))
         m = PyKCS11.Mechanism(PyKCS11.CKM_AES_CBC_PAD, iv)
         retries = 0
@@ -215,12 +218,16 @@ def encrypt(self, data, iv, key_id=SecurityModule.TOKEN_KEY):
                 break
             except PyKCS11.PyKCS11Error as exx:
                 log.warning(u"Encryption failed: {0!s}".format(exx))
-                # If something goes wrong in this process, we free memory,session and handles
-                self.pkcs11.lib.C_Finalize()
-                self.initialize_hsm()
-                retries += 1
-                if retries > self.max_retries:
-                    raise HSMException("Failed to encrypt after multiple retries.")
+                # If we get an CKR_SESSION_HANDLE_INVALID error code, we free
+                # memory, session and handles and retry
+                if exx.value == PyKCS11.CKR_SESSION_HANDLE_INVALID:
+                    self.pkcs11.lib.C_Finalize()
+                    self.initialize_hsm()
+                    retries += 1
+                    if retries > self.max_retries:
+                        raise HSMException("Failed to encrypt after multiple retries")
+                else:
+                    raise HSMException("HSM encryption failed with {0!s}".format(exx))
 
         return int_list_to_bytestring(r)
 
@@ -229,8 +236,9 @@ def decrypt(self, enc_data, iv, key_id=SecurityModule.TOKEN_KEY):
 
         :rtype bytes
         """
+        # we keep this for legacy reasons, even though it hasn't worked anyway
         if len(enc_data) == 0:
-            return bytes("")
+            return bytes()
         log.debug("Decrypting {} bytes with key {}".format(len(enc_data), key_id))
         m = PyKCS11.Mechanism(PyKCS11.CKM_AES_CBC_PAD, iv)
         start = datetime.datetime.now()
@@ -242,15 +250,19 @@ def decrypt(self, enc_data, iv, key_id=SecurityModule.TOKEN_KEY):
                 self.session_lastused_time = datetime.datetime.now()
                 break
             except PyKCS11.PyKCS11Error as exx:
-                log.warning(u"Decryption retry: {0!s}".format(exx))
-                # If something goes wrong in this process, we free memory, session and handlers
-                self.pkcs11.lib.C_Finalize()
-                self.initialize_hsm()
-                retries += 1
-                if retries > self.max_retries:
-                    td = datetime.datetime.now() - start
-                    log.warning(u"Decryption finally failed: {0!s}. Time taken: {1!s}.".format(exx, td))
-                    raise HSMException("Failed to decrypt after multiple retries.")
+                log.warning("Decryption failed: {0!s}".format(exx))
+                # If we get an CKR_SESSION_HANDLE_INVALID error code, we free
+                # memory, session and handles and retry
+                if exx.value == PyKCS11.CKR_SESSION_HANDLE_INVALID:
+                    self.pkcs11.lib.C_Finalize()
+                    self.initialize_hsm()
+                    retries += 1
+                    if retries > self.max_retries:
+                        td = datetime.datetime.now() - start
+                        log.warning(u"Decryption finally failed: {0!s}. Time taken: {1!s}.".format(exx, td))
+                        raise HSMException("Failed to decrypt after multiple retries.")
+                else:
+                    raise HSMException("HSM decrypt failed with {0!s}".format(exx))
 
         if retries > 0:
             td = datetime.datetime.now() - start
@@ -283,7 +295,6 @@ def create_keys(self):
                 (PyKCS11.CKA_KEY_TYPE, PyKCS11.CKK_AES),
                 (PyKCS11.CKA_VALUE_LEN, 32),
                 (PyKCS11.CKA_LABEL, label),
-                (PyKCS11.CKA_ID, label),
                 (PyKCS11.CKA_TOKEN, PyKCS11.CK_TRUE),
                 (PyKCS11.CKA_PRIVATE, True),
                 (PyKCS11.CKA_SENSITIVE, True),
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -901,7 +901,7 @@ def update(self, param, reset_failcount=True):
                     uv_required=uv_req == USER_VERIFICATION_LEVEL.REQUIRED
                 ).verify([
                     # TODO: this might get slow when a lot of webauthn tokens are registered
-                    token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
+                    token.decrypt_otpkey() for token in get_tokens(tokentype=self.type) if token.get_serial() != self.get_serial()
                 ])
             except Exception as e:
                 log.warning('Enrollment of {0!s} token failed: '


Test patch:
diff --git a/tests/pkcs11mock.py b/tests/pkcs11mock.py
--- a/tests/pkcs11mock.py
+++ b/tests/pkcs11mock.py
@@ -30,8 +30,8 @@
 except ImportError:
     # PyKCS11 not installed, let's use our simple mock module
     class MockPyKCS11Error(Exception):
-        def __init__(self, code):
-            self.code = code
+        def __init__(self, value):
+            self.value = value
 
 
     MOCK_CONSTANTS = ['CKA_CLASS', 'CKO_SECRET_KEY', 'CKA_LABEL', 'CKR_SLOT_ID_INVALID', 'CKR_DEVICE_ERROR']
@@ -140,10 +140,13 @@ def simulate_disconnect(self, count):
         This is just a shortcut for calling ``simulate_failure`` on ``generateRandom``, ``encrypt``,
         ``decrypt`` and ``openSession``.
         """
-        with self.simulate_failure(self.session_mock.generateRandom, count), \
-            self.simulate_failure(self.session_mock.encrypt, count), \
-            self.simulate_failure(self.session_mock.decrypt, count), \
-            self.simulate_failure(self.mock.openSession, count):
+        with self.simulate_failure(self.session_mock.generateRandom, count,
+                                   error=PyKCS11.CKR_SESSION_HANDLE_INVALID), \
+                self.simulate_failure(self.session_mock.encrypt, count,
+                                      error=PyKCS11.CKR_SESSION_HANDLE_INVALID), \
+                self.simulate_failure(self.session_mock.decrypt, count,
+                                      error=PyKCS11.CKR_SESSION_HANDLE_INVALID), \
+                self.simulate_failure(self.mock.openSession, count):
             yield
 
     def _mock_openSession(self, slot):
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
@@ -24,6 +24,7 @@
 
 from flask import current_app
 from six import text_type
+import PyKCS11
 from PyKCS11 import PyKCS11Error
 import string
 import passlib.hash
@@ -445,19 +446,22 @@ def test_03_retry(self):
 
             # simulate that encryption succeeds after five tries
             password = "topSekr3t" * 16
-            with pkcs11.simulate_failure(pkcs11.session_mock.encrypt, 5):
+            with pkcs11.simulate_failure(pkcs11.session_mock.encrypt, 5,
+                                         error=PyKCS11.CKR_SESSION_HANDLE_INVALID):
                 encrypted = hsm.encrypt_password(password)
                 # the session has been opened initially, and five times after that
                 self.assertEqual(pkcs11.mock.openSession.mock_calls, [call(slot=1)] * 6)
 
             # simulate that decryption succeeds after five tries
-            with pkcs11.simulate_failure(pkcs11.session_mock.decrypt, 5):
+            with pkcs11.simulate_failure(pkcs11.session_mock.decrypt, 5,
+                                         error=PyKCS11.CKR_SESSION_HANDLE_INVALID):
                 self.assertEqual(hsm.decrypt_password(encrypted), password)
                 # the session has been opened initially, five times during encryption, and five times now
                 self.assertEqual(pkcs11.mock.openSession.mock_calls, [call(slot=1)] * 11)
 
             # simulate that random generation succeeds after five tries
-            with pkcs11.simulate_failure(pkcs11.session_mock.generateRandom, 5):
+            with pkcs11.simulate_failure(pkcs11.session_mock.generateRandom, 5,
+                                         error=PyKCS11.CKR_SESSION_HANDLE_INVALID):
                 self.assertEqual(hsm.random(4), b"\x00\x01\x02\x03")
                 self.assertEqual(pkcs11.mock.openSession.mock_calls, [call(slot=1)] * 16)
 
@@ -479,12 +483,18 @@ def test_04_fail_encrypt(self):
 
             # simulate that encryption still fails after five tries
             password = "topSekr3t" * 16
-            with pkcs11.simulate_failure(pkcs11.session_mock.encrypt, 6):
+            with pkcs11.simulate_failure(pkcs11.session_mock.encrypt, 6,
+                                         error=PyKCS11.CKR_SESSION_HANDLE_INVALID):
                 with self.assertRaises(HSMException):
                     hsm.encrypt_password(password)
                 # the session has been opened initially, and six times after that
                 self.assertEqual(pkcs11.mock.openSession.mock_calls, [call(slot=1)] * 7)
 
+            with pkcs11.simulate_failure(pkcs11.session_mock.encrypt, 1,
+                                         error=PyKCS11.CKR_ARGUMENTS_BAD):
+                with self.assertRaises(HSMException):
+                    hsm.encrypt_password(password)
+
     def test_05_hsm_recovery(self):
         with PKCS11Mock() as pkcs11:
             hsm = AESHardwareSecurityModule({
@@ -508,7 +518,8 @@ def test_05_hsm_recovery(self):
 
             # simulate that the HSM disappears after that, so we cannot
             # even open a session
-            with pkcs11.simulate_failure(pkcs11.session_mock.generateRandom, 1), \
+            with pkcs11.simulate_failure(pkcs11.session_mock.generateRandom, 1,
+                                         error=PyKCS11.CKR_SESSION_HANDLE_INVALID), \
                     pkcs11.simulate_failure(pkcs11.mock.openSession, 1):
                 with self.assertRaises(PyKCS11Error):
                     hsm.encrypt_password(password)
@@ -518,7 +529,8 @@ def test_05_hsm_recovery(self):
             # but we can recover from it!
             # simulate one failure, because this will make the security module
             # acquire a new session
-            with pkcs11.simulate_failure(pkcs11.session_mock.generateRandom, 1):
+            with pkcs11.simulate_failure(pkcs11.session_mock.generateRandom, 1,
+                                         error=PyKCS11.CKR_SESSION_HANDLE_INVALID):
                 crypted = hsm.encrypt_password(password)
             text = hsm.decrypt_password(crypted)
             self.assertEqual(text, password)