Commit 2b2c691a authored by Eli Collins's avatar Eli Collins

made all CryptAlgorithm methods classmethods instead of instance methods

parent c41d7b034578
...@@ -14,15 +14,6 @@ import os ...@@ -14,15 +14,6 @@ import os
#libs #libs
from passlib.util import classproperty, abstractmethod, is_seq, srandom from passlib.util import classproperty, abstractmethod, is_seq, srandom
try:
#try stdlib module, which is only present under posix
from crypt import crypt as unix_crypt
#XXX: might want to reconcile our implementation's behavior
# with the posix behavior so error types/messages and limitations
# are reliable
except ImportError:
from passlib._unix_crypt import crypt as unix_crypt
try: try:
#try importing py-bcrypt, it's much faster #try importing py-bcrypt, it's much faster
import bcrypt import bcrypt
...@@ -57,6 +48,7 @@ __all__ = [ ...@@ -57,6 +48,7 @@ __all__ = [
#========================================================= #=========================================================
#common helper funcs for passwords #common helper funcs for passwords
#========================================================= #=========================================================
#charmap for "hash64" encoding #charmap for "hash64" encoding
#most unix hash algorithms use this mapping (though bcrypt put it's numerals at the end) #most unix hash algorithms use this mapping (though bcrypt put it's numerals at the end)
CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
...@@ -185,13 +177,13 @@ def h64_gen_salt(size, pad=False): ...@@ -185,13 +177,13 @@ def h64_gen_salt(size, pad=False):
out += "=" * (-size % 4) out += "=" * (-size % 4)
return out return out
class UnixHash(object): class HashInfo(object):
"helper used by various hash algorithm utilities" "helper used by various CryptAlgorithms to store parsed hash information"
alg = None alg = None #name or alias identifying algorithm
salt = None salt = None #salt portion of hash
chk = None chk = None #checksum (result of hashing salt & password according to alg)
rounds = None rounds = None #number of rounds, if known & applicable
source = None source = None #source above information was parsed from, if available
def __init__(self, alg, salt, chk=None, rounds=None, source=None): def __init__(self, alg, salt, chk=None, rounds=None, source=None):
self.alg = alg self.alg = alg
...@@ -445,6 +437,7 @@ class CryptAlgorithm(object): ...@@ -445,6 +437,7 @@ class CryptAlgorithm(object):
False False
""" """
@classmethod
def verify(self, secret, hash): def verify(self, secret, hash):
"""verify a secret against an existing hash. """verify a secret against an existing hash.
...@@ -518,11 +511,13 @@ class Mysql10Crypt(CryptAlgorithm): ...@@ -518,11 +511,13 @@ class Mysql10Crypt(CryptAlgorithm):
_pat = re.compile(r"^[0-9a-f]{16}$", re.I) _pat = re.compile(r"^[0-9a-f]{16}$", re.I)
@classmethod
def identify(self, hash): def identify(self, hash):
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
@classmethod
def encrypt(self, secret, hash=None, keep_salt=False): def encrypt(self, secret, hash=None, keep_salt=False):
nr1 = 1345345333 nr1 = 1345345333
nr2 = 0x12345671 nr2 = 0x12345671
...@@ -536,6 +531,7 @@ class Mysql10Crypt(CryptAlgorithm): ...@@ -536,6 +531,7 @@ class Mysql10Crypt(CryptAlgorithm):
add = (add+tmp) & 0xffffffff add = (add+tmp) & 0xffffffff
return "%08x%08x" % (nr1 & 0x7fffffff, nr2 & 0x7fffffff) return "%08x%08x" % (nr1 & 0x7fffffff, nr2 & 0x7fffffff)
@classmethod
def verify(self, secret, hash): def verify(self, secret, hash):
if hash is None: if hash is None:
return False return False
...@@ -556,14 +552,17 @@ class Mysql41Crypt(CryptAlgorithm): ...@@ -556,14 +552,17 @@ class Mysql41Crypt(CryptAlgorithm):
_pat = re.compile(r"^\*[0-9A-F]{40}$", re.I) _pat = re.compile(r"^\*[0-9A-F]{40}$", re.I)
@classmethod
def identify(self, hash): def identify(self, hash):
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
@classmethod
def encrypt(self, secret, hash=None, keep_salt=False): def encrypt(self, secret, hash=None, keep_salt=False):
return '*' + hashlib.sha1(hashlib.sha1(secret).digest()).hexdigest().upper() return '*' + hashlib.sha1(hashlib.sha1(secret).digest()).hexdigest().upper()
@classmethod
def verify(self, secret, hash): def verify(self, secret, hash):
if hash is None: if hash is None:
return False return False
...@@ -584,7 +583,7 @@ class PostgresMd5Crypt(CryptAlgorithm): ...@@ -584,7 +583,7 @@ class PostgresMd5Crypt(CryptAlgorithm):
Usage Example:: Usage Example::
>>> from passlib import hash >>> from passlib import hash
>>> crypt = hash.PostgresMd5Crypt() >>> crypt = hash.PostgresMd5Crypt()
>>> crypt.encrypt("mypass", user="postgres") >>> crypt.encrypt("mypass", user="postgres")
'md55fba2ea04fd36069d2574ea71c8efe9d' 'md55fba2ea04fd36069d2574ea71c8efe9d'
...@@ -597,11 +596,13 @@ class PostgresMd5Crypt(CryptAlgorithm): ...@@ -597,11 +596,13 @@ class PostgresMd5Crypt(CryptAlgorithm):
_pat = re.compile(r"^md5[0-9a-f]{32}$") _pat = re.compile(r"^md5[0-9a-f]{32}$")
@classmethod
def identify(self, hash): def identify(self, hash):
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
@classmethod
def encrypt(self, secret, hash=None, keep_salt=False, user=None): def encrypt(self, secret, hash=None, keep_salt=False, user=None):
if isinstance(secret, tuple): if isinstance(secret, tuple):
if user: if user:
...@@ -611,6 +612,7 @@ class PostgresMd5Crypt(CryptAlgorithm): ...@@ -611,6 +612,7 @@ class PostgresMd5Crypt(CryptAlgorithm):
raise ValueError, "user keyword must be specified for this algorithm" raise ValueError, "user keyword must be specified for this algorithm"
return "md5" + hashlib.md5(secret + user).hexdigest().lower() return "md5" + hashlib.md5(secret + user).hexdigest().lower()
@classmethod
def verify(self, secret, hash, user=None): def verify(self, secret, hash, user=None):
if hash is None: if hash is None:
return False return False
...@@ -619,6 +621,15 @@ class PostgresMd5Crypt(CryptAlgorithm): ...@@ -619,6 +621,15 @@ class PostgresMd5Crypt(CryptAlgorithm):
#========================================================= #=========================================================
#old unix crypt #old unix crypt
#========================================================= #=========================================================
try:
#try stdlib module, which is only present under posix
from crypt import crypt as unix_crypt
except ImportError:
#TODO: need to reconcile our implementation's behavior
# with the stdlib's behavior so error types, messages, and limitations
# are the same. (eg: handling of None and unicode chars)
from passlib._unix_crypt import crypt as unix_crypt
class UnixCrypt(CryptAlgorithm): class UnixCrypt(CryptAlgorithm):
"""Old Unix-Crypt Algorithm, as originally used on unix before md5-crypt arrived. """Old Unix-Crypt Algorithm, as originally used on unix before md5-crypt arrived.
This implementation uses the builtin ``crypt`` module when available, This implementation uses the builtin ``crypt`` module when available,
...@@ -630,17 +641,20 @@ class UnixCrypt(CryptAlgorithm): ...@@ -630,17 +641,20 @@ class UnixCrypt(CryptAlgorithm):
has_rounds = False has_rounds = False
secret_chars = 8 secret_chars = 8
#FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
_pat = re.compile(r""" _pat = re.compile(r"""
^ ^
(?P<salt>[./a-z0-9]{2}) (?P<salt>[./a-z0-9]{2})
(?P<hash>[./a-z0-9]{11}) (?P<hash>[./a-z0-9]{11})
$""", re.X|re.I) $""", re.X|re.I)
@classmethod
def identify(self, hash): def identify(self, hash):
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
@classmethod
def encrypt(self, secret, hash=None, keep_salt=False): def encrypt(self, secret, hash=None, keep_salt=False):
if hash and keep_salt: if hash and keep_salt:
salt = hash[:2] salt = hash[:2]
...@@ -667,6 +681,7 @@ class Md5Crypt(CryptAlgorithm): ...@@ -667,6 +681,7 @@ class Md5Crypt(CryptAlgorithm):
hash_bits = 96 hash_bits = 96
has_rounds = False has_rounds = False
@classmethod
def _md5_crypt_raw(self, secret, salt): def _md5_crypt_raw(self, secret, salt):
#init salt #init salt
if not salt: if not salt:
...@@ -724,7 +739,7 @@ class Md5Crypt(CryptAlgorithm): ...@@ -724,7 +739,7 @@ class Md5Crypt(CryptAlgorithm):
) )
for idx in xrange(5) for idx in xrange(5)
) + _enc64b1(hash, 11) ) + _enc64b1(hash, 11)
return UnixHash('1', salt, out) return HashInfo('1', salt, out)
_pat = re.compile(r""" _pat = re.compile(r"""
^ ^
...@@ -734,34 +749,38 @@ class Md5Crypt(CryptAlgorithm): ...@@ -734,34 +749,38 @@ class Md5Crypt(CryptAlgorithm):
$ $
""", re.X) """, re.X)
@classmethod
def identify(self, hash): def identify(self, hash):
"identify md5-crypt hash" "identify md5-crypt hash"
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
def parse(self, hash): @classmethod
def _parse(self, hash):
"parse an md5-crypt hash" "parse an md5-crypt hash"
m = self._pat.match(hash) m = self._pat.match(hash)
if not m: if not m:
raise ValueError, "invalid md5 salt" raise ValueError, "invalid md5 salt"
return UnixHash(m.group("alg"), m.group("salt"), m.group("chk")) return HashInfo(m.group("alg"), m.group("salt"), m.group("chk"))
@classmethod
def encrypt(self, secret, salt=None, keep_salt=False): def encrypt(self, secret, salt=None, keep_salt=False):
"encrypt an md5-crypt hash" "encrypt an md5-crypt hash"
real_salt = None real_salt = None
if salt: if salt:
rec = self.parse(salt) rec = self._parse(salt)
if keep_salt: if keep_salt:
real_salt = rec.salt real_salt = rec.salt
rec = self._md5_crypt_raw(secret, real_salt) rec = self._md5_crypt_raw(secret, real_salt)
return "$1$%s$%s" % (rec.salt, rec.chk) return "$1$%s$%s" % (rec.salt, rec.chk)
@classmethod
def verify(self, secret, hash): def verify(self, secret, hash):
"verify an md5-crypt hash" "verify an md5-crypt hash"
if hash is None: if hash is None:
return False return False
rec = self.parse(hash) rec = self._parse(hash)
other = self._md5_crypt_raw(secret, rec.salt) other = self._md5_crypt_raw(secret, rec.salt)
return other.chk == rec.chk return other.chk == rec.chk
...@@ -802,6 +821,7 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -802,6 +821,7 @@ class _ShaCrypt(CryptAlgorithm):
#========================================================= #=========================================================
#core sha crypt algorithm #core sha crypt algorithm
#========================================================= #=========================================================
@classmethod
def _sha_crypt_raw(self, rounds, salt, secret): def _sha_crypt_raw(self, rounds, salt, secret):
"perform sha crypt, returning just the checksum" "perform sha crypt, returning just the checksum"
#setup alg-specific parameters #setup alg-specific parameters
...@@ -886,8 +906,9 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -886,8 +906,9 @@ class _ShaCrypt(CryptAlgorithm):
#encode result using 256/512 specific func #encode result using 256/512 specific func
out = self._encode(last_result) out = self._encode(last_result)
assert len(out) == self._hash_size, "wrong length: %r" % (out,) assert len(out) == self._hash_size, "wrong length: %r" % (out,)
return UnixHash(self._key, salt, out, rounds=rounds) return HashInfo(self._key, salt, out, rounds=rounds)
@classmethod
def _sha_crypt(self, rounds, salt, secret): def _sha_crypt(self, rounds, salt, secret):
rec = self._sha_crypt_raw(rounds, salt, secret) rec = self._sha_crypt_raw(rounds, salt, secret)
if rec.rounds == -1: if rec.rounds == -1:
...@@ -898,13 +919,15 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -898,13 +919,15 @@ class _ShaCrypt(CryptAlgorithm):
#========================================================= #=========================================================
#frontend helpers #frontend helpers
#========================================================= #=========================================================
@classmethod
def identify(self, hash): def identify(self, hash):
"identify bcrypt hash" "identify bcrypt hash"
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
def parse(self, hash): @classmethod
def _parse(self, hash):
"parse bcrypt hash" "parse bcrypt hash"
m = self._pat.match(hash) m = self._pat.match(hash)
if not m: if not m:
...@@ -915,8 +938,9 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -915,8 +938,9 @@ class _ShaCrypt(CryptAlgorithm):
else: else:
rounds = int(rounds) rounds = int(rounds)
assert alg == self._key assert alg == self._key
return UnixHash(alg, salt, chk, rounds=rounds, source=hash) return HashInfo(alg, salt, chk, rounds=rounds, source=hash)
@classmethod
def encrypt(self, secret, hash=None, rounds=None, keep_salt=False): def encrypt(self, secret, hash=None, rounds=None, keep_salt=False):
"""encrypt using sha256/512-crypt. """encrypt using sha256/512-crypt.
...@@ -933,7 +957,7 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -933,7 +957,7 @@ class _ShaCrypt(CryptAlgorithm):
""" """
salt = None salt = None
if hash: if hash:
rec = self.parse(hash) rec = self._parse(hash)
if keep_salt: if keep_salt:
salt = rec.salt salt = rec.salt
if rounds is None: if rounds is None:
...@@ -941,6 +965,7 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -941,6 +965,7 @@ class _ShaCrypt(CryptAlgorithm):
rounds = self._norm_rounds(rounds) rounds = self._norm_rounds(rounds)
return self._sha_crypt(rounds, salt, secret) return self._sha_crypt(rounds, salt, secret)
@classmethod
def _norm_rounds(self, rounds): def _norm_rounds(self, rounds):
if isinstance(rounds, int): if isinstance(rounds, int):
return rounds return rounds
...@@ -953,10 +978,11 @@ class _ShaCrypt(CryptAlgorithm): ...@@ -953,10 +978,11 @@ class _ShaCrypt(CryptAlgorithm):
log.warning("unknown rounds alias %r, using 'medium'", rounds) log.warning("unknown rounds alias %r, using 'medium'", rounds)
return self.medium_rounds return self.medium_rounds
@classmethod
def verify(self, secret, hash): def verify(self, secret, hash):
if hash is None: if hash is None:
return False return False
rec = self.parse(hash) rec = self._parse(hash)
other = self._sha_crypt_raw(rec.rounds, rec.salt, secret) other = self._sha_crypt_raw(rec.rounds, rec.salt, secret)
return other.chk == rec.chk return other.chk == rec.chk
...@@ -985,6 +1011,7 @@ class Sha256Crypt(_ShaCrypt): ...@@ -985,6 +1011,7 @@ class Sha256Crypt(_ShaCrypt):
_chunk_size = 32 _chunk_size = 32
_hash_size = 43 _hash_size = 43
@classmethod
def _encode(self, result): def _encode(self, result):
out = '' out = ''
a, b, c = [0, 10, 20] a, b, c = [0, 10, 20]
...@@ -1056,6 +1083,7 @@ class Sha512Crypt(_ShaCrypt): ...@@ -1056,6 +1083,7 @@ class Sha512Crypt(_ShaCrypt):
_chunk_size = 64 _chunk_size = 64
_hash_size = 86 _hash_size = 86
@classmethod
def _encode(self, result): def _encode(self, result):
out = '' out = ''
a, b, c = [0, 21, 42] a, b, c = [0, 21, 42]
...@@ -1123,20 +1151,23 @@ class BCrypt(CryptAlgorithm): ...@@ -1123,20 +1151,23 @@ class BCrypt(CryptAlgorithm):
$ $
""", re.X) """, re.X)
@classmethod
def identify(self, hash): def identify(self, hash):
"identify bcrypt hash" "identify bcrypt hash"
if hash is None: if hash is None:
return False return False
return self._pat.match(hash) is not None return self._pat.match(hash) is not None
def parse(self, hash): @classmethod
def _parse(self, hash):
"parse bcrypt hash" "parse bcrypt hash"
m = self._pat.match(hash) m = self._pat.match(hash)
if not m: if not m:
raise ValueError, "invalid bcrypt hash/salt" raise ValueError, "invalid bcrypt hash/salt"
alg, rounds, salt, chk = m.group("alg", "rounds", "salt", "chk") alg, rounds, salt, chk = m.group("alg", "rounds", "salt", "chk")
return UnixHash(alg, salt, chk, rounds=int(rounds), source=hash) return HashInfo(alg, salt, chk, rounds=int(rounds), source=hash)
@classmethod
def encrypt(self, secret, hash=None, keep_salt=False, rounds=None): def encrypt(self, secret, hash=None, keep_salt=False, rounds=None):
"""encrypt using bcrypt. """encrypt using bcrypt.
...@@ -1154,7 +1185,7 @@ class BCrypt(CryptAlgorithm): ...@@ -1154,7 +1185,7 @@ class BCrypt(CryptAlgorithm):
""" """
#validate salt #validate salt
if hash: if hash:
rec = self.parse(hash) rec = self._parse(hash)
if rounds is None: if rounds is None:
rounds = rec.rounds rounds = rec.rounds
#generate new salt #generate new salt
...@@ -1166,6 +1197,7 @@ class BCrypt(CryptAlgorithm): ...@@ -1166,6 +1197,7 @@ class BCrypt(CryptAlgorithm):
#encrypt secret #encrypt secret
return bcrypt.hashpw(secret, salt) return bcrypt.hashpw(secret, salt)
@classmethod
def _norm_rounds(self, rounds): def _norm_rounds(self, rounds):
if isinstance(rounds, int): if isinstance(rounds, int):
return rounds return rounds
...@@ -1178,6 +1210,7 @@ class BCrypt(CryptAlgorithm): ...@@ -1178,6 +1210,7 @@ class BCrypt(CryptAlgorithm):
log.warning("unknown rounds alias %r, using 'medium'", rounds) log.warning("unknown rounds alias %r, using 'medium'", rounds)
return self.medium_rounds return self.medium_rounds
@classmethod
def verify(self, secret, hash): def verify(self, secret, hash):
"verify bcrypt hash" "verify bcrypt hash"
return bcrypt.hashpw(secret, hash) == hash return bcrypt.hashpw(secret, hash) == hash
...@@ -1395,7 +1428,7 @@ class CryptContext(list): ...@@ -1395,7 +1428,7 @@ class CryptContext(list):
#========================================================= #=========================================================
#TODO: recode default to be explicitly settable, not just using first one. #TODO: recode default to be explicitly settable, not just using first one.
#TODO: simplify interface as much as possible. #TODO: simplify interface as much as possible.
def resolve(self, name=None, default=None): def resolve(self, name=None, default=None):
"""given an algorithm name, return CryptAlgorithm instance which manages it. """given an algorithm name, return CryptAlgorithm instance which manages it.
if no match is found, returns None. if no match is found, returns None.
...@@ -1551,7 +1584,7 @@ def identify(hash, resolve=False): ...@@ -1551,7 +1584,7 @@ def identify(hash, resolve=False):
=================== ================================================ =================== ================================================
Name Description Name Description
------------------- ------------------------------------------------ ------------------- ------------------------------------------------
``"unix-crypt"`` the historic unix-crypt algorithm ``"unix-crypt"`` the historical unix-crypt algorithm
``"md5-crypt"`` the md5-crypt algorithm, usually identified ``"md5-crypt"`` the md5-crypt algorithm, usually identified
by the prefix ``$1$`` in unix shadow files. by the prefix ``$1$`` in unix shadow files.
......
...@@ -23,6 +23,7 @@ class UnsaltedAlg(pwhash.CryptAlgorithm): ...@@ -23,6 +23,7 @@ class UnsaltedAlg(pwhash.CryptAlgorithm):
name = "unsalted" name = "unsalted"
salt_bits = 0 salt_bits = 0
@classmethod
def identify(self, hash): def identify(self, hash):
if hash is None: if hash is None:
return False return False
...@@ -34,6 +35,7 @@ class UnsaltedAlg(pwhash.CryptAlgorithm): ...@@ -34,6 +35,7 @@ class UnsaltedAlg(pwhash.CryptAlgorithm):
return False return False
return True return True
@classmethod
def encrypt(self, secret, salt=None, keep_salt=False): def encrypt(self, secret, salt=None, keep_salt=False):
#NOTE: that salt / keep_salted are simply ignored #NOTE: that salt / keep_salted are simply ignored
return hashlib.sha1("boblious" + secret).hexdigest() return hashlib.sha1("boblious" + secret).hexdigest()
...@@ -45,19 +47,23 @@ class SaltedAlg(pwhash.CryptAlgorithm): ...@@ -45,19 +47,23 @@ class SaltedAlg(pwhash.CryptAlgorithm):
name = "salted" name = "salted"
salt_bits = 6*2 salt_bits = 6*2
@classmethod
def identify(self, hash): def identify(self, hash):
if hash is None: if hash is None:
return False return False
return hash.startswith("@salt") return hash.startswith("@salt")
@classmethod
def _raw(self, secret, salt): def _raw(self, secret, salt):
return "@salt%s%s" % (salt, hashlib.sha1(salt+secret).hexdigest()) return "@salt%s%s" % (salt, hashlib.sha1(salt+secret).hexdigest())
@classmethod
def encrypt(self, secret, salt=None, keep_salt=False): def encrypt(self, secret, salt=None, keep_salt=False):
## warn("keep_salt not supported by this algorithm") ## warn("keep_salt not supported by this algorithm")
real_salt = pwhash.h64_gen_salt(2) real_salt = pwhash.h64_gen_salt(2)
return self._raw(secret, real_salt)