"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "roundup/password.py" between
roundup-1.6.1.tar.gz and roundup-2.0.0.tar.gz

About: Roundup is an highly customisable issue-tracking system with command-line, web and e-mail interfaces (written in Python).

password.py  (roundup-1.6.1):password.py  (roundup-2.0.0)
skipping to change at line 22 skipping to change at line 22
# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# #
"""Password handling (encoding, decoding). """Password handling (encoding, decoding).
""" """
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
import re, string, random import re, string
import os
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from hashlib import md5, sha1 from hashlib import md5, sha1
from roundup.anypy.strings import us2s, b2s, s2b
import roundup.anypy.random_ as random_
try: try:
import crypt import crypt
except ImportError: except ImportError:
crypt = None crypt = None
_bempty = "" _bempty = b""
_bjoin = _bempty.join _bjoin = _bempty.join
def getrandbytes(count): def bchr(c):
return _bjoin(chr(random.randint(0,255)) for i in xrange(count)) if bytes == str:
# Python 2.
return chr(c)
else:
# Python 3.
return bytes((c,))
def bord(c):
if bytes == str:
# Python 2.
return ord(c)
else:
# Python 3. Elements of bytes are integers.
return c
#NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size, # NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size,
# and have charset that's compatible w/ unix crypt variants # and have charset that's compatible w/ unix crypt variants
def h64encode(data): def h64encode(data):
"""encode using variant of base64""" """encode using variant of base64"""
return b64encode(data, "./").strip("=\n") return b2s(b64encode(data, b"./").strip(b"=\n"))
def h64decode(data): def h64decode(data):
"""decode using variant of base64""" """decode using variant of base64"""
data = s2b(data)
off = len(data) % 4 off = len(data) % 4
if off == 0: if off == 0:
return b64decode(data, "./") return b64decode(data, b"./")
elif off == 1: elif off == 1:
raise ValueError("Invalid base64 input") raise ValueError("Invalid base64 input")
elif off == 2: elif off == 2:
return b64decode(data + "==", "./") return b64decode(data + b"==", b"./")
else: else:
return b64decode(data + "=", "./") return b64decode(data + b"=", b"./")
try: try:
from M2Crypto.EVP import pbkdf2 as _pbkdf2 from hashlib import pbkdf2_hmac
def _pbkdf2(password, salt, rounds, keylen):
return pbkdf2_hmac('sha1', password, salt, rounds, keylen)
except ImportError: except ImportError:
#no m2crypto - make our own pbkdf2 function # no hashlib.pbkdf2_hmac - make our own pbkdf2 function
from struct import pack from struct import pack
from hmac import HMAC from hmac import HMAC
def xor_bytes(left, right): def xor_bytes(left, right):
"perform bitwise-xor of two byte-strings" "perform bitwise-xor of two byte-strings"
return _bjoin(chr(ord(l) ^ ord(r)) for l, r in zip(left, right)) return _bjoin(bchr(bord(l) ^ bord(r)) for l, r in zip(left, right))
def _pbkdf2(password, salt, rounds, keylen): def _pbkdf2(password, salt, rounds, keylen):
digest_size = 20 # sha1 generates 20-byte blocks digest_size = 20 # sha1 generates 20-byte blocks
total_blocks = int((keylen+digest_size-1)/digest_size) total_blocks = int((keylen+digest_size-1)/digest_size)
hmac_template = HMAC(password, None, sha1) hmac_template = HMAC(password, None, sha1)
out = _bempty out = _bempty
for i in xrange(1, total_blocks+1): for i in range(1, total_blocks+1):
hmac = hmac_template.copy() hmac = hmac_template.copy()
hmac.update(salt + pack(">L",i)) hmac.update(salt + pack(">L", i))
block = tmp = hmac.digest() block = tmp = hmac.digest()
for j in xrange(rounds-1): for _j in range(rounds-1):
hmac = hmac_template.copy() hmac = hmac_template.copy()
hmac.update(tmp) hmac.update(tmp)
tmp = hmac.digest() tmp = hmac.digest()
#TODO: need to speed up this call # TODO: need to speed up this call
block = xor_bytes(block, tmp) block = xor_bytes(block, tmp)
out += block out += block
return out[:keylen] return out[:keylen]
def ssha(password, salt): def ssha(password, salt):
''' Make ssha digest from password and salt. ''' Make ssha digest from password and salt.
Based on code of Roberto Aguilar <roberto@baremetal.io> Based on code of Roberto Aguilar <roberto@baremetal.io>
https://gist.github.com/rca/7217540 https://gist.github.com/rca/7217540
''' '''
shaval = sha1(password) shaval = sha1(password) # nosec
shaval.update( salt ) shaval.update(salt)
ssha_digest = b64encode( '{}{}'.format(shaval.digest(), salt) ).strip() ssha_digest = b2s(b64encode(shaval.digest() + salt).strip())
return ssha_digest return ssha_digest
def pbkdf2(password, salt, rounds, keylen): def pbkdf2(password, salt, rounds, keylen):
"""pkcs#5 password-based key derivation v2.0 """pkcs#5 password-based key derivation v2.0
:arg password: passphrase to use to generate key (if unicode, converted to u :arg password: passphrase to use to generate key (if unicode,
tf-8) converted to utf-8)
:arg salt: salt string to use when generating key (if unicode, converted to :arg salt: salt bytes to use when generating key
utf-8)
:param rounds: number of rounds to use to generate key :param rounds: number of rounds to use to generate key
:arg keylen: number of bytes to generate :arg keylen: number of bytes to generate
If M2Crypto is present, uses it's implementation as backend. If hashlib supports pbkdf2, uses it's implementation as backend.
:returns: :returns:
raw bytes of generated key raw bytes of generated key
""" """
if isinstance(password, unicode): password = s2b(us2s(password))
password = password.encode("utf-8")
if isinstance(salt, unicode):
salt = salt.encode("utf-8")
if keylen > 40: if keylen > 40:
#NOTE: pbkdf2 allows up to (2**31-1)*20 bytes, # NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
# but m2crypto has issues on some platforms above 40, # but m2crypto has issues on some platforms above 40,
# and such sizes aren't needed for a password hash anyways... # and such sizes aren't needed for a password hash anyways...
raise ValueError, "key length too large" raise ValueError("key length too large")
if rounds < 1: if rounds < 1:
raise ValueError, "rounds must be positive number" raise ValueError("rounds must be positive number")
return _pbkdf2(password, salt, rounds, keylen) return _pbkdf2(password, salt, rounds, keylen)
class PasswordValueError(ValueError): class PasswordValueError(ValueError):
""" The password value is not valid """ """ The password value is not valid """
pass pass
def pbkdf2_unpack(pbkdf2): def pbkdf2_unpack(pbkdf2):
""" unpack pbkdf2 encrypted password into parts, """ unpack pbkdf2 encrypted password into parts,
assume it has format "{rounds}${salt}${digest} assume it has format "{rounds}${salt}${digest}
""" """
if isinstance(pbkdf2, unicode): pbkdf2 = us2s(pbkdf2)
pbkdf2 = pbkdf2.encode("ascii")
try: try:
rounds, salt, digest = pbkdf2.split("$") rounds, salt, digest = pbkdf2.split("$")
except ValueError: except ValueError:
raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separato raise PasswordValueError("invalid PBKDF2 hash (wrong number of "
rs)" "separators)")
if rounds.startswith("0"): if rounds.startswith("0"):
raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)" raise PasswordValueError("invalid PBKDF2 hash (zero-padded rounds)")
try: try:
rounds = int(rounds) rounds = int(rounds)
except ValueError: except ValueError:
raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)" raise PasswordValueError("invalid PBKDF2 hash (invalid rounds)")
raw_salt = h64decode(salt) raw_salt = h64decode(salt)
return rounds, salt, raw_salt, digest return rounds, salt, raw_salt, digest
def encodePassword(plaintext, scheme, other=None, config=None): def encodePassword(plaintext, scheme, other=None, config=None):
"""Encrypt the plaintext password. """Encrypt the plaintext password.
""" """
if plaintext is None: if plaintext is None:
plaintext = "" plaintext = ""
if scheme == "PBKDF2": if scheme == "PBKDF2":
if other: if other:
rounds, salt, raw_salt, digest = pbkdf2_unpack(other) rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
else: else:
raw_salt = getrandbytes(20) raw_salt = random_.token_bytes(20)
salt = h64encode(raw_salt) salt = h64encode(raw_salt)
if config: if config:
rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
else: else:
rounds = 10000 rounds = 10000
if rounds < 1000: if rounds < 1000:
raise PasswordValueError, "invalid PBKDF2 hash (rounds too low)" raise PasswordValueError("invalid PBKDF2 hash (rounds too low)")
raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20) raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20)
return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest)) return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest))
elif scheme == 'SSHA': elif scheme == 'SSHA':
if other: if other:
raw_other = b64decode(other) raw_other = b64decode(other)
salt = raw_other[20:] salt = raw_other[20:]
else: else:
#new password # new password
# variable salt length # variable salt length
salt_len = random.randrange(36, 52) salt_len = random_.randbelow(52-36) + 36
salt = os.urandom(salt_len) salt = random_.token_bytes(salt_len)
s = ssha(plaintext, salt) s = ssha(s2b(plaintext), salt)
elif scheme == 'SHA': elif scheme == 'SHA':
s = sha1(plaintext).hexdigest() s = sha1(s2b(plaintext)).hexdigest() # nosec
elif scheme == 'MD5': elif scheme == 'MD5':
s = md5(plaintext).hexdigest() s = md5(s2b(plaintext)).hexdigest() # nosec
elif scheme == 'crypt' and crypt is not None: elif scheme == 'crypt' and crypt is not None:
if other is not None: if other is not None:
salt = other salt = other
else: else:
saltchars = './0123456789'+string.letters saltchars = './0123456789'+string.ascii_letters
salt = random.choice(saltchars) + random.choice(saltchars) salt = random_.choice(saltchars) + random_.choice(saltchars)
s = crypt.crypt(plaintext, salt) s = crypt.crypt(plaintext, salt)
elif scheme == 'plaintext': elif scheme == 'plaintext':
s = plaintext s = plaintext
else: else:
raise PasswordValueError, 'Unknown encryption scheme %r'%scheme raise PasswordValueError('Unknown encryption scheme %r' % scheme)
return s return s
def generatePassword(length=12): def generatePassword(length=12):
chars = string.letters+string.digits chars = string.ascii_letters+string.digits
password = [random.choice(chars) for x in range(length)] password = [random_.choice(chars) for x in range(length - 1)]
# make sure there is at least one digit # make sure there is at least one digit
password[0] = random.choice(string.digits) digitidx = random_.randbelow(length)
random.shuffle(password) password[digitidx:digitidx] = [random_.choice(string.digits)]
return ''.join(password) return ''.join(password)
class JournalPassword: class JournalPassword:
""" Password dummy instance intended for journal operation. """ Password dummy instance intended for journal operation.
We do not store passwords in the journal any longer. The dummy We do not store passwords in the journal any longer. The dummy
version only reads the encryption scheme from the given version only reads the encryption scheme from the given
encrypted password. encrypted password.
""" """
default_scheme = 'PBKDF2' # new encryptions use this scheme default_scheme = 'PBKDF2' # new encryptions use this scheme
pwre = re.compile(r'{(\w+)}(.+)') pwre = re.compile(r'{(\w+)}(.+)')
def __init__ (self, encrypted=''): def __init__(self, encrypted=''):
if isinstance(encrypted, self.__class__): if isinstance(encrypted, self.__class__):
self.scheme = encrypted.scheme or self.default_scheme self.scheme = encrypted.scheme or self.default_scheme
else: else:
m = self.pwre.match(encrypted) m = self.pwre.match(encrypted)
if m: if m:
self.scheme = m.group(1) self.scheme = m.group(1)
else: else:
self.scheme = self.default_scheme self.scheme = self.default_scheme
self.password = '' self.password = ''
def dummystr(self): def dummystr(self):
""" return dummy string to store in journal """ return dummy string to store in journal
- reports scheme, but nothing else - reports scheme, but nothing else
""" """
return "{%s}*encrypted*" % (self.scheme,) return "{%s}*encrypted*" % (self.scheme,)
__str__ = dummystr __str__ = dummystr
def __cmp__(self, other): def __eq__(self, other):
"""Compare this password against another password.""" """Compare this password against another password."""
# check to see if we're comparing instances # check to see if we're comparing instances
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
if self.scheme != other.scheme: if self.scheme != other.scheme:
return cmp(self.scheme, other.scheme) return False
return cmp(self.password, other.password) return self.password == other.password
# assume password is plaintext # assume password is plaintext
if self.password is None: if self.password is None:
raise ValueError, 'Password not set' raise ValueError('Password not set')
return cmp(self.password, encodePassword(other, self.scheme, return self.password == encodePassword(other, self.scheme,
self.password or None)) self.password or None)
def __ne__(self, other):
return not self.__eq__(other)
class Password(JournalPassword): class Password(JournalPassword):
"""The class encapsulates a Password property type value in the database. """The class encapsulates a Password property type value in the database.
The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'. The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
The encodePassword function is used to actually encode the password from The encodePassword function is used to actually encode the password from
plaintext. The None encoding is used in legacy databases where no plaintext. The None encoding is used in legacy databases where no
encoding scheme is identified. encoding scheme is identified.
The scheme is stored with the encoded data in the database: The scheme is stored with the encoded data in the database:
skipping to change at line 262 skipping to change at line 282
>>> p = Password('sekrit') >>> p = Password('sekrit')
>>> p == 'sekrit' >>> p == 'sekrit'
1 1
>>> p != 'not sekrit' >>> p != 'not sekrit'
1 1
>>> 'sekrit' == p >>> 'sekrit' == p
1 1
>>> 'not sekrit' != p >>> 'not sekrit' != p
1 1
""" """
#TODO: code to migrate from old password schemes. # TODO: code to migrate from old password schemes.
deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"] deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"]
known_schemes = ["PBKDF2", "SSHA"] + deprecated_schemes known_schemes = ["PBKDF2", "SSHA"] + deprecated_schemes
def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False def __init__(self, plaintext=None, scheme=None, encrypted=None,
, config=None): strict=False, config=None):
"""Call setPassword if plaintext is not None.""" """Call setPassword if plaintext is not None."""
if scheme is None: if scheme is None:
scheme = self.default_scheme scheme = self.default_scheme
if plaintext is not None: if plaintext is not None:
self.setPassword (plaintext, scheme, config=config) self.setPassword(plaintext, scheme, config=config)
elif encrypted is not None: elif encrypted is not None:
self.unpack(encrypted, scheme, strict=strict, config=config) self.unpack(encrypted, scheme, strict=strict, config=config)
else: else:
self.scheme = self.default_scheme self.scheme = self.default_scheme
self.password = None self.password = None
self.plaintext = None self.plaintext = None
def __repr__(self):
return self.__str__()
def needs_migration(self): def needs_migration(self):
""" Password has insecure scheme or other insecure parameters """ Password has insecure scheme or other insecure parameters
and needs migration to new password scheme and needs migration to new password scheme
""" """
if self.scheme in self.deprecated_schemes: if self.scheme in self.deprecated_schemes:
return True return True
rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password) rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password)
if rounds < 1000: if rounds < 1000:
return True return True
return False return False
skipping to change at line 304 skipping to change at line 328
""" """
m = self.pwre.match(encrypted) m = self.pwre.match(encrypted)
if m: if m:
self.scheme = m.group(1) self.scheme = m.group(1)
self.password = m.group(2) self.password = m.group(2)
self.plaintext = None self.plaintext = None
else: else:
# currently plaintext - encrypt # currently plaintext - encrypt
self.setPassword(encrypted, scheme, config=config) self.setPassword(encrypted, scheme, config=config)
if strict and self.scheme not in self.known_schemes: if strict and self.scheme not in self.known_schemes:
raise PasswordValueError, "Unknown encryption scheme: %r" % (self.sc raise PasswordValueError("Unknown encryption scheme: %r" %
heme,) (self.scheme,))
def setPassword(self, plaintext, scheme=None, config=None): def setPassword(self, plaintext, scheme=None, config=None):
"""Sets encrypts plaintext.""" """Sets encrypts plaintext."""
if scheme is None: if scheme is None:
scheme = self.default_scheme scheme = self.default_scheme
self.scheme = scheme self.scheme = scheme
self.password = encodePassword(plaintext, scheme, config=config) self.password = encodePassword(plaintext, scheme, config=config)
self.plaintext = plaintext self.plaintext = plaintext
def __str__(self): def __str__(self):
"""Stringify the encrypted password for database storage.""" """Stringify the encrypted password for database storage."""
if self.password is None: if self.password is None:
raise ValueError, 'Password not set' raise ValueError('Password not set')
return '{%s}%s'%(self.scheme, self.password) return '{%s}%s' % (self.scheme, self.password)
def test(): def test():
# SHA # SHA
p = Password('sekrit') p = Password('sekrit')
assert Password(encrypted=str(p)) == 'sekrit'
assert 'sekrit' == Password(encrypted=str(p))
assert p == 'sekrit' assert p == 'sekrit'
assert p != 'not sekrit' assert p != 'not sekrit'
assert 'sekrit' == p assert 'sekrit' == p
assert 'not sekrit' != p assert 'not sekrit' != p
# MD5 # MD5
p = Password('sekrit', 'MD5') p = Password('sekrit', 'MD5')
assert Password(encrypted=str(p)) == 'sekrit'
assert 'sekrit' == Password(encrypted=str(p))
assert p == 'sekrit' assert p == 'sekrit'
assert p != 'not sekrit' assert p != 'not sekrit'
assert 'sekrit' == p assert 'sekrit' == p
assert 'not sekrit' != p assert 'not sekrit' != p
# crypt # crypt
if crypt: # not available on Windows if crypt: # not available on Windows
p = Password('sekrit', 'crypt') p = Password('sekrit', 'crypt')
assert Password(encrypted=str(p)) == 'sekrit'
assert 'sekrit' == Password(encrypted=str(p))
assert p == 'sekrit' assert p == 'sekrit'
assert p != 'not sekrit' assert p != 'not sekrit'
assert 'sekrit' == p assert 'sekrit' == p
assert 'not sekrit' != p assert 'not sekrit' != p
# SSHA # SSHA
p = Password('sekrit', 'SSHA') p = Password('sekrit', 'SSHA')
assert Password(encrypted=str(p)) == 'sekrit'
assert 'sekrit' == Password(encrypted=str(p))
assert p == 'sekrit' assert p == 'sekrit'
assert p != 'not sekrit' assert p != 'not sekrit'
assert 'sekrit' == p assert 'sekrit' == p
assert 'not sekrit' != p assert 'not sekrit' != p
# PBKDF2 - low level function # PBKDF2 - low level function
from binascii import unhexlify from binascii import unhexlify
k = pbkdf2("password", "ATHENA.MIT.EDUraeburn", 1200, 32) k = pbkdf2("password", b"ATHENA.MIT.EDUraeburn", 1200, 32)
assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a3 1e2e62b1e13") assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a3 1e2e62b1e13")
# PBKDF2 - hash function # PBKDF2 - hash function
h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE" h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
assert encodePassword("sekrit", "PBKDF2", h) == h assert encodePassword("sekrit", "PBKDF2", h) == h
# PBKDF2 - high level integration # PBKDF2 - high level integration
p = Password('sekrit', 'PBKDF2') p = Password('sekrit', 'PBKDF2')
assert Password(encrypted=str(p)) == 'sekrit'
assert 'sekrit' == Password(encrypted=str(p))
assert p == 'sekrit' assert p == 'sekrit'
assert p != 'not sekrit' assert p != 'not sekrit'
assert 'sekrit' == p assert 'sekrit' == p
assert 'not sekrit' != p assert 'not sekrit' != p
if __name__ == '__main__': if __name__ == '__main__':
test() test()
# vim: set filetype=python sts=4 sw=4 et si : # vim: set filetype=python sts=4 sw=4 et si :
 End of changes. 55 change blocks. 
70 lines changed or deleted 100 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)