"Fossies" - the Fresh Open Source Software Archive

Member "roundup-2.0.0/roundup/password.py" (30 Apr 2020, 13792 Bytes) of package /linux/www/roundup-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "password.py": 1.6.1_vs_2.0.0.

    1 #
    2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
    3 # This module is free software, and you may redistribute it and/or modify
    4 # under the same terms as Python, so long as this copyright message and
    5 # disclaimer are retained in their original form.
    6 #
    7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
    8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
    9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
   10 # POSSIBILITY OF SUCH DAMAGE.
   11 #
   12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
   13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
   15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
   16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
   17 #
   18 """Password handling (encoding, decoding).
   19 """
   20 __docformat__ = 'restructuredtext'
   21 
   22 import re, string
   23 from base64 import b64encode, b64decode
   24 from hashlib import md5, sha1
   25 
   26 from roundup.anypy.strings import us2s, b2s, s2b
   27 import roundup.anypy.random_ as random_
   28 
   29 try:
   30     import crypt
   31 except ImportError:
   32     crypt = None
   33 
   34 _bempty = b""
   35 _bjoin = _bempty.join
   36 
   37 
   38 def bchr(c):
   39     if bytes == str:
   40         # Python 2.
   41         return chr(c)
   42     else:
   43         # Python 3.
   44         return bytes((c,))
   45 
   46 
   47 def bord(c):
   48     if bytes == str:
   49         # Python 2.
   50         return ord(c)
   51     else:
   52         # Python 3.  Elements of bytes are integers.
   53         return c
   54 
   55 
   56 # NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size,
   57 #      and have charset that's compatible w/ unix crypt variants
   58 def h64encode(data):
   59     """encode using variant of base64"""
   60     return b2s(b64encode(data, b"./").strip(b"=\n"))
   61 
   62 
   63 def h64decode(data):
   64     """decode using variant of base64"""
   65     data = s2b(data)
   66     off = len(data) % 4
   67     if off == 0:
   68         return b64decode(data, b"./")
   69     elif off == 1:
   70         raise ValueError("Invalid base64 input")
   71     elif off == 2:
   72         return b64decode(data + b"==", b"./")
   73     else:
   74         return b64decode(data + b"=", b"./")
   75 
   76 
   77 try:
   78     from hashlib import pbkdf2_hmac
   79 
   80     def _pbkdf2(password, salt, rounds, keylen):
   81         return pbkdf2_hmac('sha1', password, salt, rounds, keylen)
   82 except ImportError:
   83     # no hashlib.pbkdf2_hmac - make our own pbkdf2 function
   84     from struct import pack
   85     from hmac import HMAC
   86 
   87     def xor_bytes(left, right):
   88         "perform bitwise-xor of two byte-strings"
   89         return _bjoin(bchr(bord(l) ^ bord(r)) for l, r in zip(left, right))
   90 
   91     def _pbkdf2(password, salt, rounds, keylen):
   92         digest_size = 20  # sha1 generates 20-byte blocks
   93         total_blocks = int((keylen+digest_size-1)/digest_size)
   94         hmac_template = HMAC(password, None, sha1)
   95         out = _bempty
   96         for i in range(1, total_blocks+1):
   97             hmac = hmac_template.copy()
   98             hmac.update(salt + pack(">L", i))
   99             block = tmp = hmac.digest()
  100             for _j in range(rounds-1):
  101                 hmac = hmac_template.copy()
  102                 hmac.update(tmp)
  103                 tmp = hmac.digest()
  104                 # TODO: need to speed up this call
  105                 block = xor_bytes(block, tmp)
  106             out += block
  107         return out[:keylen]
  108 
  109 
  110 def ssha(password, salt):
  111     ''' Make ssha digest from password and salt.
  112     Based on code of Roberto Aguilar <roberto@baremetal.io>
  113     https://gist.github.com/rca/7217540
  114     '''
  115     shaval = sha1(password)  # nosec
  116     shaval.update(salt)
  117     ssha_digest = b2s(b64encode(shaval.digest() + salt).strip())
  118     return ssha_digest
  119 
  120 
  121 def pbkdf2(password, salt, rounds, keylen):
  122     """pkcs#5 password-based key derivation v2.0
  123 
  124     :arg password: passphrase to use to generate key (if unicode,
  125      converted to utf-8)
  126     :arg salt: salt bytes to use when generating key
  127     :param rounds: number of rounds to use to generate key
  128     :arg keylen: number of bytes to generate
  129 
  130     If hashlib supports pbkdf2, uses it's implementation as backend.
  131 
  132     :returns:
  133         raw bytes of generated key
  134     """
  135     password = s2b(us2s(password))
  136     if keylen > 40:
  137         # NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
  138         # but m2crypto has issues on some platforms above 40,
  139         # and such sizes aren't needed for a password hash anyways...
  140         raise ValueError("key length too large")
  141     if rounds < 1:
  142         raise ValueError("rounds must be positive number")
  143     return _pbkdf2(password, salt, rounds, keylen)
  144 
  145 
  146 class PasswordValueError(ValueError):
  147     """ The password value is not valid """
  148     pass
  149 
  150 
  151 def pbkdf2_unpack(pbkdf2):
  152     """ unpack pbkdf2 encrypted password into parts,
  153         assume it has format "{rounds}${salt}${digest}
  154     """
  155     pbkdf2 = us2s(pbkdf2)
  156     try:
  157         rounds, salt, digest = pbkdf2.split("$")
  158     except ValueError:
  159         raise PasswordValueError("invalid PBKDF2 hash (wrong number of "
  160                                  "separators)")
  161     if rounds.startswith("0"):
  162         raise PasswordValueError("invalid PBKDF2 hash (zero-padded rounds)")
  163     try:
  164         rounds = int(rounds)
  165     except ValueError:
  166         raise PasswordValueError("invalid PBKDF2 hash (invalid rounds)")
  167     raw_salt = h64decode(salt)
  168     return rounds, salt, raw_salt, digest
  169 
  170 
  171 def encodePassword(plaintext, scheme, other=None, config=None):
  172     """Encrypt the plaintext password.
  173     """
  174     if plaintext is None:
  175         plaintext = ""
  176     if scheme == "PBKDF2":
  177         if other:
  178             rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
  179         else:
  180             raw_salt = random_.token_bytes(20)
  181             salt = h64encode(raw_salt)
  182             if config:
  183                 rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
  184             else:
  185                 rounds = 10000
  186         if rounds < 1000:
  187             raise PasswordValueError("invalid PBKDF2 hash (rounds too low)")
  188         raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20)
  189         return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest))
  190     elif scheme == 'SSHA':
  191         if other:
  192             raw_other = b64decode(other)
  193             salt = raw_other[20:]
  194         else:
  195             # new password
  196             # variable salt length
  197             salt_len = random_.randbelow(52-36) + 36
  198             salt = random_.token_bytes(salt_len)
  199         s = ssha(s2b(plaintext), salt)
  200     elif scheme == 'SHA':
  201         s = sha1(s2b(plaintext)).hexdigest()  # nosec
  202     elif scheme == 'MD5':
  203         s = md5(s2b(plaintext)).hexdigest()  # nosec
  204     elif scheme == 'crypt' and crypt is not None:
  205         if other is not None:
  206             salt = other
  207         else:
  208             saltchars = './0123456789'+string.ascii_letters
  209             salt = random_.choice(saltchars) + random_.choice(saltchars)
  210         s = crypt.crypt(plaintext, salt)
  211     elif scheme == 'plaintext':
  212         s = plaintext
  213     else:
  214         raise PasswordValueError('Unknown encryption scheme %r' % scheme)
  215     return s
  216 
  217 
  218 def generatePassword(length=12):
  219     chars = string.ascii_letters+string.digits
  220     password = [random_.choice(chars) for x in range(length - 1)]
  221     # make sure there is at least one digit
  222     digitidx = random_.randbelow(length)
  223     password[digitidx:digitidx] = [random_.choice(string.digits)]
  224     return ''.join(password)
  225 
  226 
  227 class JournalPassword:
  228     """ Password dummy instance intended for journal operation.
  229         We do not store passwords in the journal any longer.  The dummy
  230         version only reads the encryption scheme from the given
  231         encrypted password.
  232     """
  233     default_scheme = 'PBKDF2'        # new encryptions use this scheme
  234     pwre = re.compile(r'{(\w+)}(.+)')
  235 
  236     def __init__(self, encrypted=''):
  237         if isinstance(encrypted, self.__class__):
  238             self.scheme = encrypted.scheme or self.default_scheme
  239         else:
  240             m = self.pwre.match(encrypted)
  241             if m:
  242                 self.scheme = m.group(1)
  243             else:
  244                 self.scheme = self.default_scheme
  245         self.password = ''
  246 
  247     def dummystr(self):
  248         """ return dummy string to store in journal
  249             - reports scheme, but nothing else
  250         """
  251         return "{%s}*encrypted*" % (self.scheme,)
  252 
  253     __str__ = dummystr
  254 
  255     def __eq__(self, other):
  256         """Compare this password against another password."""
  257         # check to see if we're comparing instances
  258         if isinstance(other, self.__class__):
  259             if self.scheme != other.scheme:
  260                 return False
  261             return self.password == other.password
  262 
  263         # assume password is plaintext
  264         if self.password is None:
  265             raise ValueError('Password not set')
  266         return self.password == encodePassword(other, self.scheme,
  267                                                self.password or None)
  268 
  269     def __ne__(self, other):
  270         return not self.__eq__(other)
  271 
  272 
  273 class Password(JournalPassword):
  274     """The class encapsulates a Password property type value in the database.
  275 
  276     The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
  277     The encodePassword function is used to actually encode the password from
  278     plaintext. The None encoding is used in legacy databases where no
  279     encoding scheme is identified.
  280 
  281     The scheme is stored with the encoded data in the database:
  282         {scheme}data
  283 
  284     Example usage:
  285     >>> p = Password('sekrit')
  286     >>> p == 'sekrit'
  287     1
  288     >>> p != 'not sekrit'
  289     1
  290     >>> 'sekrit' == p
  291     1
  292     >>> 'not sekrit' != p
  293     1
  294     """
  295     # TODO: code to migrate from old password schemes.
  296 
  297     deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"]
  298     known_schemes = ["PBKDF2", "SSHA"] + deprecated_schemes
  299 
  300     def __init__(self, plaintext=None, scheme=None, encrypted=None,
  301                  strict=False, config=None):
  302         """Call setPassword if plaintext is not None."""
  303         if scheme is None:
  304             scheme = self.default_scheme
  305         if plaintext is not None:
  306             self.setPassword(plaintext, scheme, config=config)
  307         elif encrypted is not None:
  308             self.unpack(encrypted, scheme, strict=strict, config=config)
  309         else:
  310             self.scheme = self.default_scheme
  311             self.password = None
  312             self.plaintext = None
  313 
  314     def __repr__(self):
  315         return self.__str__()
  316 
  317     def needs_migration(self):
  318         """ Password has insecure scheme or other insecure parameters
  319             and needs migration to new password scheme
  320         """
  321         if self.scheme in self.deprecated_schemes:
  322             return True
  323         rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password)
  324         if rounds < 1000:
  325             return True
  326         return False
  327 
  328     def unpack(self, encrypted, scheme=None, strict=False, config=None):
  329         """Set the password info from the scheme:<encryted info> string
  330            (the inverse of __str__)
  331         """
  332         m = self.pwre.match(encrypted)
  333         if m:
  334             self.scheme = m.group(1)
  335             self.password = m.group(2)
  336             self.plaintext = None
  337         else:
  338             # currently plaintext - encrypt
  339             self.setPassword(encrypted, scheme, config=config)
  340         if strict and self.scheme not in self.known_schemes:
  341             raise PasswordValueError("Unknown encryption scheme: %r" %
  342                                      (self.scheme,))
  343 
  344     def setPassword(self, plaintext, scheme=None, config=None):
  345         """Sets encrypts plaintext."""
  346         if scheme is None:
  347             scheme = self.default_scheme
  348         self.scheme = scheme
  349         self.password = encodePassword(plaintext, scheme, config=config)
  350         self.plaintext = plaintext
  351 
  352     def __str__(self):
  353         """Stringify the encrypted password for database storage."""
  354         if self.password is None:
  355             raise ValueError('Password not set')
  356         return '{%s}%s' % (self.scheme, self.password)
  357 
  358 
  359 def test():
  360     # SHA
  361     p = Password('sekrit')
  362     assert Password(encrypted=str(p)) == 'sekrit'
  363     assert 'sekrit' == Password(encrypted=str(p))
  364     assert p == 'sekrit'
  365     assert p != 'not sekrit'
  366     assert 'sekrit' == p
  367     assert 'not sekrit' != p
  368 
  369     # MD5
  370     p = Password('sekrit', 'MD5')
  371     assert Password(encrypted=str(p)) == 'sekrit'
  372     assert 'sekrit' == Password(encrypted=str(p))
  373     assert p == 'sekrit'
  374     assert p != 'not sekrit'
  375     assert 'sekrit' == p
  376     assert 'not sekrit' != p
  377 
  378     # crypt
  379     if crypt:  # not available on Windows
  380         p = Password('sekrit', 'crypt')
  381         assert Password(encrypted=str(p)) == 'sekrit'
  382         assert 'sekrit' == Password(encrypted=str(p))
  383         assert p == 'sekrit'
  384         assert p != 'not sekrit'
  385         assert 'sekrit' == p
  386         assert 'not sekrit' != p
  387 
  388     # SSHA
  389     p = Password('sekrit', 'SSHA')
  390     assert Password(encrypted=str(p)) == 'sekrit'
  391     assert 'sekrit' == Password(encrypted=str(p))
  392     assert p == 'sekrit'
  393     assert p != 'not sekrit'
  394     assert 'sekrit' == p
  395     assert 'not sekrit' != p
  396 
  397     # PBKDF2 - low level function
  398     from binascii import unhexlify
  399     k = pbkdf2("password", b"ATHENA.MIT.EDUraeburn", 1200, 32)
  400     assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13")
  401 
  402     # PBKDF2 - hash function
  403     h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
  404     assert encodePassword("sekrit", "PBKDF2", h) == h
  405 
  406     # PBKDF2 - high level integration
  407     p = Password('sekrit', 'PBKDF2')
  408     assert Password(encrypted=str(p)) == 'sekrit'
  409     assert 'sekrit' == Password(encrypted=str(p))
  410     assert p == 'sekrit'
  411     assert p != 'not sekrit'
  412     assert 'sekrit' == p
  413     assert 'not sekrit' != p
  414 
  415 
  416 if __name__ == '__main__':
  417     test()
  418 
  419 # vim: set filetype=python sts=4 sw=4 et si :