summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-06-17 23:54:39 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-06-17 23:54:39 -0400
commit0ad4f023487be628c7d163aa1e2c7775d3d94b5f (patch)
treea232ac60dba19107ee7f8a55e313911e9f218efc
parent6b2b7178a81af7c3f1a8574b04dfec42d0e05fba (diff)
downloadpasslib-0ad4f023487be628c7d163aa1e2c7775d3d94b5f.tar.gz
converted remaining handlers to py3 compat (all unix crypt hashes)
-rw-r--r--passlib/handlers/bcrypt.py62
-rw-r--r--passlib/handlers/des_crypt.py119
-rw-r--r--passlib/handlers/nthash.py32
-rw-r--r--passlib/handlers/sha1_crypt.py32
-rw-r--r--passlib/handlers/sha2_crypt.py125
-rw-r--r--passlib/handlers/sun_md5_crypt.py64
-rw-r--r--passlib/tests/test_drivers.py53
7 files changed, 285 insertions, 202 deletions
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index ae0ab1f..89fd05e 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -25,7 +25,7 @@ try:
except ImportError: #pragma: no cover - though should run whole suite w/o bcryptor installed
bcryptor_engine = None
#libs
-from passlib.utils import os_crypt, classproperty, handlers as uh, h64
+from passlib.utils import safe_os_crypt, classproperty, handlers as uh, h64, to_hash_str
#pkg
#local
@@ -76,9 +76,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
checksum_size = 31
#--HasManyIdents--
- default_ident = "$2a$"
- ident_values = ("$2$", "$2a$")
- ident_aliases = {"2":"$2$", "2a": "$2a$"}
+ default_ident = u"$2a$"
+ ident_values = (u"$2$", u"$2a$")
+ ident_aliases = {u"2": u"$2$", u"2a": u"$2a$"}
#--HasSalt--
min_salt_size = max_salt_size = 22
@@ -98,16 +98,16 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
for ident in cls.ident_values:
if hash.startswith(ident):
break
else:
raise ValueError("invalid bcrypt hash")
- rounds, data = hash[len(ident):].split("$")
+ rounds, data = hash[len(ident):].split(u"$")
rval = int(rounds)
- if rounds != '%02d' % (rval,):
+ if rounds != u'%02d' % (rval,):
raise ValueError("invalid bcrypt hash (no rounds padding)")
salt, chk = data[:22], data[22:]
return cls(
@@ -118,8 +118,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
strict=bool(chk),
)
- def to_string(self):
- return "%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum or '')
+ def to_string(self, native=True):
+ hash = u"%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
#=========================================================
#primary interface
@@ -137,13 +138,13 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
@classproperty
def _has_backend_os_crypt(cls):
return (
- os_crypt is not None
+ safe_os_crypt is not None
and
- os_crypt("test", "$2a$04$......................") ==
- '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
+ safe_os_crypt(u"test", u"$2a$04$......................")[1] ==
+ u'$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
and
- os_crypt("test", "$2$04$......................") ==
- '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
+ safe_os_crypt(u"test", u"$2$04$......................")[1] ==
+ u'$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
)
@classmethod
@@ -151,19 +152,38 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
return "no BCrypt backends available - please install pybcrypt or bcryptor for BCrypt support"
def _calc_checksum_os_crypt(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return os_crypt(secret, self.to_string())[-31:]
+ ok, hash = safe_os_crypt(secret, self.to_string(native=False))
+ if ok:
+ return hash[-31:]
+ else:
+ #NOTE: not checking backends since this is lowest priority,
+ # so they probably aren't available either
+ raise ValueError("encoded password can't be handled by os_crypt"
+ " (recommend installing pybcrypt or bcryptor)")
def _calc_checksum_pybcrypt(self, secret):
+ #pybcrypt behavior:
+ # py2: unicode secret -> ascii bytes (we override this)
+ # unicode hash -> ascii bytes (we provide ascii bytes)
+ # returns ascii bytes
+ # py3: can't get to install
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- return pybcrypt_hashpw(secret, self.to_string())[-31:]
+ hash = pybcrypt_hashpw(secret,
+ self.to_string(native=False))
+ return hash[-31:].decode("ascii")
def _calc_checksum_bcryptor(self, secret):
+ #bcryptor behavior:
+ # py2: unicode secret -> ascii bytes (we have to override)
+ # unicode hash -> ascii bytes (we provide ascii bytes)
+ # returns ascii bytes
+ # py3: can't get to install
if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return bcryptor_engine(False).hash_key(secret, self.to_string())[-31:]
+ secret = secret.encode("utf-8")
+ hash = bcryptor_engine(False).hash_key(secret,
+ self.to_string(native=False))
+ return hash[-31:].decode("ascii")
#=========================================================
#eoc
diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py
index 616e330..d916128 100644
--- a/passlib/handlers/des_crypt.py
+++ b/passlib/handlers/des_crypt.py
@@ -58,7 +58,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import h64, classproperty, os_crypt, handlers as uh
+from passlib.utils import h64, classproperty, safe_os_crypt, b, bytes, \
+ to_hash_str, handlers as uh, bord
from passlib.utils.des import mdes_encrypt_int_block
#pkg
#local
@@ -75,7 +76,7 @@ __all__ = [
def _crypt_secret_to_key(secret):
"crypt helper which converts lower 7 bits of first 8 chars of secret -> 56-bit des key, padded to 64 bits"
return sum(
- (ord(c) & 0x7f) << (57-8*i)
+ (bord(c) & 0x7f) << (57-8*i)
for i, c in enumerate(secret[:8])
)
@@ -111,7 +112,7 @@ def raw_ext_crypt(secret, rounds, salt):
raise ValueError("invalid salt")
#validate secret
- if '\x00' in secret: #pragma: no cover - always caught by class
+ if b('\x00') in secret: #pragma: no cover - always caught by class
#builtin linux crypt doesn't like this, so we don't either
#XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt
raise ValueError("secret must be string without null bytes")
@@ -171,7 +172,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
#=========================================================
#FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
- _pat = re.compile(r"""
+ _pat = re.compile(ur"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{11})?
@@ -179,19 +180,20 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
@classmethod
def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
+ return uh.identify_regexp(hash, cls._pat)
@classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
salt, chk = hash[:2], hash[2:]
return cls(salt=salt, checksum=chk, strict=bool(chk))
- def to_string(self):
- return "%s%s" % (self.salt, self.checksum or '')
+ def to_string(self, native=True):
+ hash = u"%s%s" % (self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
#=========================================================
#backend
@@ -202,25 +204,30 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
@classproperty
def _has_backend_os_crypt(cls):
- return os_crypt is not None and os_crypt("test", "ab") == 'abgOeLfPimXQo'
+ return safe_os_crypt and safe_os_crypt(u"test", u"ab")[1] == u'abgOeLfPimXQo'
def _calc_checksum_builtin(self, secret):
- #forbidding nul chars because linux crypt (and most C implementations) won't accept it either.
- if '\x00' in secret:
- raise ValueError("null char in secret")
#gotta do something - no official policy since des-crypt predates unicode
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- return raw_crypt(secret, self.salt)
+ #forbidding nul chars because linux crypt (and most C implementations) won't accept it either.
+ if b('\x00') in secret:
+ raise ValueError("null char in secret")
+ return raw_crypt(secret, self.salt.encode("ascii")).decode("ascii")
def _calc_checksum_os_crypt(self, secret):
#os_crypt() would raise less useful error
- if '\x00' in secret:
+ null = u'\x00' if isinstance(secret, unicode) else b('\x00')
+ if null in secret:
raise ValueError("null char in secret")
- #gotta do something - no official policy since des-crypt predates unicode
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return os_crypt(secret, self.salt)[2:]
+
+ #NOTE: safe_os_crypt encodes unicode secret -> utf8
+ #no official policy since des-crypt predates unicode
+ ok, hash = safe_os_crypt(secret, self.salt)
+ if ok:
+ return hash[2:]
+ else:
+ return self._calc_checksum_builtin(secret)
#=========================================================
#eoc
@@ -270,7 +277,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
salt_chars = uh.H64_CHARS
#--HasRounds--
- default_rounds = 5000
+ default_rounds = 5001
min_rounds = 0
max_rounds = 16777215 # (1<<24)-1
rounds_cost = "linear"
@@ -281,7 +288,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
#=========================================================
#internal helpers
#=========================================================
- _pat = re.compile(r"""
+ _pat = re.compile(ur"""
^
_
(?P<rounds>[./a-z0-9]{4})
@@ -291,27 +298,29 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
@classmethod
def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
+ return uh.identify_regexp(hash, cls._pat)
@classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
m = cls._pat.match(hash)
if not m:
raise ValueError("invalid ext-des-crypt hash")
rounds, salt, chk = m.group("rounds", "salt", "chk")
return cls(
- rounds=h64.decode_int24(rounds),
+ rounds=h64.decode_int24(rounds.encode("ascii")),
salt=salt,
checksum=chk,
strict=bool(chk),
)
- def to_string(self):
- return "_%s%s%s" % (h64.encode_int24(self.rounds), self.salt, self.checksum or '')
+ def to_string(self, native=True):
+ hash = u"_%s%s%s" % (h64.encode_int24(self.rounds).decode("ascii"),
+ self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
#=========================================================
#backend
@@ -322,18 +331,20 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
@classproperty
def _has_backend_os_crypt(cls):
- h = '_/...lLDAxARksGCHin.'
- return os_crypt is not None and os_crypt("test", h) == h
+ h = u'_/...lLDAxARksGCHin.'
+ return safe_os_crypt and safe_os_crypt(u"test", h)[1] == h
def _calc_checksum_builtin(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- return raw_ext_crypt(secret, self.rounds, self.salt)
+ return raw_ext_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
def _calc_checksum_os_crypt(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return os_crypt(secret, self.to_string())[9:]
+ ok, hash = safe_os_crypt(secret, self.salt)
+ if ok:
+ return hash[9:]
+ else:
+ return self._calc_checksum_builtin(secret)
#=========================================================
#eoc
@@ -370,7 +381,7 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
#=========================================================
#internal helpers
#=========================================================
- _pat = re.compile(r"""
+ _pat = re.compile(ur"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{11,})?
@@ -378,22 +389,30 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
@classmethod
def identify(cls, hash):
- return bool(hash and cls._pat.match(hash)) and (len(hash)-2) % 11 == 0
+ if not hash:
+ return False
+ if isinstance(hash, bytes):
+ try:
+ hash = hash.decode("ascii")
+ except UnicodeDecodeError:
+ return False
+ return bool(cls._pat.match(hash)) and (len(hash)-2) % 11 == 0
@classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
m = cls._pat.match(hash)
if not m:
raise ValueError("invalid bigcrypt hash")
salt, chk = m.group("salt", "chk")
return cls(salt=salt, checksum=chk, strict=bool(chk))
- def to_string(self):
- return "%s%s" % (self.salt, self.checksum or '')
+ def to_string(self, native=True):
+ hash = u"%s%s" % (self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
@classmethod
def norm_checksum(cls, value, strict=False):
@@ -410,14 +429,14 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
def calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- chk = raw_crypt(secret, self.salt)
+ chk = raw_crypt(secret, self.salt.encode("ascii"))
idx = 8
end = len(secret)
while idx < end:
next = idx + 8
chk += raw_crypt(secret[idx:next], chk[-11:-9])
idx = next
- return chk
+ return chk.decode("ascii")
#=========================================================
#eoc
@@ -453,7 +472,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
#=========================================================
#internal helpers
#=========================================================
- _pat = re.compile(r"""
+ _pat = re.compile(ur"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{22})?
@@ -461,22 +480,23 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
@classmethod
def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
+ return uh.identify_regexp(hash, cls._pat)
@classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
m = cls._pat.match(hash)
if not m:
raise ValueError("invalid crypt16 hash")
salt, chk = m.group("salt", "chk")
return cls(salt=salt, checksum=chk, strict=bool(chk))
- def to_string(self):
- return "%s%s" % (self.salt, self.checksum or '')
+ def to_string(self, native=True):
+ hash = u"%s%s" % (self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
#=========================================================
#backend
@@ -489,7 +509,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
#parse salt value
try:
- salt_value = h64.decode_int12(self.salt)
+ salt_value = h64.decode_int12(self.salt.encode("ascii"))
except ValueError: #pragma: no cover - caught by class
raise ValueError("invalid chars in salt")
@@ -506,7 +526,8 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
result2 = mdes_encrypt_int_block(key2, 0, salt_value, 5)
#done
- return h64.encode_dc_int64(result1) + h64.encode_dc_int64(result2)
+ chk = h64.encode_dc_int64(result1) + h64.encode_dc_int64(result2)
+ return chk.decode("ascii")
#=========================================================
#eoc
diff --git a/passlib/handlers/nthash.py b/passlib/handlers/nthash.py
index de06025..33d6f25 100644
--- a/passlib/handlers/nthash.py
+++ b/passlib/handlers/nthash.py
@@ -8,7 +8,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import handlers as uh
+from passlib.utils import handlers as uh, to_unicode, to_hash_str, to_bytes
from passlib.utils.md4 import md4
#pkg
#local
@@ -41,12 +41,12 @@ class nthash(uh.HasManyIdents, uh.GenericHandler):
setting_kwds = ("ident",)
checksum_chars = uh.LC_HEX_CHARS
- _stub_checksum = "0" * 32
+ _stub_checksum = u"0" * 32
#--HasManyIdents--
- default_ident = "$3$$"
- ident_values = ("$3$$", "$NT$")
- ident_aliases = {"3": "$3$$", "NT": "$NT$"}
+ default_ident = u"$3$$"
+ ident_values = (u"$3$$", u"$NT$")
+ ident_aliases = {u"3": u"$3$$", u"NT": u"$NT$"}
#=========================================================
#formatting
@@ -56,8 +56,8 @@ class nthash(uh.HasManyIdents, uh.GenericHandler):
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
for ident in cls.ident_values:
if hash.startswith(ident):
break
@@ -67,22 +67,30 @@ class nthash(uh.HasManyIdents, uh.GenericHandler):
return cls(ident=ident, checksum=chk, strict=True)
def to_string(self):
- return self.ident + (self.checksum or self._stub_checksum)
+ hash = self.ident + (self.checksum or self._stub_checksum)
+ return to_hash_str(hash)
#=========================================================
#primary interface
#=========================================================
def calc_checksum(self, secret):
- if secret is None:
- raise TypeError("secret must be a string")
return self.raw_nthash(secret, hex=True)
@staticmethod
def raw_nthash(secret, hex=False):
- "encode password using md4-based NTHASH algorithm; returns string of raw bytes"
+ """encode password using md4-based NTHASH algorithm
+
+ :returns:
+ returns string of raw bytes if ``hex=False``,
+ returns digest as hexidecimal unicode if ``hex=True``.
+ """
+ secret = to_unicode(secret, "utf-8")
hash = md4(secret.encode("utf-16le"))
- return hash.hexdigest() if hex else hash.digest()
+ if hex:
+ return to_unicode(hash.hexdigest(), 'ascii')
+ else:
+ return hash.digest()
#=========================================================
#eoc
diff --git a/passlib/handlers/sha1_crypt.py b/passlib/handlers/sha1_crypt.py
index 6ad4e02..8273000 100644
--- a/passlib/handlers/sha1_crypt.py
+++ b/passlib/handlers/sha1_crypt.py
@@ -13,7 +13,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import h64, handlers as uh, os_crypt, classproperty
+from passlib.utils import h64, handlers as uh, safe_os_crypt, classproperty, \
+ to_hash_str, to_unicode, bytes, b
from passlib.utils.pbkdf2 import hmac_sha1
#pkg
#local
@@ -56,7 +57,7 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
#--GenericHandler--
name = "sha1_crypt"
setting_kwds = ("salt", "salt_size", "rounds")
- ident = "$sha1$"
+ ident = u"$sha1$"
checksum_size = 28
#--HasSalt--
@@ -87,11 +88,11 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
strict=bool(chk),
)
- def to_string(self):
- out = "$sha1$%d$%s" % (self.rounds, self.salt)
+ def to_string(self, native=True):
+ out = u"$sha1$%d$%s" % (self.rounds, self.salt)
if self.checksum:
- out += "$" + self.checksum
- return out
+ out += u"$" + self.checksum
+ return to_hash_str(out) if native else out
#=========================================================
#backend
@@ -102,19 +103,21 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
@classproperty
def _has_backend_os_crypt(cls):
- h = '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHimExLaiSFlGkAe'
- return os_crypt is not None and os_crypt("test", h) == h
+ h = u'$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHimExLaiSFlGkAe'
+ return safe_os_crypt and safe_os_crypt(u"test", h)[1] == h
def _calc_checksum_builtin(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
rounds = self.rounds
- result = "%s$sha1$%s" % (self.salt, rounds)
+ #NOTE: this uses a different format than the hash...
+ result = u"%s$sha1$%s" % (self.salt, rounds)
+ result = result.encode("ascii")
r = 0
while r < rounds:
result = hmac_sha1(secret, result)
r += 1
- return h64.encode_transposed_bytes(result, self._chk_offsets)
+ return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii")
_chk_offsets = [
2,1,0,
@@ -127,10 +130,11 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
]
def _calc_checksum_os_crypt(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- h = os_crypt(secret, self.to_string())
- return h[h.rindex("$")+1:]
+ ok, hash = safe_os_crypt(secret, self.to_string(native=False))
+ if ok:
+ return hash[hash.rindex("$")+1:]
+ else:
+ return self._calc_checksum_builtin(secret)
#=========================================================
#eoc
diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py
index 56e72e9..58effd8 100644
--- a/passlib/handlers/sha2_crypt.py
+++ b/passlib/handlers/sha2_crypt.py
@@ -9,7 +9,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import h64, os_crypt, classproperty, handlers as uh
+from passlib.utils import h64, safe_os_crypt, classproperty, handlers as uh, \
+ to_hash_str, to_unicode, bytes, b, bord
#pkg
#local
__all__ = [
@@ -20,6 +21,8 @@ __all__ = [
#=========================================================
#pure-python backend (shared between sha256-crypt & sha512-crypt)
#=========================================================
+INVALID_SALT_VALUES = b("\x00$")
+
def raw_sha_crypt(secret, salt, rounds, hash):
"""perform raw sha crypt
@@ -33,8 +36,8 @@ def raw_sha_crypt(secret, salt, rounds, hash):
"""
#validate secret
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
+ if not isinstance(secret, bytes):
+ raise TypeError("secret must be encoded as bytes")
#validate rounds
if rounds < 1000:
@@ -43,7 +46,9 @@ def raw_sha_crypt(secret, salt, rounds, hash):
rounds = 999999999
#validate salt
- if any(c in salt for c in '\x00$'):
+ if not isinstance(salt, bytes):
+ raise TypeError("salt must be encoded as bytes")
+ if any(c in salt for c in INVALID_SALT_VALUES):
raise ValueError("invalid chars in salt")
if len(salt) > 16:
salt = salt[:16]
@@ -86,7 +91,7 @@ def raw_sha_crypt(secret, salt, rounds, hash):
dp_result = extend(dp.digest(), secret)
#calc DS - hash of salt, extended to size of salt
- ds = hash(salt * (16+ord(a_result[0])))
+ ds = hash(salt * (16+bord(a_result[0])))
ds_result = extend(ds.digest(), salt) #aka 'S'
#
@@ -242,7 +247,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#--GenericHandler--
name = "sha256_crypt"
setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")
- ident = "$5$"
+ ident = u"$5$"
#--HasSalt--
min_salt_size = 0
@@ -270,7 +275,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#=========================================================
#: regexp used to parse hashes
- _pat = re.compile(r"""
+ _pat = re.compile(ur"""
^
\$5
(\$rounds=(?P<rounds>\d+))?
@@ -289,13 +294,13 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
m = cls._pat.match(hash)
if not m:
raise ValueError("invalid sha256-crypt hash")
rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
- if rounds and rounds.startswith("0"):
+ if rounds and rounds.startswith(u"0"):
raise ValueError("invalid sha256-crypt hash (zero-padded rounds)")
return cls(
implicit_rounds = not rounds,
@@ -305,11 +310,12 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
strict=bool(chk),
)
- def to_string(self):
+ def to_string(self, native=True):
if self.rounds == 5000 and self.implicit_rounds:
- return "$5$%s$%s" % (self.salt, self.checksum or '')
+ hash = u"$5$%s$%s" % (self.salt, self.checksum or u'')
else:
- return "$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '')
+ hash = u"$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
#=========================================================
#backend
@@ -321,27 +327,34 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
@classproperty
def _has_backend_os_crypt(cls):
return bool(
- os_crypt is not None and
- os_crypt("test", "$5$rounds=1000$test") ==
- "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/"
+ safe_os_crypt and
+ safe_os_crypt(u"test", u"$5$rounds=1000$test")[1] ==
+ u"$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/"
)
def _calc_checksum_builtin(self, secret):
- checksum, salt, rounds = raw_sha256_crypt(secret, self.salt, self.rounds)
- assert salt == self.salt, "class doesn't agree w/ builtin backend"
- assert rounds == self.rounds, "class doesn't agree w/ builtin backend"
- return checksum
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ checksum, salt, rounds = raw_sha256_crypt(secret,
+ self.salt.encode("ascii"),
+ self.rounds)
+ assert salt == self.salt.encode("ascii"), \
+ "class doesn't agree w/ builtin backend: salt %r != %r" % (salt, self.salt.encode("ascii"))
+ assert rounds == self.rounds, \
+ "class doesn't agree w/ builtin backend: rounds %r != %r" % (rounds, self.rounds)
+ return checksum.decode("ascii")
def _calc_checksum_os_crypt(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- #NOTE: avoiding full parsing routine via from_string().checksum,
- # and just extracting the bit we need.
- result = os_crypt(secret, self.to_string())
- assert result.startswith("$5$")
- chk = result[-43:]
- assert '$' not in chk
- return chk
+ ok, result = safe_os_crypt(secret, self.to_string(native=False))
+ if ok:
+ #NOTE: avoiding full parsing routine via from_string().checksum,
+ # and just extracting the bit we need.
+ assert result.startswith(u"$5$")
+ chk = result[-43:]
+ assert u'$' not in chk
+ return chk
+ else:
+ return self._calc_checksum_builtin(secret)
#=========================================================
#eoc
@@ -386,7 +399,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#algorithm information
#=========================================================
name = "sha512_crypt"
- ident = "$6$"
+ ident = u"$6$"
setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")
@@ -414,7 +427,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#=========================================================
#: regexp used to parse hashes
- _pat = re.compile(r"""
+ _pat = re.compile(ur"""
^
\$6
(\$rounds=(?P<rounds>\d+))?
@@ -435,8 +448,8 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
m = cls._pat.match(hash)
if not m:
raise ValueError("invalid sha512-crypt hash")
@@ -451,11 +464,12 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
strict=bool(chk),
)
- def to_string(self):
+ def to_string(self, native=True):
if self.rounds == 5000 and self.implicit_rounds:
- return "$6$%s$%s" % (self.salt, self.checksum or '')
+ hash = u"$6$%s$%s" % (self.salt, self.checksum or u'')
else:
- return "$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '')
+ hash = u"$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or u'')
+ return to_hash_str(hash) if native else hash
#=========================================================
#backend
@@ -467,29 +481,36 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
@classproperty
def _has_backend_os_crypt(cls):
return bool(
- os_crypt is not None and
- os_crypt("test", "$6$rounds=1000$test") ==
- "$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0"
+ safe_os_crypt and
+ safe_os_crypt(u"test", u"$6$rounds=1000$test")[1] ==
+ u"$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0"
)
#NOTE: testing w/ HashTimer shows 64-bit linux's crypt to be ~2.6x faster than builtin (627253 vs 238152 rounds/sec)
def _calc_checksum_builtin(self, secret):
- checksum, salt, rounds = raw_sha512_crypt(secret, self.salt, self.rounds)
- assert salt == self.salt, "class doesn't agree w/ builtin backend"
- assert rounds == self.rounds, "class doesn't agree w/ builtin backend"
- return checksum
-
- def _calc_checksum_os_crypt(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
- #NOTE: avoiding full parsing routine via from_string().checksum,
- # and just extracting the bit we need.
- result = os_crypt(secret, self.to_string())
- assert result.startswith("$6$")
- chk = result[-86:]
- assert '$' not in chk
- return chk
+ checksum, salt, rounds = raw_sha512_crypt(secret,
+ self.salt.encode("ascii"),
+ self.rounds)
+ assert salt == self.salt.encode("ascii"), \
+ "class doesn't agree w/ builtin backend: salt %r != %r" % (salt, self.salt.encode("ascii"))
+ assert rounds == self.rounds, \
+ "class doesn't agree w/ builtin backend: rounds %r != %r" % (rounds, self.rounds)
+ return checksum.decode("ascii")
+
+ def _calc_checksum_os_crypt(self, secret):
+ ok, result = safe_os_crypt(secret, self.to_string(native=False))
+ if ok:
+ #NOTE: avoiding full parsing routine via from_string().checksum,
+ # and just extracting the bit we need.
+ assert result.startswith(u"$6$")
+ chk = result[-86:]
+ assert u'$' not in chk
+ return chk
+ else:
+ return self._calc_checksum_builtin(secret)
#=========================================================
#eoc
diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py
index 4306b5c..9bcb9f7 100644
--- a/passlib/handlers/sun_md5_crypt.py
+++ b/passlib/handlers/sun_md5_crypt.py
@@ -17,7 +17,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import h64, handlers as uh
+from passlib.utils import h64, handlers as uh, to_hash_str, to_unicode, bytes, b, bord
#pkg
#local
__all__ = [
@@ -31,7 +31,7 @@ __all__ = [
# exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt
# from Project Gutenberg.
-MAGIC_HAMLET = (
+MAGIC_HAMLET = b(
"To be, or not to be,--that is the question:--\n"
"Whether 'tis nobler in the mind to suffer\n"
"The slings and arrows of outrageous fortune\n"
@@ -82,12 +82,8 @@ del xr
def raw_sun_md5_crypt(secret, rounds, salt):
"given secret & salt, return encoded sun-md5-crypt checksum"
global MAGIC_HAMLET
-
- #validate secret
- #FIXME: no definitive information about how it handles unicode,
- # so using this as a fallback...
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
+ assert isinstance(secret, bytes)
+ assert isinstance(salt, bytes)
#validate rounds
if rounds <= 0:
@@ -117,7 +113,7 @@ def raw_sun_md5_crypt(secret, rounds, salt):
round = 0
while round < real_rounds:
#convert last result byte string to list of byte-ints for easy access
- rval = [ ord(c) for c in result ].__getitem__
+ rval = [ bord(c) for c in result ].__getitem__
#build up X bit by bit
x = 0
@@ -144,7 +140,7 @@ def raw_sun_md5_crypt(secret, rounds, salt):
h = md5(result)
if coin:
h.update(MAGIC_HAMLET)
- h.update(str(round))
+ h.update(unicode(round).encode("ascii"))
result = h.digest()
round += 1
@@ -233,25 +229,32 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#=========================================================
@classmethod
def identify(cls, hash):
- return bool(hash) and (hash.startswith("$md5$") or hash.startswith("$md5,"))
+ if not hash:
+ return False
+ if isinstance(hash, bytes):
+ try:
+ hash = hash.decode("ascii")
+ except UnicodeDecodeError:
+ return False
+ return hash.startswith(u"$md5$") or hash.startswith(u"$md5,")
@classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
- if isinstance(hash, unicode):
- hash = hash.encode("ascii")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
#
#detect if hash specifies rounds value.
#if so, parse and validate it.
#by end, set 'rounds' to int value, and 'tail' containing salt+chk
#
- if hash.startswith("$md5$"):
+ if hash.startswith(u"$md5$"):
rounds = 0
salt_idx = 5
- elif hash.startswith("$md5,rounds="):
- idx = hash.find("$", 12)
+ elif hash.startswith(u"$md5,rounds="):
+ idx = hash.find(u"$", 12)
if idx == -1:
raise ValueError("invalid sun-md5-crypt hash (unexpected end of rounds)")
rstr = hash[12:idx]
@@ -259,7 +262,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
rounds = int(rstr)
except ValueError:
raise ValueError("invalid sun-md5-crypt hash (bad rounds)")
- if rstr != str(rounds):
+ if rstr != unicode(rounds):
raise ValueError("invalid sun-md5-crypt hash (zero-padded rounds)")
if rounds == 0:
#NOTE: not sure if this is *forbidden* precisely,
@@ -275,20 +278,20 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#to deal cleanly with some backward-compatible workarounds
#implemented by original implementation.
#
- chk_idx = hash.rfind("$", salt_idx)
+ chk_idx = hash.rfind(u"$", salt_idx)
if chk_idx == -1:
# ''-config for $-hash
salt = hash[salt_idx:]
chk = None
bare_salt = True
elif chk_idx == len(hash)-1:
- if chk_idx > salt_idx and hash[-2] == "$":
+ if chk_idx > salt_idx and hash[-2] == u"$":
raise ValueError("invalid sun-md5-crypt hash (too many $)")
# $-config for $$-hash
salt = hash[salt_idx:-1]
chk = None
bare_salt = False
- elif chk_idx > 0 and hash[chk_idx-1] == "$":
+ elif chk_idx > 0 and hash[chk_idx-1] == u"$":
# $$-hash
salt = hash[salt_idx:chk_idx-1]
chk = hash[chk_idx+1:]
@@ -307,18 +310,18 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
strict=bool(chk),
)
- def to_string(self, withchk=True):
- ss = '' if self.bare_salt else '$'
+ def to_string(self, withchk=True, native=True):
+ ss = u'' if self.bare_salt else u'$'
rounds = self.rounds
if rounds > 0:
- out = "$md5,rounds=%d$%s%s" % (rounds, self.salt, ss)
+ out = u"$md5,rounds=%d$%s%s" % (rounds, self.salt, ss)
else:
- out = "$md5$%s%s" % (self.salt, ss)
+ out = u"$md5$%s%s" % (self.salt, ss)
if withchk:
chk = self.checksum
if chk:
- out = "%s$%s" % (out, chk)
- return out
+ out = u"%s$%s" % (out, chk)
+ return to_hash_str(out) if native else out
#=========================================================
#primary interface
@@ -329,8 +332,13 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
# especially, when using ''-config, make sure to append '$x' to string.
def calc_checksum(self, secret):
- config = self.to_string(withchk=False)
- return raw_sun_md5_crypt(secret, self.rounds, config)
+ #NOTE: no reference for how sun_md5_crypt handles unicode
+ if secret is None:
+ raise TypeError("no secret specified")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ config = self.to_string(withchk=False,native=False).encode("ascii")
+ return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii")
#=========================================================
#eoc
diff --git a/passlib/tests/test_drivers.py b/passlib/tests/test_drivers.py
index 23c2e96..e40a783 100644
--- a/passlib/tests/test_drivers.py
+++ b/passlib/tests/test_drivers.py
@@ -36,17 +36,18 @@ class AprMd5CryptTest(HandlerCase):
from passlib.handlers.bcrypt import bcrypt
class BCryptTest(HandlerCase):
+
handler = bcrypt
secret_chars = 72
- known_correct_hashes = (
+ known_correct_hashes = [
#selected bcrypt test vectors
('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'),
('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'),
('abcdefghijklmnopqrstuvwxyz', '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'),
- )
+ ]
known_unidentified_hashes = [
#unsupported minor version
@@ -73,20 +74,20 @@ class BCryptTest(HandlerCase):
kwds['ident'] = 'Q'
self.assertRaises(ValueError, handler, **kwds)
- #this method is added in order to maximize test coverage on systems
- #where os_crypt is missing or doesn't support bcrypt
- if enable_option("cover") and not bcrypt.has_backend("os_crypt") and bcrypt.has_backend("pybcrypt"):
- def test_backend(self):
- from passlib.handlers import bcrypt as bcrypt_mod
- orig = bcrypt_mod.os_crypt
- bcrypt_mod.os_crypt = bcrypt_mod.pybcrypt_hashpw
- orig = bcrypt.get_backend()
- try:
- bcrypt.set_backend("os_crypt")
- bcrypt.encrypt(u"test", rounds=4)
- finally:
- bcrypt.set_backend(orig)
- bcrypt_mod.os_crypt = orig
+ ###this method is added in order to maximize test coverage on systems
+ ###where os_crypt is missing or doesn't support bcrypt
+ ##if enable_option("cover") and not bcrypt.has_backend("os_crypt") and bcrypt.has_backend("pybcrypt"):
+ ## def test_backend(self):
+ ## from passlib.handlers import bcrypt as bcrypt_mod
+ ## orig = bcrypt_mod.safe_os_crypt
+ ## bcrypt_mod.safe_os_crypt = bcrypt_mod.pybcrypt_hashpw
+ ## orig = bcrypt.get_backend()
+ ## try:
+ ## bcrypt.set_backend("os_crypt")
+ ## bcrypt.encrypt(u"test", rounds=4)
+ ## finally:
+ ## bcrypt.set_backend(orig)
+ ## bcrypt_mod.os_crypt = orig
bcrypt._no_backends_msg() #call this for coverage purposes
@@ -174,7 +175,7 @@ class DesCryptTest(HandlerCase):
handler = des_crypt
secret_chars = 8
- known_correct_hashes = (
+ known_correct_hashes = [
#secret, example hash which matches secret
('', 'OgAwTx2l6NADI'),
(' ', '/Hk.VPuwQTXbc'),
@@ -183,7 +184,7 @@ class DesCryptTest(HandlerCase):
('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'),
('AlOtBsOl', 'cEpWz5IUCShqM'),
(u'hell\u00D6', 'saykDgk3BPZ9E'),
- )
+ ]
known_unidentified_hashes = [
#bad char in otherwise correctly formatted hash
'!gAwTx2l6NADI',
@@ -423,10 +424,10 @@ from passlib.handlers.nthash import nthash
class NTHashTest(HandlerCase):
handler = nthash
- known_correct_hashes = (
+ known_correct_hashes = [
('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'),
('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'),
- )
+ ]
known_malformed_hashes = [
#bad char in otherwise correct hash
@@ -740,10 +741,10 @@ from passlib.handlers.sha1_crypt import sha1_crypt
class SHA1CryptTest(HandlerCase):
handler = sha1_crypt
- known_correct_hashes = (
+ known_correct_hashes = [
("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"),
("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"),
- )
+ ]
known_malformed_hashes = [
#bad char in otherwise correct hash
@@ -852,12 +853,12 @@ class SHA256CryptTest(HandlerCase):
def test_raw(self):
#run some tests on raw backend func to ensure it works right
self.assertEqual(
- raw_sha_crypt('secret', 'salt'*10, 1, hashlib.md5),
- ('\x1f\x96\x1cO\x11\xa9h\x12\xc4\xf3\x9c\xee\xf5\x93\xf3\xdd',
- 'saltsaltsaltsalt',
+ raw_sha_crypt(b('secret'), b('salt')*10, 1, hashlib.md5),
+ (b('\x1f\x96\x1cO\x11\xa9h\x12\xc4\xf3\x9c\xee\xf5\x93\xf3\xdd'),
+ b('saltsaltsaltsalt'),
1000)
)
- self.assertRaises(ValueError, raw_sha_crypt, 'secret', '$', 1, hashlib.md5)
+ self.assertRaises(ValueError, raw_sha_crypt, b('secret'), b('$'), 1, hashlib.md5)
BuiltinSHA256CryptTest = create_backend_case(SHA256CryptTest, "builtin")