auth.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2008 Nicolas Kandel
  3. # Copyright © 2008 Pascal Halter
  4. # Copyright © 2008-2016 Guillaume Ayoub
  5. #
  6. # This library is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. Authentication management.
  20. Default is htpasswd authentication.
  21. Apache's htpasswd command (httpd.apache.org/docs/programs/htpasswd.html) manages
  22. a file for storing user credentials. It can encrypt passwords using different
  23. methods, e.g. BCRYPT, MD5-APR1 (a version of MD5 modified for Apache), SHA1, or
  24. by using the system's CRYPT routine. The CRYPT and SHA1 encryption methods
  25. implemented by htpasswd are considered as insecure. MD5-APR1 provides medium
  26. security as of 2015. Only BCRYPT can be considered secure by current standards.
  27. MD5-APR1-encrypted credentials can be written by all versions of htpasswd (its
  28. the default, in fact), whereas BCRYPT requires htpasswd 2.4.x or newer.
  29. The `is_authenticated(user, password)` function provided by this module
  30. verifies the user-given credentials by parsing the htpasswd credential file
  31. pointed to by the ``htpasswd_filename`` configuration value while assuming
  32. the password encryption method specified via the ``htpasswd_encryption``
  33. configuration value.
  34. The following htpasswd password encrpytion methods are supported by Radicale
  35. out-of-the-box:
  36. - plain-text (created by htpasswd -p...) -- INSECURE
  37. - CRYPT (created by htpasswd -d...) -- INSECURE
  38. - SHA1 (created by htpasswd -s...) -- INSECURE
  39. When passlib (https://pypi.python.org/pypi/passlib) is importable, the
  40. following significantly more secure schemes are parsable by Radicale:
  41. - MD5-APR1 (htpasswd -m...) -- htpasswd's default method
  42. - BCRYPT (htpasswd -B...) -- Requires htpasswd 2.4.x
  43. """
  44. import base64
  45. import hashlib
  46. import os
  47. import sys
  48. from . import config, log
  49. def _load():
  50. """Load the authentication manager chosen in configuration."""
  51. auth_type = config.get("auth", "type")
  52. log.LOGGER.debug("Authentication type is %s" % auth_type)
  53. if auth_type == "None":
  54. sys.modules[__name__].is_authenticated = lambda user, password: True
  55. elif auth_type == "htpasswd":
  56. pass # is_authenticated is already defined
  57. else:
  58. __import__(auth_type)
  59. sys.modules[__name__].is_authenticated = (
  60. sys.modules[auth_type].is_authenticated)
  61. FILENAME = os.path.expanduser(config.get("auth", "htpasswd_filename"))
  62. ENCRYPTION = config.get("auth", "htpasswd_encryption")
  63. def _plain(hash_value, password):
  64. """Check if ``hash_value`` and ``password`` match, using plain method."""
  65. return hash_value == password
  66. def _crypt(hash_value, password):
  67. """Check if ``hash_value`` and ``password`` match, using crypt method."""
  68. return crypt.crypt(password, hash_value) == hash_value
  69. def _sha1(hash_value, password):
  70. """Check if ``hash_value`` and ``password`` match, using sha1 method."""
  71. hash_value = hash_value.replace("{SHA}", "").encode("ascii")
  72. password = password.encode(config.get("encoding", "stock"))
  73. sha1 = hashlib.sha1() # pylint: disable=E1101
  74. sha1.update(password)
  75. return sha1.digest() == base64.b64decode(hash_value)
  76. def _ssha(hash_salt_value, password):
  77. """Check if ``hash_salt_value`` and ``password`` match, using salted sha1
  78. method. This method is not directly supported by htpasswd, but it can be
  79. written with e.g. openssl, and nginx can parse it."""
  80. hash_salt_value = hash_salt_value.replace(
  81. "{SSHA}", "").encode("ascii").decode('base64')
  82. password = password.encode(config.get("encoding", "stock"))
  83. hash_value = hash_salt_value[:20]
  84. salt_value = hash_salt_value[20:]
  85. sha1 = hashlib.sha1() # pylint: disable=E1101
  86. sha1.update(password)
  87. sha1.update(salt_value)
  88. return sha1.digest() == hash_value
  89. def _bcrypt(hash_value, password):
  90. return _passlib_bcrypt.verify(password, hash_value)
  91. def _md5apr1(hash_value, password):
  92. return _passlib_md5apr1.verify(password, hash_value)
  93. # Prepare mapping between encryption names and verification functions.
  94. # Pre-fill with methods that do not have external dependencies.
  95. _verifuncs = {
  96. "ssha": _ssha,
  97. "sha1": _sha1,
  98. "plain": _plain}
  99. # Conditionally attempt to import external dependencies.
  100. if ENCRYPTION == "md5":
  101. try:
  102. from passlib.hash import apr_md5_crypt as _passlib_md5apr1
  103. except ImportError:
  104. raise RuntimeError(("The htpasswd_encryption method 'md5' requires "
  105. "availability of the passlib module."))
  106. _verifuncs["md5"] = _md5apr1
  107. elif ENCRYPTION == "bcrypt":
  108. try:
  109. from passlib.hash import bcrypt as _passlib_bcrypt
  110. except ImportError:
  111. raise RuntimeError(("The htpasswd_encryption method 'bcrypt' requires "
  112. "availability of the passlib module with bcrypt support."))
  113. # A call to `encrypt` raises passlib.exc.MissingBackendError with a good
  114. # error message if bcrypt backend is not available. Trigger this here.
  115. _passlib_bcrypt.encrypt("test-bcrypt-backend")
  116. _verifuncs["bcrypt"] = _bcrypt
  117. elif ENCRYPTION == "crypt":
  118. try:
  119. import crypt
  120. except ImportError:
  121. raise RuntimeError(("The htpasswd_encryption method 'crypt' requires "
  122. "crypt() system support."))
  123. _verifuncs["crypt"] = _crypt
  124. # Validate initial configuration.
  125. if ENCRYPTION not in _verifuncs:
  126. raise RuntimeError(("The htpasswd encryption method '%s' is not "
  127. "supported." % ENCRYPTION))
  128. def is_authenticated(user, password):
  129. """Validate credentials.
  130. Iterate through htpasswd credential file until user matches, extract hash
  131. (encrypted password) and check hash against user-given password, using the
  132. method specified in the Radicale config.
  133. """
  134. with open(FILENAME) as f:
  135. for line in f:
  136. strippedline = line.strip()
  137. if strippedline:
  138. login, hash_value = strippedline.split(":")
  139. if login == user:
  140. # Allow encryption method to be overridden at runtime.
  141. return _verifuncs[ENCRYPTION](hash_value, password)
  142. return False