Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AuthEncoding.py 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. ##############################################################################
  2. #
  3. # Copyright (c) 2002, 2015 Zope Foundation and Contributors.
  4. #
  5. # This software is subject to the provisions of the Zope Public License,
  6. # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
  7. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
  8. # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  9. # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
  10. # FOR A PARTICULAR PURPOSE
  11. #
  12. ##############################################################################
  13. import binascii
  14. import six
  15. from binascii import b2a_base64, a2b_base64
  16. from hashlib import sha1 as sha
  17. from hashlib import sha256
  18. from os import getpid
  19. import time
  20. from .compat import long, b, u
  21. # Use the system PRNG if possible
  22. import random
  23. try:
  24. random = random.SystemRandom()
  25. using_sysrandom = True
  26. except NotImplementedError:
  27. using_sysrandom = False
  28. def _reseed():
  29. if not using_sysrandom:
  30. # This is ugly, and a hack, but it makes things better than
  31. # the alternative of predictability. This re-seeds the PRNG
  32. # using a value that is hard for an attacker to predict, every
  33. # time a random string is required. This may change the
  34. # properties of the chosen random sequence slightly, but this
  35. # is better than absolute predictability.
  36. random.seed(sha256(
  37. "%s%s%s" % (random.getstate(), time.time(), getpid())
  38. ).digest())
  39. def _choice(c):
  40. _reseed()
  41. return random.choice(c)
  42. def _randrange(r):
  43. _reseed()
  44. return random.randrange(r)
  45. def constant_time_compare(val1, val2):
  46. """
  47. Returns True if the two strings are equal, False otherwise.
  48. The time taken is independent of the number of characters that match.
  49. """
  50. if len(val1) != len(val2):
  51. return False
  52. result = 0
  53. for x, y in zip(six.iterbytes(val1), six.iterbytes(val2)):
  54. result |= x ^ y
  55. return result == 0
  56. class PasswordEncryptionScheme: # An Interface
  57. def encrypt(pw):
  58. """
  59. Encrypt the provided plain text password.
  60. """
  61. def validate(reference, attempt):
  62. """
  63. Validate the provided password string. Reference is the
  64. correct password, which may be encrypted; attempt is clear text
  65. password attempt.
  66. """
  67. _schemes = []
  68. def registerScheme(id, s):
  69. '''
  70. Registers an LDAP password encoding scheme.
  71. '''
  72. _schemes.append((id, u'{%s}' % id, s))
  73. def listSchemes():
  74. return [id for id, prefix, scheme in _schemes]
  75. class SSHADigestScheme:
  76. '''
  77. SSHA is a modification of the SHA digest scheme with a salt
  78. starting at byte 20 of the base64-encoded string.
  79. '''
  80. # Source: http://developer.netscape.com/docs/technote/ldap/pass_sha.html
  81. def generate_salt(self):
  82. # Salt can be any length, but not more than about 37 characters
  83. # because of limitations of the binascii module.
  84. # 7 is what Netscape's example used and should be enough.
  85. # All 256 characters are available.
  86. salt = b''
  87. for n in range(7):
  88. salt += six.int2byte(_randrange(256))
  89. return salt
  90. def encrypt(self, pw):
  91. return self._encrypt_with_salt(pw, self.generate_salt())
  92. def validate(self, reference, attempt):
  93. try:
  94. ref = a2b_base64(reference)
  95. except binascii.Error:
  96. # Not valid base64.
  97. return 0
  98. salt = ref[20:]
  99. compare = self._encrypt_with_salt(attempt, salt)
  100. return constant_time_compare(compare, reference)
  101. def _encrypt_with_salt(self, pw, salt):
  102. pw = b(pw)
  103. return b2a_base64(sha(pw + salt).digest() + salt)[:-1]
  104. registerScheme(u'SSHA', SSHADigestScheme())
  105. class SHADigestScheme:
  106. def encrypt(self, pw):
  107. return self._encrypt(pw)
  108. def validate(self, reference, attempt):
  109. compare = self._encrypt(attempt)
  110. return constant_time_compare(compare, reference)
  111. def _encrypt(self, pw):
  112. pw = b(pw)
  113. return b2a_base64(sha(pw).digest())[:-1]
  114. registerScheme(u'SHA', SHADigestScheme())
  115. class SHA256DigestScheme:
  116. def encrypt(self, pw):
  117. return b(sha256(b(pw)).hexdigest())
  118. def validate(self, reference, attempt):
  119. a = self.encrypt(attempt)
  120. return constant_time_compare(a, reference)
  121. registerScheme(u'SHA256', SHA256DigestScheme())
  122. # Bogosity on various platforms due to ITAR restrictions
  123. try:
  124. from crypt import crypt
  125. except ImportError:
  126. crypt = None
  127. if crypt is not None:
  128. class CryptDigestScheme:
  129. def generate_salt(self):
  130. choices = (u"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  131. u"abcdefghijklmnopqrstuvwxyz"
  132. u"0123456789./")
  133. return _choice(choices) + _choice(choices)
  134. def encrypt(self, pw):
  135. return b(crypt(self._recode_password(pw), self.generate_salt()))
  136. def validate(self, reference, attempt):
  137. attempt = self._recode_password(attempt)
  138. a = b(crypt(attempt, reference[:2].decode('ascii')))
  139. return constant_time_compare(a, reference)
  140. def _recode_password(self, pw):
  141. # crypt always requires `str` which has a different meaning among
  142. # the Python versions:
  143. if six.PY3:
  144. return u(pw)
  145. return b(pw)
  146. registerScheme(u'CRYPT', CryptDigestScheme())
  147. class MySQLDigestScheme:
  148. def encrypt(self, pw):
  149. pw = u(pw)
  150. nr = long(1345345333)
  151. add = 7
  152. nr2 = long(0x12345671)
  153. for i in pw:
  154. if i == ' ' or i == '\t':
  155. continue
  156. nr ^= (((nr & 63) + add) * ord(i)) + (nr << 8)
  157. nr2 += (nr2 << 8) ^ nr
  158. add += ord(i)
  159. r0 = nr & ((long(1) << 31) - long(1))
  160. r1 = nr2 & ((long(1) << 31) - long(1))
  161. return (u"%08lx%08lx" % (r0, r1)).encode('ascii')
  162. def validate(self, reference, attempt):
  163. a = self.encrypt(attempt)
  164. return constant_time_compare(a, reference)
  165. registerScheme(u'MYSQL', MySQLDigestScheme())
  166. def pw_validate(reference, attempt):
  167. """Validate the provided password string, which uses LDAP-style encoding
  168. notation. Reference is the correct password, attempt is clear text
  169. password attempt."""
  170. reference = b(reference)
  171. for id, prefix, scheme in _schemes:
  172. lp = len(prefix)
  173. if reference[:lp] == b(prefix):
  174. return scheme.validate(reference[lp:], attempt)
  175. # Assume cleartext.
  176. return constant_time_compare(reference, b(attempt))
  177. def is_encrypted(pw):
  178. for id, prefix, scheme in _schemes:
  179. lp = len(prefix)
  180. if pw[:lp] == b(prefix):
  181. return 1
  182. return 0
  183. def pw_encrypt(pw, encoding=u'SSHA'):
  184. """Encrypt the provided plain text password using the encoding if provided
  185. and return it in an LDAP-style representation."""
  186. encoding = u(encoding)
  187. for id, prefix, scheme in _schemes:
  188. if encoding == id:
  189. return b(prefix) + scheme.encrypt(pw)
  190. raise ValueError('Not supported: %s' % encoding)
  191. pw_encode = pw_encrypt # backward compatibility