Repository: privacyidea/privacyidea

Issue:
Check secret length when creating a yubikey token
If a Yubikey token is created through the WebUI, the secret key is passed to the API as the `otpkey` parameter.
We should check that it has exactly 16 bytes (32 chars) since it is directly used as the AES key here:
https://github.com/privacyidea/privacyidea/blob/b2bf26ab889270d76c6daef27e4280a406cd8c17/privacyidea/lib/crypto.py#L143
If the key wasn't entered correctly, this leads to a "500 Server Error"



Discussion:
We should implement this by implementing #1631 

Or: please compare this at least to checks e.g. in HOTP or TOTP enrollment. 
  If the otpkey length is checked in different tokentypes, we should do this in a similar/generic way.

Resolving patch:
diff --git a/privacyidea/lib/tokens/yubikeytoken.py b/privacyidea/lib/tokens/yubikeytoken.py
--- a/privacyidea/lib/tokens/yubikeytoken.py
+++ b/privacyidea/lib/tokens/yubikeytoken.py
@@ -53,6 +53,8 @@
 """
 
 import logging
+
+from privacyidea.lib.error import EnrollmentError
 from privacyidea.lib.log import log_with
 from privacyidea.lib.policydecorators import challenge_response_allowed
 from privacyidea.lib.tokenclass import TokenClass
@@ -477,9 +479,12 @@ def check_yubikey_pass(passw):
     @log_with(log)
     def update(self, param, reset_failcount=True):
         update_params = param.copy()
+
         # As the secret is usually copy-pasted from the Yubikey personalization GUI,
         # which separates hexlified bytes by spaces, we remove all spaces from the OTP key.
         if "otpkey" in update_params:
             update_params["otpkey"] = update_params["otpkey"].replace(" ", "")
+        if not len(update_params["otpkey"]) == 32:
+            raise EnrollmentError("The otpkey must be 32 characters long for yubikey token in AES mode")
         TokenClass.update(self, update_params, reset_failcount)
         self.add_tokeninfo("tokenkind", TOKENKIND.HARDWARE)
\ No newline at end of file


Test patch:
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -1897,7 +1897,7 @@ def test_20_init_yubikey(self):
                                            data={
                                                "type": "yubikey",
                                                "serial": "yk1",
-                                               "otpkey": self.otpkey,
+                                               "otpkey": "31323334353637383930313233343536",
                                                "yubikey.prefix": "vv123456"},
                                            headers={'Authorization': self.at}):
             res = self.app.full_dispatch_request()
@@ -1909,6 +1909,24 @@ def test_20_init_yubikey(self):
         tokens = get_tokens(serial="yk1")
         self.assertEqual(tokens[0].get_tokeninfo("yubikey.prefix"), "vv123456")
 
+    def test_20b_init_yubikey_with_wrong_otp_length(self):
+        # save yubikey.prefix
+        with self.app.test_request_context('/token/init',
+                                           method='POST',
+                                           data={
+                                               "type": "yubikey",
+                                               "serial": "yk1",
+                                               "otpkey": "31323334353637383930313233343565436",
+                                               "yubikey.prefix": "vv123456"},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 400, res)
+            result = res.json.get("result")
+            self.assertEqual(result['error']['code'], 404, result)
+            self.assertEqual(result['error']['message'],
+                             "ERR404: The otpkey must be 32 characters long for yubikey token in AES mode",
+                             result)
+
     def test_21_time_policies(self):
         # Here we test, if an admin policy does not match in time,
         # it still used to evaluate, that admin policies are defined at all
diff --git a/tests/test_lib_token.py b/tests/test_lib_token.py
--- a/tests/test_lib_token.py
+++ b/tests/test_lib_token.py
@@ -79,6 +79,7 @@ class TokenTestCase(MyTestCase):
     """
     Test the lib.token on an interface level
     """
+    yubikey_token_key = '31323334353637383930313233343536'
 
     def test_00_create_realms(self):
         self.setUp_user_realms()
@@ -172,7 +173,7 @@ def test_02_get_tokens(self):
         token = init_token({"type": "yubikey",
                             "serial": "yk1",
                             "yubikey.prefix": "vv123456",
-                            "otpkey": self.otpkey})
+                            "otpkey": self.yubikey_token_key})
         self.assertEqual(token.token.serial, "yk1")
         tokenobject_list = get_tokens(tokeninfo={"yubikey.prefix": "vv123456"})
         self.assertEqual(len(tokenobject_list), 1)
@@ -1171,12 +1172,12 @@ def test_46_init_with_validity_period(self):
     def test_47_use_yubikey_and_hotp(self):
         # fix problem https://github.com/privacyidea/privacyidea/issues/279
         user = User("cornelius", self.realm1)
-        token = init_token({"type": "hotp",
-                            "otpkey": self.otpkey,
-                            "pin": "pin47"}, user)
-        token = init_token({"type": "yubikey",
-                            "otpkey": self.otpkey,
-                            "pin": "pin47"}, user)
+        init_token({"type": "hotp",
+                    "otpkey": self.otpkey,
+                    "pin": "pin47"}, user)
+        init_token({"type": "yubikey",
+                    "otpkey": self.yubikey_token_key,
+                    "pin": "pin47"}, user)
         r = check_user_pass(user, "pin47888888")
         self.assertEqual(r[0], False)
         self.assertEqual(r[1].get('message'), "wrong otp value")
@@ -1421,7 +1422,7 @@ def test_51_tokenkind(self):
         tok.delete_token()
 
         # A yubikey and yubicloud tokentype is hardware
-        tok = init_token({"type": "yubikey", "otpkey": self.otpkey})
+        tok = init_token({"type": "yubikey", "otpkey": self.yubikey_token_key})
         kind = tok.get_tokeninfo("tokenkind")
         self.assertEqual(kind, TOKENKIND.HARDWARE)
         tok.delete_token()
diff --git a/tests/test_lib_tokens_yubikey.py b/tests/test_lib_tokens_yubikey.py
--- a/tests/test_lib_tokens_yubikey.py
+++ b/tests/test_lib_tokens_yubikey.py
@@ -2,18 +2,19 @@
 This test file tests the lib.tokens.yubikeytoken
 
 """
-PWFILE = "tests/testdata/passwords"
-
 from .base import MyTestCase
 from privacyidea.lib.tokens.yubikeytoken import (YubikeyTokenClass,
                                                  yubico_api_signature,
                                                  yubico_check_api_signature)
 from privacyidea.lib.token import init_token
-from privacyidea.models import (Token)
+from privacyidea.lib.error import EnrollmentError
+from privacyidea.models import Token
 from flask import Request, g
 from werkzeug.test import EnvironBuilder
 from privacyidea.lib.config import set_privacyidea_config
 
+PWFILE = "tests/testdata/passwords"
+
 
 class YubikeyTokenTestCase(MyTestCase):
     """
@@ -149,6 +150,14 @@ def test_05_check_maxfail(self):
         self.assertEqual(db_token.failcount, 5)
         token.set_failcount(old_failcounter)
 
+    def test_06_check_init_update(self):
+        self.assertRaises(EnrollmentError, init_token,
+                          {"type": "yubikey",
+                           "otpkey": '313233343536373839303132333435'})
+        self.assertRaises(EnrollmentError, init_token,
+                          {"type": "yubikey",
+                           "otpkey": '3132333435363738393031323334353637'})
+
     def test_09_api_signature(self):
         api_key = "LqeG/IZscF1f7/oGQBqNnGY7MLk="
         signature = "0KRJecfPNSrpZ79+xODbJl0HM8I="