summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-01-07 07:37:39 +0000
committerEli Collins <elic@assurancetechnologies.com>2011-01-07 07:37:39 +0000
commitdf53832315758ad9dcecae91fc9a511f3cba575a (patch)
treea140a764e6c17df24afa34ea7ee6f22a0690bcb0
parentc9748b14693b2378a20088f46f44b2f56ccfda6f (diff)
downloadpasslib-df53832315758ad9dcecae91fc9a511f3cba575a.tar.gz
rearranging
=========== * moved hash.py to package * in process of rearranging so core code is in hash.base, and separate submodules exist for each of the algorithms. - sha crypt split out - unix crypt split out - similar refactor of UT files * moved hash64 encoding helpers to passlib.util.H64 class * reversed order of h64 encoding helpers so offsets match order of output * added H64 unit tests unix crypt ---------- (yes this is overkill for legacy crypt) * cleaned up builtin unix_crypt semantics * wrapping stdlib crypt to deal w/ border case errors * added better unix crypt backend test suite
-rw-r--r--passlib.komodoproject2
-rw-r--r--passlib/_unix_crypt.py35
-rw-r--r--passlib/hash/__init__.py154
-rw-r--r--passlib/hash/base.py (renamed from passlib/hash.py)630
-rw-r--r--passlib/hash/sha_crypt.py355
-rw-r--r--passlib/hash/unix_crypt.py96
-rw-r--r--passlib/tests/test_crypt_context.py601
-rw-r--r--passlib/tests/test_hash.py720
-rw-r--r--passlib/tests/test_sha_crypt.py130
-rw-r--r--passlib/tests/test_unix_crypt.py121
-rw-r--r--passlib/tests/test_util.py49
-rw-r--r--passlib/tests/utils.py18
-rw-r--r--passlib/util.py166
13 files changed, 1723 insertions, 1354 deletions
diff --git a/passlib.komodoproject b/passlib.komodoproject
index 62697df..d4e8545 100644
--- a/passlib.komodoproject
+++ b/passlib.komodoproject
@@ -3,7 +3,7 @@
<project id="ab14e891-3bad-4dee-94c1-e8ad8d6ca1a9" kpf_version="5" name="passlib.komodoproject">
<preference-set idref="ab14e891-3bad-4dee-94c1-e8ad8d6ca1a9">
<string relative="path" id="import_dirname"></string>
- <string id="import_exclude_matches">dist;build;.*;*.*~;*.bak;*.tmp;CVS;.#*;*.pyo;*.pyc;.svn;*%*;tmp*.html;.DS_Store</string>
+ <string id="import_exclude_matches">_build;dist;build;.*;*.*~;*.bak;*.tmp;CVS;.#*;*.pyo;*.pyc;.svn;*%*;tmp*.html;.DS_Store</string>
<string id="import_include_matches"></string>
<boolean id="import_live">1</boolean>
</preference-set>
diff --git a/passlib/_unix_crypt.py b/passlib/_unix_crypt.py
index 9520bd2..05898a9 100644
--- a/passlib/_unix_crypt.py
+++ b/passlib/_unix_crypt.py
@@ -44,6 +44,8 @@ which is compatible with stdlib, so it can be used as a drop-in replacement.
#simple constants
#=========================================================
+#XXX: can b64_encode / b64_decode be replaced with H64.encode/decode?
+
#base64 char sequence
CHARS = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
b64_encode = CHARS.__getitem__ # int -> char
@@ -621,10 +623,26 @@ def crypt(key, salt):
if PCXROT is None:
load_tables()
+ #parse key values
if '\x00' in key:
- #builtin linux crypt doesn't like this,
- #so we don't either
- raise ValueError, "key must not contain null characters"
+ #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, "key must be string without null bytes"
+
+ #XXX: doesn't match stdlib, but just to useful to not add in
+ if isinstance(key, unicode):
+ key = key.encode("utf-8")
+
+ #parse salt into bytes
+ if not salt or len(salt) < 2:
+ raise ValueError, "invalid salt"
+ sa, sb = salt[0:2]
+ try:
+ salt_value = (b64_decode(sb) << 6) + b64_decode(sa)
+ except KeyError:
+ raise ValueError, "invalid salt"
+ #FIXME: ^ this will throws error if bad salt chars are used
+ # whereas linux crypt does something (inexplicable) with it
#convert key string into an integer
if len(key) < 8:
@@ -637,17 +655,6 @@ def crypt(key, salt):
#convert key int -> key schedule
key_sched = des_setkey(key_value)
- #parse salt into int
- if not salt:
- sa = sb = '.' #no std behavior in this case
- elif len(salt) < 2:
- sa = sb = salt #linux's behavior, probably no real standard
- else:
- sa, sb = salt[0:2]
- salt_value = (b64_decode(sb) << 6) + b64_decode(sa)
- #FIXME: ^ this will throw a KeyError if bad salt chars are used
- # whereas linux crypt does something with it
-
#run data through des using input of 0
result = des_cipher(0, salt_value, 25, key_sched)
diff --git a/passlib/hash/__init__.py b/passlib/hash/__init__.py
new file mode 100644
index 0000000..a7b03dc
--- /dev/null
+++ b/passlib/hash/__init__.py
@@ -0,0 +1,154 @@
+from passlib.hash.base import *
+from passlib.hash.unix_crypt import *
+from passlib.hash.sha_crypt import *
+
+#=========================================================
+#build up the standard context objects
+#=========================================================
+
+#default context for quick use.. recognizes all known algorithms,
+# currently uses SHA-512 as default
+default_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt, Sha256Crypt, Sha512Crypt ])
+
+def identify(hash, resolve=False):
+ """Identify algorithm which generated a password hash.
+
+ :arg hash:
+ The hash string to identify.
+ :param resolve:
+ If ``True``, this function will return a :class:`CryptAlgorithm`
+ instance which can handle the hash.
+ If ``False`` (the default), then only the name of the hash algorithm
+ will be returned.
+
+ The following algorithms are currently recognized:
+
+ =================== ================================================
+ Name Description
+ ------------------- ------------------------------------------------
+ ``"unix-crypt"`` the historical unix-crypt algorithm
+
+ ``"md5-crypt"`` the md5-crypt algorithm, usually identified
+ by the prefix ``$1$`` in unix shadow files.
+
+ ``"bcrypt"`` the openbsd blowfish-crypt algorithm,
+ usually identified by the prefixes ``$2$`` or ``$2a$``
+ in unix shadow files.
+
+ ``"sha256-crypt"`` the 256-bit version of the sha-crypt algorithm,
+ usually identified by the prefix ``$5$``
+ in unix shadow files.
+
+ ``"sha512-crypt"`` the 512-bit version of the sha-crypt algorithm,
+ usually identified by the prefix ``$6$``
+ in unix shadow files.
+ =================== ================================================
+
+ :returns:
+ The name of the hash, or ``None`` if the hash could not be identified.
+ (The return may be altered by the *resolve* keyword).
+
+ .. note::
+ This is a convience wrapper for ``pwhash.default_context.identify(hash)``.
+ """
+ return default_context.identify(hash, resolve=resolve)
+
+def encrypt(secret, hash=None, alg=None, **kwds):
+ """Encrypt secret using a password hash algorithm.
+
+ :type secret: str
+ :arg secret:
+ String containing the secret to encrypt
+
+ :type hash: str|None
+ :arg hash:
+ Optional previously existing hash string which
+ will be used to provide default value for the salt, rounds,
+ or other algorithm-specific options.
+ If not specified, algorithm-chosen defaults will be used.
+
+ :type alg: str|None
+ :param alg:
+ Optionally specify the name of the algorithm to use.
+ If no algorithm is specified, an attempt is made
+ to guess from the hash string. If no hash string
+ is specified, sha512-crypt will be used.
+ See :func:`identify` for a list of algorithm names.
+
+ All other keywords are passed on to the specific password algorithm
+ being used to encrypt the secret.
+
+ :type keep_salt: bool
+ :param keep_salt:
+ This option is accepted by all of the builtin algorithms.
+
+ By default, a new salt value generated each time
+ a secret is encrypted. However, if this keyword
+ is set to ``True``, and a previous hash string is provided,
+ the salt from that string will be used instead.
+
+ .. note::
+ This is generally only useful when verifying an existing hash
+ (see :func:`verify`). Other than that, this option should be
+ avoided, as re-using a salt will needlessly decrease security.
+
+ :type rounds: int
+ :param rounds:
+ For the sha256-crypt and sha512-crypt algorithms,
+ this option lets you specify the number of rounds
+ of encryption to use. For the bcrypt algorithm,
+ this option lets you specify the log-base-2 of
+ the number of rounds of encryption to use.
+
+ For all three of these algorithms, you can either
+ specify a positive integer, or one of the strings
+ "fast", "medium", "slow" to choose a preset number
+ of rounds corresponding to an appropriate level
+ of encryption.
+
+ :returns:
+ The secret as encoded by the specified algorithm and options.
+ """
+ return default_context.encrypt(secret, hash=hash, alg=alg, **kwds)
+
+def verify(secret, hash, alg=None):
+ """verify a secret against an existing hash.
+
+ This checks if a secret matches against the one stored
+ inside the specified hash. By default this uses :func:`encrypt`
+ to re-crypt the secret, and compares it to the provided hash;
+ though some algorithms may implement this in a more efficient manner.
+
+ :type secret: str
+ :arg secret:
+ A string containing the secret to check.
+
+ :type hash: str
+ :param hash:
+ A string containing the hash to check against.
+
+ :type alg: str|None
+ :param alg:
+ Optionally specify the name of the algorithm to use.
+ If no algorithm is specified, an attempt is made
+ to guess from the hash string. If it can't be
+ identified, a ValueError will be raised.
+ See :func:`identify` for a list of algorithm names.
+
+ :returns:
+ ``True`` if the secret matches, otherwise ``False``.
+ """
+ return default_context.verify(secret, hash, alg=alg)
+
+#some general os-context helpers (these may not match your os policy exactly)
+linux_context = CryptContext([ UnixCrypt, Md5Crypt, Sha256Crypt, Sha512Crypt ])
+bsd_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt ])
+
+#some sql db context helpers
+mysql40_context = CryptContext([Mysql10Crypt])
+mysql_context = CryptContext([Mysql10Crypt, Mysql41Crypt])
+postgres_context = CryptContext([PostgresMd5Crypt])
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash.py b/passlib/hash/base.py
index 2a2a3eb..ad50936 100644
--- a/passlib/hash.py
+++ b/passlib/hash/base.py
@@ -12,7 +12,7 @@ import time
import os
#site
#libs
-from passlib.util import classproperty, abstractmethod, is_seq, srandom
+from passlib.util import classproperty, abstractmethod, is_seq, srandom, H64
try:
#try importing py-bcrypt, it's much faster
@@ -28,155 +28,19 @@ __all__ = [
'CryptAlgorithm',
'UnixCrypt',
'Md5Crypt',
- 'Sha256Crypt',
- 'Sha512Crypt',
'BCrypt',
+ 'Mysql10Crypt',
+ 'Mysql41Crypt',
+ 'PostgresMd5Crypt',
#crypt context
'CryptContext',
- 'default_context',
- 'linux_context',
- 'bsd_context',
-
- #quick helpers
- 'identify_secret',
- 'encrypt_secret',
- 'verify_secret',
-
]
#=========================================================
#common helper funcs for passwords
#=========================================================
-#charmap for "hash64" encoding
-#most unix hash algorithms use this mapping (though bcrypt put it's numerals at the end)
-CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-CHARIDX = dict( (c,i) for i,c in enumerate(CHARS))
-
-def _enc64(value, offset=0, num=False):
- if num:
- x, y, z = value[offset], value[offset+1], value[offset+2]
- else:
- x, y, z = ord(value[offset]), ord(value[offset+1]), ord(value[offset+2])
- #xxxxxx xxyyyy yyyyzz zzzzzz
- #aaaaaa bbbbbb cccccc dddddd
- a = (x >> 2) # x [8..3]
- b = ((x & 0x3) << 4) + (y>>4) # x[2..1] + y [8..5]
- c = ((y & 0xf) << 2) + (z>>6) #y[4..1] + d[8..7]
- d = z & 0x3f
- return CHARS[a] + CHARS[b] + CHARS[c] + CHARS[d]
-
-def _dec64(value, offset=0, num=False):
- a, b, c, d = CHARIDX[value[offset]], CHARIDX[value[offset+1]], \
- CHARIDX[value[offset+2]], CHARIDX[value[offset+3]]
- #aaaaaabb bbbbcccc ccdddddd
- #xxxxxxxx yyyyyyyy zzzzzzzz
- x = (a<<2) + (b >> 4) #a[6..1] + b[6..5]
- y = ((b & 0xf) << 4) + (c >> 2) #b[4..1] + c[6..3]
- z = ((c & 0x3) << 6) + d #c[2..1] + d[6..1]
- if num:
- return x, y, z
- return chr(x) + chr(y) + chr(z)
-
-def h64_encode(value, pad=False, num=False):
- "encode string of bytes into hash64 format"
- if num:
- value = list(value)
- #pad value to align w/ 3 byte chunks
- x = len(value) % 3
- if x == 2:
- if num:
- value += [0]
- else:
- value += "\x00"
- p = 1
- elif x == 1:
- if num:
- value += [0, 0]
- else:
- value += "\x00\x00"
- p = 2
- else:
- p = 0
- assert len(value) % 3 == 0
- out = "".join( _enc64(value, offset, num=num) for offset in xrange(0, len(value), 3))
- assert len(out) % 4 == 0
- if p:
- if pad:
- out = out[:-p] + "=" * p
- else:
- out = out[:-p]
- return out
-
-def h64_decode(value, pad=False, num=False):
- "decode string of bytes from hash64 format"
- if value.endswith("="):
- assert len(value) % 4 == 0, value
- if value.endswith('=='):
- p = 2
- value = value[:-2] + '..'
- else:
- p = 1
- value = value[:-1] + '.'
- else:
- #else add padding if needed
- x = len(value) % 4
- if x == 0:
- p = 0
- elif pad:
- raise ValueError, "size must be multiple of 4"
- elif x == 3:
- p = 1
- value += "."
- elif x == 2:
- p = 2
- value += ".."
- elif x == 1:
- p = 3
- value += "..."
- assert len(value) % 4 == 0, value
- if num:
- out = []
- for offset in xrange(0, len(value), 4):
- out.extend(_dec64(value, offset, num=True))
- else:
- out = "".join( _dec64(value, offset) for offset in xrange(0, len(value), 4))
- assert len(out) % 3 == 0
- if p: #strip out garbage chars
- out = out[:-p]
- return out
-
-def _enc64b(a, b, c, n=4):
- "std hash64 bit encoding"
- v = (ord(a) << 16) + (ord(b) << 8) + ord(c)
- return "".join(
- CHARS[(v >> (i*6)) & 0x3F]
- for i in range(n)
- )
-
-def _enc64b1(buffer, a):
- "do 64bit encode of single element of a buffer"
- return _enc64b('\x00', '\x00', buffer[a], 2)
-
-def _enc64b2(buffer, a, b):
- "do 64bit encode of 2 elements of a buffer"
- return _enc64b('\x00', buffer[a], buffer[b], 3)
-
-def _enc64b3(buffer, a, b, c):
- "do 64bit encode of 3 elements of a buffer"
- return _enc64b(buffer[a], buffer[b], buffer[c], 4)
-
-def h64_gen_salt(size, pad=False):
- "generate hash64 salt of arbitrary length"
- out = ''.join(
- srandom.choice(CHARS)
- for idx in xrange(size)
- )
- if pad and size % 4:
- out += "=" * (-size % 4)
- return out
-
class HashInfo(object):
"helper used by various CryptAlgorithms to store parsed hash information"
alg = None #name or alias identifying algorithm
@@ -659,7 +523,7 @@ class UnixCrypt(CryptAlgorithm):
if hash and keep_salt:
salt = hash[:2]
else:
- salt = h64_gen_salt(2)
+ salt = H64.randstr(2)
return unix_crypt(secret, salt)
#default verify used
@@ -685,7 +549,7 @@ class Md5Crypt(CryptAlgorithm):
def _md5_crypt_raw(self, secret, salt):
#init salt
if not salt:
- salt = h64_gen_salt(8)
+ salt = H64.randstr(8)
assert len(salt) == 8
h = hashlib.md5()
@@ -732,13 +596,13 @@ class Md5Crypt(CryptAlgorithm):
hash = h.digest()
out = ''.join(
- _enc64b3(hash,
- idx,
- idx+6,
+ H64.encode_3_offsets(hash,
idx+12 if idx < 4 else 5,
+ idx+6,
+ idx,
)
for idx in xrange(5)
- ) + _enc64b1(hash, 11)
+ ) + H64.encode_1_offset(hash, 11)
return HashInfo('1', salt, out)
_pat = re.compile(r"""
@@ -785,333 +649,6 @@ class Md5Crypt(CryptAlgorithm):
return other.chk == rec.chk
#=========================================================
-#ids 5,6 -- sha
-#algorithm defined on this page:
-# http://people.redhat.com/drepper/SHA-crypt.txt
-#=========================================================
-class _ShaCrypt(CryptAlgorithm):
- "this is the base class used by SHA-256 & SHA-512. don't use directly."
- #=========================================================
- #algorithm info
- #=========================================================
- #hash_bits, name filled in for subclass
- salt_bits = 96
- has_rounds = True
- has_named_rounds = True
-
- #tuning the round aliases
- rounds_per_second = 156000 #last tuned 2009-7-6 on a 2gz system
- fast_rounds = int(rounds_per_second * .25)
- medium_rounds = int(rounds_per_second * .75)
- slow_rounds = int(rounds_per_second * 1.5)
-
- #=========================================================
- #internals required from subclass
- #=========================================================
- _key = None #alg id (5, 6) of specific sha alg
- _hash = None #callable to use for hashing
- _chunk_size = None #bytes at a time to input secret
- _hash_size = None #bytes in hash
- _pat = None #regexp for sha variant
-
- @abstractmethod
- def _encode(self, result):
- "encode raw result into h64 style"
-
- #=========================================================
- #core sha crypt algorithm
- #=========================================================
- @classmethod
- def _sha_crypt_raw(self, rounds, salt, secret):
- "perform sha crypt, returning just the checksum"
- #setup alg-specific parameters
- hash = self._hash
- chunk_size = self._chunk_size
-
- #init salt
- if salt is None:
- salt = h64_gen_salt(16)
- elif len(salt) > 16:
- salt = salt[:16] #spec says to use up to first chars 16 only
-
- #init rounds
- if rounds == -1:
- real_rounds = 5000
- else:
- if rounds < 1000:
- rounds = 1000
- if rounds > 999999999:
- rounds = 999999999
- real_rounds = rounds
-
- def extend(source, size_ref):
- size = len(size_ref)
- return source * int(size/chunk_size) + source[:size % chunk_size]
-
- #calc digest B
- b = hash()
- b.update(secret)
- b.update(salt)
- b.update(secret)
- b_result = b.digest()
- b_extend = extend(b_result, secret)
-
- #begin digest A
- a = hash()
- a.update(secret)
- a.update(salt)
- a.update(b_extend)
-
- #for each bit in slen, add B or SECRET
- value = len(secret)
- while value > 0:
- if value % 2:
- a.update(b_result)
- else:
- a.update(secret)
- value >>= 1
-
- #finish A
- a_result = a.digest()
-
- #calc DP
- dp = hash()
- dp.update(secret * len(secret))
- dp_result = extend(dp.digest(), secret)
-
- #calc DS
- ds = hash()
- for i in xrange(0, 16+ord(a_result[0])):
- ds.update(salt)
- ds_result = extend(ds.digest(), salt) #aka 'S'
-
- #calc digest C
- last_result = a_result
- for i in xrange(0, real_rounds):
- c = hash()
- if i % 2:
- c.update(dp_result)
- else:
- c.update(last_result)
- if i % 3:
- c.update(ds_result)
- if i % 7:
- c.update(dp_result)
- if i % 2:
- c.update(last_result)
- else:
- c.update(dp_result)
- last_result = c.digest()
-
- #encode result using 256/512 specific func
- out = self._encode(last_result)
- assert len(out) == self._hash_size, "wrong length: %r" % (out,)
- return HashInfo(self._key, salt, out, rounds=rounds)
-
- @classmethod
- def _sha_crypt(self, rounds, salt, secret):
- rec = self._sha_crypt_raw(rounds, salt, secret)
- if rec.rounds == -1:
- return "$%s$%s$%s" % (rec.alg, rec.salt, rec.chk)
- else:
- return "$%s$rounds=%d$%s$%s" % (rec.alg, rec.rounds, rec.salt, rec.chk)
-
- #=========================================================
- #frontend helpers
- #=========================================================
- @classmethod
- def identify(self, hash):
- "identify bcrypt hash"
- if hash is None:
- return False
- return self._pat.match(hash) is not None
-
- @classmethod
- def _parse(self, hash):
- "parse bcrypt hash"
- m = self._pat.match(hash)
- if not m:
- raise ValueError, "invalid sha hash/salt"
- alg, rounds, salt, chk = m.group("alg", "rounds", "salt", "chk")
- if rounds is None:
- rounds = -1 #indicate we're using the default mode
- else:
- rounds = int(rounds)
- assert alg == self._key
- return HashInfo(alg, salt, chk, rounds=rounds, source=hash)
-
- @classmethod
- def encrypt(self, secret, hash=None, rounds=None, keep_salt=False):
- """encrypt using sha256/512-crypt.
-
- In addition to the normal options that :meth:`CryptAlgorithm.encrypt` takes,
- this function also accepts the following:
-
- :param rounds:
- Optionally specify the number of rounds to use.
- This can be one of "fast", "medium", "slow",
- or an integer in the range 1000...999999999.
-
- See :attr:`CryptAlgorithm.has_named_rounds` for details
- on the meaning of "fast", "medium" and "slow".
- """
- salt = None
- if hash:
- rec = self._parse(hash)
- if keep_salt:
- salt = rec.salt
- if rounds is None:
- rounds = rec.rounds
- rounds = self._norm_rounds(rounds)
- return self._sha_crypt(rounds, salt, secret)
-
- @classmethod
- def _norm_rounds(self, rounds):
- if isinstance(rounds, int):
- return rounds
- elif rounds == "fast" or rounds is None:
- return self.fast_rounds
- elif rounds == "slow":
- return self.slow_rounds
- else:
- if rounds != "medium":
- log.warning("unknown rounds alias %r, using 'medium'", rounds)
- return self.medium_rounds
-
- @classmethod
- def verify(self, secret, hash):
- if hash is None:
- return False
- rec = self._parse(hash)
- other = self._sha_crypt_raw(rec.rounds, rec.salt, secret)
- return other.chk == rec.chk
-
- #=========================================================
- #eoc
- #=========================================================
-
-class Sha256Crypt(_ShaCrypt):
- """This class implements the SHA-256 Crypt Algorithm,
- according to the specification at `<http://people.redhat.com/drepper/SHA-crypt.txt>`_.
- It should be byte-compatible with unix shadow hashes beginning with ``$5$``.
-
- See Sha512Crypt for usage examples and details.
- """
- #=========================================================
- #algorithm info
- #=========================================================
- name='sha256-crypt'
- hash_bits = 256
-
- #=========================================================
- #internals
- #=========================================================
- _hash = hashlib.sha256
- _key = '5'
- _chunk_size = 32
- _hash_size = 43
-
- @classmethod
- def _encode(self, result):
- out = ''
- a, b, c = [0, 10, 20]
- while a < 30:
- out += _enc64b3(result, a, b, c)
- a, b, c = c+1, a+1, b+1
- assert a == 30, "loop to far: %r" % (a,)
- out += _enc64b2(result, 31, 30)
- return out
-
- #=========================================================
- #frontend
- #=========================================================
- _pat = re.compile(r"""
- ^
- \$(?P<alg>5)
- (\$rounds=(?P<rounds>\d+))?
- \$(?P<salt>[A-Za-z0-9./]+)
- (\$(?P<chk>[A-Za-z0-9./]+))?
- $
- """, re.X)
-
- #=========================================================
- #eof
- #=========================================================
-
-class Sha512Crypt(_ShaCrypt):
- """This class implements the SHA-512 Crypt Algorithm,
- according to the specification at `http://people.redhat.com/drepper/SHA-crypt.txt`_.
- It should be byte-compatible with unix shadow hashes beginning with ``$6$``.
-
- This implementation is based on a pure-python translation
- of the original specification.
-
- .. note::
- This is *not* just the raw SHA-512 hash of the password,
- which is sometimes incorrectly referred to as sha512-crypt.
- This is a variable-round descendant of md5-crypt,
- and is comparable in strength to bcrypt.
-
- Usage Example::
-
- >>> from passlib.hash import Sha512Crypt
- >>> crypt = Sha512Crypt()
- >>> #to encrypt a new secret with this algorithm
- >>> hash = crypt.encrypt("forget me not")
- >>> hash
- '$6$rounds=11949$KkBupsnnII6YXqgT$O8qAEcEgDyJlMC4UB3buST8vE1PsPPABA.0lQIUARTNnlLPZyBRVXAvqqynVByGRLTRMIorkcR0bsVQS5i3Xw1'
- >>> #to verify an existing secret
- >>> crypt.verify("forget me not", hash)
- True
- >>> crypt.verify("i forgot it", hash)
- False
-
- .. automethod:: encrypt
- """
- #=========================================================
- #algorithm info
- #=========================================================
-
- name='sha512-crypt'
- hash_bits = 512
-
- #=========================================================
- #internals
- #=========================================================
- _hash = hashlib.sha512
- _key = '6'
- _chunk_size = 64
- _hash_size = 86
-
- @classmethod
- def _encode(self, result):
- out = ''
- a, b, c = [0, 21, 42]
- while c < 63:
- out += _enc64b3(result, a, b, c)
- a, b, c = b+1, c+1, a+1
- assert c == 63, "loop to far: %r" % (c,)
- out += _enc64b1(result, 63)
- return out
-
- #=========================================================
- #frontend
- #=========================================================
-
- _pat = re.compile(r"""
- ^
- \$(?P<alg>6)
- (\$rounds=(?P<rounds>\d+))?
- \$(?P<salt>[A-Za-z0-9./]+)
- (\$(?P<chk>[A-Za-z0-9./]+))?
- $
- """, re.X)
-
- #=========================================================
- #eof
- #=========================================================
-
-#=========================================================
#OpenBSD's BCrypt
#=========================================================
class BCrypt(CryptAlgorithm):
@@ -1561,152 +1098,5 @@ def is_crypt_context(obj):
))
#=========================================================
-#build up the standard context objects
-#=========================================================
-
-#default context for quick use.. recognizes all known algorithms,
-# currently uses SHA-512 as default
-default_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt, Sha256Crypt, Sha512Crypt ])
-
-def identify(hash, resolve=False):
- """Identify algorithm which generated a password hash.
-
- :arg hash:
- The hash string to identify.
- :param resolve:
- If ``True``, this function will return a :class:`CryptAlgorithm`
- instance which can handle the hash.
- If ``False`` (the default), then only the name of the hash algorithm
- will be returned.
-
- The following algorithms are currently recognized:
-
- =================== ================================================
- Name Description
- ------------------- ------------------------------------------------
- ``"unix-crypt"`` the historical unix-crypt algorithm
-
- ``"md5-crypt"`` the md5-crypt algorithm, usually identified
- by the prefix ``$1$`` in unix shadow files.
-
- ``"bcrypt"`` the openbsd blowfish-crypt algorithm,
- usually identified by the prefixes ``$2$`` or ``$2a$``
- in unix shadow files.
-
- ``"sha256-crypt"`` the 256-bit version of the sha-crypt algorithm,
- usually identified by the prefix ``$5$``
- in unix shadow files.
-
- ``"sha512-crypt"`` the 512-bit version of the sha-crypt algorithm,
- usually identified by the prefix ``$6$``
- in unix shadow files.
- =================== ================================================
-
- :returns:
- The name of the hash, or ``None`` if the hash could not be identified.
- (The return may be altered by the *resolve* keyword).
-
- .. note::
- This is a convience wrapper for ``pwhash.default_context.identify(hash)``.
- """
- return default_context.identify(hash, resolve=resolve)
-
-def encrypt(secret, hash=None, alg=None, **kwds):
- """Encrypt secret using a password hash algorithm.
-
- :type secret: str
- :arg secret:
- String containing the secret to encrypt
-
- :type hash: str|None
- :arg hash:
- Optional previously existing hash string which
- will be used to provide default value for the salt, rounds,
- or other algorithm-specific options.
- If not specified, algorithm-chosen defaults will be used.
-
- :type alg: str|None
- :param alg:
- Optionally specify the name of the algorithm to use.
- If no algorithm is specified, an attempt is made
- to guess from the hash string. If no hash string
- is specified, sha512-crypt will be used.
- See :func:`identify` for a list of algorithm names.
-
- All other keywords are passed on to the specific password algorithm
- being used to encrypt the secret.
-
- :type keep_salt: bool
- :param keep_salt:
- This option is accepted by all of the builtin algorithms.
-
- By default, a new salt value generated each time
- a secret is encrypted. However, if this keyword
- is set to ``True``, and a previous hash string is provided,
- the salt from that string will be used instead.
-
- .. note::
- This is generally only useful when verifying an existing hash
- (see :func:`verify`). Other than that, this option should be
- avoided, as re-using a salt will needlessly decrease security.
-
- :type rounds: int
- :param rounds:
- For the sha256-crypt and sha512-crypt algorithms,
- this option lets you specify the number of rounds
- of encryption to use. For the bcrypt algorithm,
- this option lets you specify the log-base-2 of
- the number of rounds of encryption to use.
-
- For all three of these algorithms, you can either
- specify a positive integer, or one of the strings
- "fast", "medium", "slow" to choose a preset number
- of rounds corresponding to an appropriate level
- of encryption.
-
- :returns:
- The secret as encoded by the specified algorithm and options.
- """
- return default_context.encrypt(secret, hash=hash, alg=alg, **kwds)
-
-def verify(secret, hash, alg=None):
- """verify a secret against an existing hash.
-
- This checks if a secret matches against the one stored
- inside the specified hash. By default this uses :func:`encrypt`
- to re-crypt the secret, and compares it to the provided hash;
- though some algorithms may implement this in a more efficient manner.
-
- :type secret: str
- :arg secret:
- A string containing the secret to check.
-
- :type hash: str
- :param hash:
- A string containing the hash to check against.
-
- :type alg: str|None
- :param alg:
- Optionally specify the name of the algorithm to use.
- If no algorithm is specified, an attempt is made
- to guess from the hash string. If it can't be
- identified, a ValueError will be raised.
- See :func:`identify` for a list of algorithm names.
-
- :returns:
- ``True`` if the secret matches, otherwise ``False``.
- """
- return default_context.verify(secret, hash, alg=alg)
-
-#some general os-context helpers (these may not match your os policy exactly)
-linux_context = CryptContext([ UnixCrypt, Md5Crypt, Sha256Crypt, Sha512Crypt ])
-bsd_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt ])
-
-#some sql db context helpers
-mysql40_context = CryptContext([Mysql10Crypt])
-mysql_context = CryptContext([Mysql10Crypt, Mysql41Crypt])
-postgres_context = CryptContext([PostgresMd5Crypt])
-
-#=========================================================
# eof
#=========================================================
diff --git a/passlib/hash/sha_crypt.py b/passlib/hash/sha_crypt.py
new file mode 100644
index 0000000..22bd590
--- /dev/null
+++ b/passlib/hash/sha_crypt.py
@@ -0,0 +1,355 @@
+"""passlib.hash.sha_crypt - implements SHA-256-Crypt & SHA-512-Crypt
+
+Implementation written based on specification at `<http://www.akkadia.org/drepper/SHA-crypt.txt>`_.
+It should be byte-compatible with unix shadow hashes beginning with ``$5$``.
+"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import re
+import hashlib
+import logging; log = logging.getLogger(__name__)
+import time
+import os
+#site
+#libs
+from passlib.hash.base import CryptAlgorithm, HashInfo
+from passlib.util import classproperty, abstractmethod, is_seq, srandom, H64
+#pkg
+#local
+__all__ = [
+ 'Sha256Crypt',
+ 'Sha512Crypt',
+]
+
+#=========================================================
+#ids 5,6 -- sha
+#algorithm defined on this page:
+# http://people.redhat.com/drepper/SHA-crypt.txt
+#=========================================================
+class _ShaCrypt(CryptAlgorithm):
+ "common code for used by Sha(256|512)Crypt Classes"
+ #=========================================================
+ #algorithm info
+ #=========================================================
+ #hash_bits, name filled in for subclass
+ salt_bits = 96
+ has_rounds = True
+ has_named_rounds = True
+
+ #tuning the round aliases
+ rounds_per_second = 156000 #last tuned 2009-7-6 on a 2gz system
+ fast_rounds = int(rounds_per_second * .25)
+ medium_rounds = int(rounds_per_second * .75)
+ slow_rounds = int(rounds_per_second * 1.5)
+
+ #=========================================================
+ #internals required from subclass
+ #=========================================================
+ _key = None #alg id (5, 6) of specific sha alg
+ _hash = None #callable to use for hashing
+ _chunk_size = None #bytes at a time to input secret
+ _hash_size = None #bytes in hash
+ _pat = None #regexp for sha variant
+
+ @abstractmethod
+ def _encode(self, result):
+ "encode raw result into h64 style"
+
+ #=========================================================
+ #core sha crypt algorithm
+ #=========================================================
+ @classmethod
+ def _sha_crypt_raw(self, rounds, salt, secret):
+ "perform sha crypt, returning just the checksum"
+ #setup alg-specific parameters
+ hash = self._hash
+ chunk_size = self._chunk_size
+
+ #init salt
+ if salt is None:
+ salt = H64.randstr(16)
+ elif len(salt) > 16:
+ salt = salt[:16] #spec says to use up to first chars 16 only
+
+ #init rounds
+ if rounds == -1:
+ real_rounds = 5000
+ else:
+ if rounds < 1000:
+ rounds = 1000
+ if rounds > 999999999:
+ rounds = 999999999
+ real_rounds = rounds
+
+ def extend(source, size_ref):
+ size = len(size_ref)
+ return source * int(size/chunk_size) + source[:size % chunk_size]
+
+ #calc digest B
+ b = hash()
+ b.update(secret)
+ b.update(salt)
+ b.update(secret)
+ b_result = b.digest()
+ b_extend = extend(b_result, secret)
+
+ #begin digest A
+ a = hash()
+ a.update(secret)
+ a.update(salt)
+ a.update(b_extend)
+
+ #for each bit in slen, add B or SECRET
+ value = len(secret)
+ while value > 0:
+ if value % 2:
+ a.update(b_result)
+ else:
+ a.update(secret)
+ value >>= 1
+
+ #finish A
+ a_result = a.digest()
+
+ #calc DP
+ dp = hash()
+ dp.update(secret * len(secret))
+ dp_result = extend(dp.digest(), secret)
+
+ #calc DS
+ ds = hash()
+ for i in xrange(0, 16+ord(a_result[0])):
+ ds.update(salt)
+ ds_result = extend(ds.digest(), salt) #aka 'S'
+
+ #calc digest C
+ last_result = a_result
+ for i in xrange(0, real_rounds):
+ c = hash()
+ if i % 2:
+ c.update(dp_result)
+ else:
+ c.update(last_result)
+ if i % 3:
+ c.update(ds_result)
+ if i % 7:
+ c.update(dp_result)
+ if i % 2:
+ c.update(last_result)
+ else:
+ c.update(dp_result)
+ last_result = c.digest()
+
+ #encode result using 256/512 specific func
+ out = self._encode(last_result)
+ assert len(out) == self._hash_size, "wrong length: %r" % (out,)
+ return HashInfo(self._key, salt, out, rounds=rounds)
+
+ @classmethod
+ def _sha_crypt(self, rounds, salt, secret):
+ rec = self._sha_crypt_raw(rounds, salt, secret)
+ if rec.rounds == -1:
+ return "$%s$%s$%s" % (rec.alg, rec.salt, rec.chk)
+ else:
+ return "$%s$rounds=%d$%s$%s" % (rec.alg, rec.rounds, rec.salt, rec.chk)
+
+ #=========================================================
+ #frontend helpers
+ #=========================================================
+ @classmethod
+ def identify(self, hash):
+ "identify bcrypt hash"
+ if hash is None:
+ return False
+ return self._pat.match(hash) is not None
+
+ @classmethod
+ def _parse(self, hash):
+ "parse bcrypt hash"
+ m = self._pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sha hash/salt"
+ alg, rounds, salt, chk = m.group("alg", "rounds", "salt", "chk")
+ if rounds is None:
+ rounds = -1 #indicate we're using the default mode
+ else:
+ rounds = int(rounds)
+ assert alg == self._key
+ return HashInfo(alg, salt, chk, rounds=rounds, source=hash)
+
+ @classmethod
+ def encrypt(self, secret, hash=None, rounds=None, keep_salt=False):
+ """encrypt using sha256/512-crypt.
+
+ In addition to the normal options that :meth:`CryptAlgorithm.encrypt` takes,
+ this function also accepts the following:
+
+ :param rounds:
+ Optionally specify the number of rounds to use.
+ This can be one of "fast", "medium", "slow",
+ or an integer in the range 1000...999999999.
+
+ See :attr:`CryptAlgorithm.has_named_rounds` for details
+ on the meaning of "fast", "medium" and "slow".
+ """
+ salt = None
+ if hash:
+ rec = self._parse(hash)
+ if keep_salt:
+ salt = rec.salt
+ if rounds is None:
+ rounds = rec.rounds
+ rounds = self._norm_rounds(rounds)
+ return self._sha_crypt(rounds, salt, secret)
+
+ @classmethod
+ def _norm_rounds(self, rounds):
+ if isinstance(rounds, int):
+ return rounds
+ elif rounds == "fast" or rounds is None:
+ return self.fast_rounds
+ elif rounds == "slow":
+ return self.slow_rounds
+ else:
+ if rounds != "medium":
+ log.warning("unknown rounds alias %r, using 'medium'", rounds)
+ return self.medium_rounds
+
+ @classmethod
+ def verify(self, secret, hash):
+ if hash is None:
+ return False
+ rec = self._parse(hash)
+ other = self._sha_crypt_raw(rec.rounds, rec.salt, secret)
+ return other.chk == rec.chk
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+class Sha256Crypt(_ShaCrypt):
+ """This class implements the SHA-256 Crypt Algorithm,
+ according to the specification at `<http://people.redhat.com/drepper/SHA-crypt.txt>`_.
+ It should be byte-compatible with unix shadow hashes beginning with ``$5$``.
+
+ See Sha512Crypt for usage examples and details.
+ """
+ #=========================================================
+ #algorithm info
+ #=========================================================
+ name='sha-256-crypt'
+ hash_bits = 256
+
+ #=========================================================
+ #internals
+ #=========================================================
+ _hash = hashlib.sha256
+ _key = '5'
+ _chunk_size = 32
+ _hash_size = 43
+
+ @classmethod
+ def _encode(self, result):
+ out = ''
+ a, b, c = 0, 10, 20
+ while a < 30:
+ out += H64.encode_3_offsets(result, c, b, a)
+ a, b, c = c+1, a+1, b+1
+ assert a == 30, "loop went to far: %r" % (a,)
+ out += H64.encode_2_offsets(result, 30, 31)
+ return out
+
+ #=========================================================
+ #frontend
+ #=========================================================
+ _pat = re.compile(r"""
+ ^
+ \$(?P<alg>5)
+ (\$rounds=(?P<rounds>\d+))?
+ \$(?P<salt>[A-Za-z0-9./]+)
+ (\$(?P<chk>[A-Za-z0-9./]+))?
+ $
+ """, re.X)
+
+ #=========================================================
+ #eof
+ #=========================================================
+
+class Sha512Crypt(_ShaCrypt):
+ """This class implements the SHA-512 Crypt Algorithm,
+ according to the specification at `http://people.redhat.com/drepper/SHA-crypt.txt`_.
+ It should be byte-compatible with unix shadow hashes beginning with ``$6$``.
+
+ This implementation is based on a pure-python translation
+ of the original specification.
+
+ .. note::
+ This is *not* just the raw SHA-512 hash of the password,
+ which is sometimes incorrectly referred to as sha512-crypt.
+ This is a variable-round descendant of md5-crypt,
+ and is comparable in strength to bcrypt.
+
+ Usage Example::
+
+ >>> from passlib.hash import Sha512Crypt
+ >>> crypt = Sha512Crypt()
+ >>> #to encrypt a new secret with this algorithm
+ >>> hash = crypt.encrypt("forget me not")
+ >>> hash
+ '$6$rounds=11949$KkBupsnnII6YXqgT$O8qAEcEgDyJlMC4UB3buST8vE1PsPPABA.0lQIUARTNnlLPZyBRVXAvqqynVByGRLTRMIorkcR0bsVQS5i3Xw1'
+ >>> #to verify an existing secret
+ >>> crypt.verify("forget me not", hash)
+ True
+ >>> crypt.verify("i forgot it", hash)
+ False
+
+ .. automethod:: encrypt
+ """
+ #=========================================================
+ #algorithm info
+ #=========================================================
+ name='sha-512-crypt'
+ hash_bits = 512
+
+ #=========================================================
+ #internals
+ #=========================================================
+ _hash = hashlib.sha512
+ _key = '6'
+ _chunk_size = 64
+ _hash_size = 86
+
+ @classmethod
+ def _encode(self, result):
+ out = ''
+ a, b, c = 0, 21, 42
+ while c < 63:
+ out += H64.encode_3_offsets(result, c, b, a)
+ a, b, c = b+1, c+1, a+1
+ assert c == 63, "loop to far: %r" % (c,)
+ out += H64.encode_1_offset(result, 63)
+ return out
+
+ #=========================================================
+ #frontend
+ #=========================================================
+
+ _pat = re.compile(r"""
+ ^
+ \$(?P<alg>6)
+ (\$rounds=(?P<rounds>\d+))?
+ \$(?P<salt>[A-Za-z0-9./]+)
+ (\$(?P<chk>[A-Za-z0-9./]+))?
+ $
+ """, re.X)
+
+ #=========================================================
+ #eof
+ #=========================================================
+
+#=========================================================
+# eof
+#=========================================================
diff --git a/passlib/hash/unix_crypt.py b/passlib/hash/unix_crypt.py
new file mode 100644
index 0000000..697eca7
--- /dev/null
+++ b/passlib/hash/unix_crypt.py
@@ -0,0 +1,96 @@
+"""passlib.hash - implementation of various password hashing functions"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import inspect
+import re
+import hashlib
+import logging; log = logging.getLogger(__name__)
+import time
+import os
+try:
+ #try stdlib module, which is only present under posix
+ from crypt import crypt as _crypt
+
+ #NOTE: we're wrapping the builtin crypt with some checks due to deficiencies in it's base implementation.
+ # 1. given an empty salt, it returns '' instead of raising an error. the wrapper raises an error.
+ # 2. given a single letter salt, it returns a hash with the original salt doubled,
+ # but appears to calculate the hash based on the letter + "G" as the second byte.
+ # this results in a hash that won't validate, which is DEFINITELY wrong.
+ # the wrapper raises an error.
+ # 3. given salt chars outside of H64.CHARS range, it does something unknown internally,
+ # but reports the hashes correctly. until this alg gets fixed in builtin crypt or stdlib crypt,
+ # wrapper raises an error for bad salts.
+ # 4. it tries to encode unicode -> ascii, unlike most hashes. the wrapper encodes to utf-8.
+ def crypt(key, salt):
+ "wrapper around stdlib's crypt"
+ if '\x00' in key:
+ raise ValueError, "null char in key"
+ if isinstance(key, unicode):
+ key = key.encode("utf-8")
+ if not salt or len(salt) < 2:
+ raise ValueError, "invalid salt"
+ elif salt[0] not in H64.CHARS or salt[1] not in H64.CHARS:
+ raise ValueError, "invalid salt"
+ return _crypt(key, salt)
+
+ backend = "stdlib"
+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
+ backend = "builtin"
+#site
+#pkg
+from passlib.hash.base import CryptAlgorithm, HashInfo
+from passlib.util import classproperty, abstractmethod, is_seq, srandom, H64
+#local
+__all__ = [
+ 'UnixCrypt',
+ 'crypt',
+]
+
+#=========================================================
+#old unix crypt
+#=========================================================
+
+class UnixCrypt(CryptAlgorithm):
+ """Old Unix-Crypt Algorithm, as originally used on unix before md5-crypt arrived.
+ This implementation uses the builtin ``crypt`` module when available,
+ but contains a pure-python fallback so that this algorithm can always be used.
+ """
+ name = "unix-crypt"
+ salt_bits = 6*2
+ hash_bits = 6*11
+ has_rounds = False
+ secret_chars = 8
+
+ #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
+ _pat = re.compile(r"""
+ ^
+ (?P<salt>[./a-z0-9]{2})
+ (?P<hash>[./a-z0-9]{11})
+ $""", re.X|re.I)
+
+ @classmethod
+ def identify(self, hash):
+ if hash is None:
+ return False
+ return self._pat.match(hash) is not None
+
+ @classmethod
+ def encrypt(self, secret, hash=None, keep_salt=False):
+ if hash and keep_salt:
+ salt = hash[:2]
+ else:
+ salt = H64.randstr(2)
+ return crypt(secret, salt)
+
+ #default verify used
+
+#=========================================================
+# eof
+#=========================================================
diff --git a/passlib/tests/test_crypt_context.py b/passlib/tests/test_crypt_context.py
new file mode 100644
index 0000000..6dc43e2
--- /dev/null
+++ b/passlib/tests/test_crypt_context.py
@@ -0,0 +1,601 @@
+"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import hashlib
+import warnings
+from logging import getLogger
+#site
+#pkg
+from passlib import hash as pwhash
+from passlib.tests.utils import TestCase, enable_suite
+from passlib.util import H64
+from passlib.hash.unix_crypt import UnixCrypt
+from passlib.hash.sha_crypt import Sha512Crypt
+from passlib.hash.base import Md5Crypt
+from passlib.tests.test_hash import UnsaltedAlg, SaltedAlg, SampleAlg
+#module
+log = getLogger(__name__)
+
+#=========================================================
+#CryptContext
+#=========================================================
+
+CryptContext = pwhash.CryptContext
+
+class CryptContextTest(TestCase):
+ "test CryptContext object's behavior"
+
+ #=========================================================
+ #0 constructor
+ #=========================================================
+ def test_00_constructor(self):
+ "test CryptContext constructor using classes"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+
+ #parse
+ a, b, c = cc
+ self.assertIsInstance(a, UnsaltedAlg)
+ self.assertIsInstance(b, SaltedAlg)
+ self.assertIsInstance(c, SampleAlg)
+
+ def test_01_constructor(self):
+ "test CryptContext constructor using instances"
+ #create crypt context
+ a = UnsaltedAlg()
+ b = SaltedAlg()
+ c = SampleAlg()
+ cc = CryptContext([a,b,c])
+
+ #verify elements
+ self.assertEquals(list(cc), [a, b, c])
+
+ #=========================================================
+ #1 list getters
+ #=========================================================
+ def test_10_getitem(self):
+ "test CryptContext.__getitem__[idx]"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ #verify len
+ self.assertEquals(len(cc), 3)
+
+ #verify getitem
+ self.assertEquals(cc[0], a)
+ self.assertEquals(cc[1], b)
+ self.assertEquals(cc[2], c)
+ self.assertEquals(cc[-1], c)
+ self.assertRaises(IndexError, cc.__getitem__, 3)
+
+ def test_11_index(self):
+ "test CryptContext.index(elem)"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ d = SampleAlg()
+
+ self.assertEquals(cc.index(a), 0)
+ self.assertEquals(cc.index(b), 1)
+ self.assertEquals(cc.index(c), 2)
+ self.assertEquals(cc.index(d), -1)
+
+ def test_12_contains(self):
+ "test CryptContext.__contains__(elem)"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ d = SampleAlg()
+
+ self.assertEquals(a in cc, True)
+ self.assertEquals(b in cc, True)
+ self.assertEquals(c in cc, True)
+ self.assertEquals(d in cc, False)
+
+ #=========================================================
+ #2 list setters
+ #=========================================================
+ def test_20_setitem(self):
+ "test CryptContext.__setitem__"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ d = SampleAlg()
+ self.assertIsNot(c, d)
+ e = pwhash.Md5Crypt()
+
+ #check baseline
+ self.assertEquals(list(cc), [a, b, c])
+
+ #replace 0 w/ d should raise error (SampleAlg already in list)
+ self.assertRaises(KeyError, cc.__setitem__, 0, d)
+ self.assertEquals(list(cc), [a, b, c])
+
+ #replace 0 w/ e
+ cc[0] = e
+ self.assertEquals(list(cc), [e, b, c])
+
+ #replace 2 w/ d
+ cc[2] = d
+ self.assertEquals(list(cc), [e, b, d])
+
+ #replace -1 w/ c
+ cc[-1] = c
+ self.assertEquals(list(cc), [e, b, c])
+
+ #replace -2 w/ d should raise error
+ self.assertRaises(KeyError, cc.__setitem__, -2, d)
+ self.assertEquals(list(cc), [e, b, c])
+
+ def test_21_append(self):
+ "test CryptContext.__setitem__"
+ cc = CryptContext([UnsaltedAlg])
+ a, = cc
+ b = SaltedAlg()
+ c = SampleAlg()
+ d = SampleAlg()
+
+ self.assertEquals(list(cc), [a])
+
+ #try append
+ cc.append(b)
+ self.assertEquals(list(cc), [a, b])
+
+ #and again
+ cc.append(c)
+ self.assertEquals(list(cc), [a, b, c])
+
+ #try append dup
+ self.assertRaises(KeyError, cc.append, d)
+ self.assertEquals(list(cc), [a, b, c])
+
+ def test_20_insert(self):
+ "test CryptContext.insert"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ d = SampleAlg()
+ self.assertIsNot(c, d)
+ e = pwhash.Md5Crypt()
+ f = pwhash.Sha512Crypt()
+ g = pwhash.UnixCrypt()
+
+ #check baseline
+ self.assertEquals(list(cc), [a, b, c])
+
+ #inserting d at 0 should raise error (SampleAlg already in list)
+ self.assertRaises(KeyError, cc.insert, 0, d)
+ self.assertEquals(list(cc), [a, b, c])
+
+ #insert e at start
+ cc.insert(0, e)
+ self.assertEquals(list(cc), [e, a, b, c])
+
+ #insert f at end
+ cc.insert(-1, f)
+ self.assertEquals(list(cc), [e, a, b, f, c])
+
+ #insert g at end
+ cc.insert(5, g)
+ self.assertEquals(list(cc), [e, a, b, f, c, g])
+
+ #=========================================================
+ #3 list dellers
+ #=========================================================
+ def test_30_remove(self):
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ d = SampleAlg()
+ self.assertIsNot(c, d)
+
+ self.assertEquals(list(cc), [a, b, c])
+
+ self.assertRaises(ValueError, cc.remove, d)
+ self.assertEquals(list(cc), [a, b, c])
+
+ cc.remove(a)
+ self.assertEquals(list(cc), [b, c])
+
+ self.assertRaises(ValueError, cc.remove, a)
+ self.assertEquals(list(cc), [b, c])
+
+ def test_31_discard(self):
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ d = SampleAlg()
+ self.assertIsNot(c, d)
+
+ self.assertEquals(list(cc), [a, b, c])
+
+ self.assertEquals(cc.discard(d), False)
+ self.assertEquals(list(cc), [a, b, c])
+
+ self.assertEquals(cc.discard(a), True)
+ self.assertEquals(list(cc), [b, c])
+
+ self.assertEquals(cc.discard(a), False)
+ self.assertEquals(list(cc), [b, c])
+
+ #=========================================================
+ #4 list composition
+ #=========================================================
+
+ def test_40_add(self, lsc=False):
+ "test CryptContext + list"
+ #build and join cc to list
+ a = UnsaltedAlg()
+ b = SaltedAlg()
+ c = SampleAlg()
+ cc = CryptContext([a, b, c])
+ ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt]
+ if lsc:
+ ls = CryptContext(ls)
+ cc2 = cc + ls
+
+ #verify types
+ self.assertIsInstance(cc, CryptContext)
+ self.assertIsInstance(cc2, CryptContext)
+ self.assertIsInstance(ls, CryptContext if lsc else list)
+
+ #verify elements
+ self.assertIsNot(cc, ls)
+ self.assertIsNot(cc, cc2)
+ self.assertIsNot(ls, cc2)
+
+ #verify cc
+ a, b, c = cc
+ self.assertIsInstance(a, UnsaltedAlg)
+ self.assertIsInstance(b, SaltedAlg)
+ self.assertIsInstance(c, SampleAlg)
+
+ #verify ls
+ d, e = ls
+ if lsc:
+ self.assertIsInstance(d, Md5Crypt)
+ self.assertIsInstance(e, Sha512Crypt)
+ else:
+ self.assertIs(d, Md5Crypt)
+ self.assertIs(e, Sha512Crypt)
+
+ #verify cc2
+ a2, b2, c2, d2, e2 = cc2
+ self.assertIs(a2, a)
+ self.assertIs(b2, b)
+ self.assertIs(c2, c)
+ if lsc:
+ self.assertIs(d2, d)
+ self.assertIs(e2, e)
+ else:
+ self.assertIsInstance(d2, Md5Crypt)
+ self.assertIsInstance(e2, Sha512Crypt)
+
+ def test_41_add(self):
+ "test CryptContext + CryptContext"
+ self.test_40_add(lsc=True)
+
+ def test_42_iadd(self, lsc=False):
+ "test CryptContext += list"
+ #build and join cc to list
+ a = UnsaltedAlg()
+ b = SaltedAlg()
+ c = SampleAlg()
+ cc = CryptContext([a, b, c])
+ ls = [Md5Crypt, Sha512Crypt]
+ if lsc:
+ ls = CryptContext(ls)
+
+ #baseline
+ self.assertEquals(list(cc), [a, b, c])
+ self.assertIsInstance(cc, CryptContext)
+ self.assertIsInstance(ls, CryptContext if lsc else list)
+ if lsc:
+ d, e = ls
+ self.assertIsInstance(d, Md5Crypt)
+ self.assertIsInstance(e, Sha512Crypt)
+
+ #add
+ cc += ls
+
+ #verify types
+ self.assertIsInstance(cc, CryptContext)
+ self.assertIsInstance(ls, CryptContext if lsc else list)
+
+ #verify elements
+ self.assertIsNot(cc, ls)
+
+ #verify cc
+ a2, b2, c2, d2, e2 = cc
+ self.assertIs(a2, a)
+ self.assertIs(b2, b)
+ self.assertIs(c2, c)
+ if lsc:
+ self.assertIs(d2, d)
+ self.assertIs(e2, e)
+ else:
+ self.assertIsInstance(d2, Md5Crypt)
+ self.assertIsInstance(e2, Sha512Crypt)
+
+ #verify ls
+ d, e = ls
+ if lsc:
+ self.assertIsInstance(d, Md5Crypt)
+ self.assertIsInstance(e, Sha512Crypt)
+ else:
+ self.assertIs(d, Md5Crypt)
+ self.assertIs(e, Sha512Crypt)
+
+ def test_43_iadd(self):
+ "test CryptContext += CryptContext"
+ self.test_42_iadd(lsc=True)
+
+ def test_44_extend(self):
+ a = UnsaltedAlg()
+ b = SaltedAlg()
+ c = SampleAlg()
+ cc = CryptContext([a, b, c])
+ ls = [Md5Crypt, Sha512Crypt]
+
+ cc.extend(ls)
+
+ a2, b2, c2, d2, e2 = cc
+ self.assertIs(a2, a)
+ self.assertIs(b2, b)
+ self.assertIs(c2, c)
+ self.assertIsInstance(d2, Md5Crypt)
+ self.assertIsInstance(e2, Sha512Crypt)
+
+ self.assertRaises(KeyError, cc.extend, [Sha512Crypt ])
+ self.assertRaises(KeyError, cc.extend, [Sha512Crypt() ])
+
+ #=========================================================
+ #5 basic crypt interface
+ #=========================================================
+ def test_50_resolve(self):
+ "test CryptContext.resolve()"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ self.assertEquals(cc.resolve('unsalted'), a)
+ self.assertEquals(cc.resolve('salted'), b)
+ self.assertEquals(cc.resolve('sample'), c)
+ self.assertEquals(cc.resolve('md5-crypt'), None)
+
+ self.assertEquals(cc.resolve(['unsalted']), a)
+ self.assertEquals(cc.resolve(['md5-crypt']), None)
+ self.assertEquals(cc.resolve(['unsalted', 'salted', 'md5-crypt']), b)
+
+ def test_51_identify(self):
+ "test CryptContext.identify"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ for crypt in (a, b, c):
+ h = crypt.encrypt("test")
+ self.assertEquals(cc.identify(h, resolve=True), crypt)
+ self.assertEquals(cc.identify(h), crypt.name)
+
+ self.assertEquals(cc.identify('$1$232323123$1287319827', resolve=True), None)
+ self.assertEquals(cc.identify('$1$232323123$1287319827'), None)
+
+ #make sure "None" is accepted
+ self.assertEquals(cc.identify(None), None)
+
+ def test_52_encrypt_and_verify(self):
+ "test CryptContext.encrypt & verify"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ #check encrypt/id/verify pass for all algs
+ for crypt in (a, b, c):
+ h = cc.encrypt("test", alg=crypt.name)
+ self.assertEquals(cc.identify(h, resolve=True), crypt)
+ self.assertEquals(cc.verify('test', h), True)
+ self.assertEquals(cc.verify('notest', h), False)
+
+ #check default alg
+ h = cc.encrypt("test")
+ self.assertEquals(cc.identify(h, resolve=True), c)
+
+ #check verify using algs
+ self.assertEquals(cc.verify('test', h, alg='sample'), True)
+ self.assertEquals(cc.verify('test', h, alg='salted'), False)
+
+ def test_53_encrypt_salting(self):
+ "test CryptContext.encrypt salting options"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+ self.assert_(c.has_salt)
+
+ h = cc.encrypt("test")
+ self.assertEquals(cc.identify(h, resolve=True), c)
+
+ h2 = cc.encrypt("test", h)
+ self.assertEquals(cc.identify(h2, resolve=True), c)
+ self.assertNotEquals(h2, h)
+
+ h3 = cc.encrypt("test", h, keep_salt=True)
+ self.assertEquals(cc.identify(h3, resolve=True), c)
+ self.assertEquals(h3, h)
+
+ def test_54_verify_empty(self):
+ "test CryptContext.verify allows hash=None"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ self.assertEquals(cc.verify('xxx', None), False)
+ for crypt in cc:
+ self.assertEquals(cc.verify('xxx', None, alg=crypt.name), False)
+
+#XXX: haven't decided if this should be part of protocol
+## def test_55_verify_empty_secret(self):
+## "test CryptContext.verify allows secret=None"
+## cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+## h = cc.encrypt("test")
+## self.assertEquals(cc.verify(None,h), False)
+
+ #=========================================================
+ #6 crypt-enhanced list interface
+ #=========================================================
+ def test_60_getitem(self):
+ "test CryptContext.__getitem__[algname]"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ #verify getitem
+ self.assertEquals(cc['unsalted'], a)
+ self.assertEquals(cc['salted'], b)
+ self.assertEquals(cc['sample'], c)
+ self.assertRaises(KeyError, cc.__getitem__, 'md5-crypt')
+
+ def test_61_get(self):
+ "test CryptContext.get(algname)"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ #verify getitem
+ self.assertEquals(cc.get('unsalted'), a)
+ self.assertEquals(cc.get('salted'), b)
+ self.assertEquals(cc.get('sample'), c)
+ self.assertEquals(cc.get('md5-crypt'), None)
+
+ def test_62_index(self):
+ "test CryptContext.index(algname)"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+
+ #verify getitem
+ self.assertEquals(cc.index('unsalted'), 0)
+ self.assertEquals(cc.index('salted'), 1)
+ self.assertEquals(cc.index('sample'), 2)
+ self.assertEquals(cc.index('md5-crypt'), -1)
+
+ def test_63_contains(self):
+ "test CryptContext.__contains__(algname)"
+ #create crypt context
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ self.assertEquals('salted' in cc, True)
+ self.assertEquals('unsalted' in cc, True)
+ self.assertEquals('sample' in cc, True)
+ self.assertEquals('md5-crypt' in cc, False)
+
+ def test_64_keys(self):
+ "test CryptContext.keys()"
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ self.assertEquals(cc.keys(), ['unsalted', 'salted', 'sample'])
+
+ def test_65_remove(self):
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ self.assertEquals(list(cc), [a, b, c])
+
+ self.assertRaises(KeyError, cc.remove, 'md5-crypt')
+ self.assertEquals(list(cc), [a, b, c])
+
+ cc.remove('unsalted')
+ self.assertEquals(list(cc), [b, c])
+
+ self.assertRaises(KeyError, cc.remove, 'unsalted')
+ self.assertEquals(list(cc), [b, c])
+
+ def test_66_discard(self):
+ cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
+ a, b, c = cc
+
+ self.assertEquals(list(cc), [a, b, c])
+
+ self.assertEquals(cc.discard('md5-crypt'), False)
+ self.assertEquals(list(cc), [a, b, c])
+
+ self.assertEquals(cc.discard('unsalted'), True)
+ self.assertEquals(list(cc), [b, c])
+
+ self.assertEquals(cc.discard('unsalted'), False)
+ self.assertEquals(list(cc), [b, c])
+ #=========================================================
+ #eoc
+ #=========================================================
+
+#=========================================================
+#quick access functions
+#=========================================================
+
+#this test suite uses info stored in the specific hash algs' test suites,
+#so we have to import them here.
+from passlib.tests.test_sha_crypt import Sha256CryptTest, Sha512CryptTest
+from passlib.tests.test_unix_crypt import UnixCryptTest
+from passlib.tests.test_hash import Md5CryptTest
+try:
+ from passlib.tests.test_hash import BCryptTest
+except ImportError:
+ BCryptTest = None
+
+class QuickAccessTest(TestCase):
+ "test quick access functions"
+
+ crypt_cases = [ UnixCryptTest, Md5CryptTest, Sha256CryptTest]
+ if BCryptTest:
+ crypt_cases.append(BCryptTest)
+ crypt_cases.extend([ Sha512CryptTest ])
+
+ def test_00_identify(self):
+ "test pwhash.identify()"
+ identify = pwhash.identify
+ for cc in self.crypt_cases:
+ name = cc.alg.name
+ for _, hash in cc.positive_knowns:
+ self.assertEqual(identify(hash), name)
+ for _, hash in cc.negative_knowns:
+ self.assertEqual(identify(hash), name)
+ for hash in cc.negative_identify:
+ self.assertNotEqual(identify(hash), name)
+ for hash in cc.invalid_identify:
+ self.assertEqual(identify(hash), None)
+
+ def test_01_verify(self):
+ "test pwhash.verify()"
+ verify = pwhash.verify
+ for cc in self.crypt_cases:
+ name = cc.alg.name
+ for secret, hash in cc.positive_knowns[:3]:
+ self.assert_(verify(secret, hash))
+ self.assert_(verify(secret, hash, alg=name))
+ for secret, hash in cc.negative_knowns[:3]:
+ self.assert_(not verify(secret, hash))
+ self.assert_(not verify(secret, hash, alg=name))
+ for hash in cc.invalid_identify[:3]:
+ #context should raise ValueError because can't be identified
+ self.assertRaises(ValueError, verify, secret, hash)
+
+ def test_02_encrypt(self):
+ "test pwhash.encrypt()"
+ identify = pwhash.identify
+ verify = pwhash.verify
+ encrypt = pwhash.encrypt
+ for cc in self.crypt_cases:
+ alg = cc.alg.name
+ s = 'test'
+ h = encrypt(s, alg=alg)
+ self.assertEqual(identify(h), alg)
+ self.assertEqual(verify(s, h), True)
+ h2 = encrypt(s, h)
+ self.assertEqual(identify(h2), alg)
+ self.assertEqual(verify(s, h2, alg=alg), True)
+
+ def test_04_default_context(self):
+ "test pwhash.default_context contents"
+ dc = pwhash.default_context
+ for case in self.crypt_cases:
+ self.assert_(case.alg.name in dc)
+
+ last = 'sha-512-crypt'
+ self.assertEqual(dc.keys()[-1], last)
+ h = dc.encrypt("test")
+ self.assertEqual(dc.identify(h), last)
+ self.assertEqual(dc.verify('test', h, alg=last), True)
+
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/tests/test_hash.py b/passlib/tests/test_hash.py
index b17897b..f7b05bb 100644
--- a/passlib/tests/test_hash.py
+++ b/passlib/tests/test_hash.py
@@ -11,6 +11,7 @@ from logging import getLogger
#pkg
from passlib import hash as pwhash
from passlib.tests.utils import TestCase, enable_suite
+from passlib.util import H64
#module
log = getLogger(__name__)
@@ -60,7 +61,7 @@ class SaltedAlg(pwhash.CryptAlgorithm):
@classmethod
def encrypt(self, secret, salt=None, keep_salt=False):
## warn("keep_salt not supported by this algorithm")
- real_salt = pwhash.h64_gen_salt(2)
+ real_salt = H64.randstr(2)
return self._raw(secret, real_salt)
@classmethod
@@ -86,7 +87,7 @@ class SampleAlg(pwhash.CryptAlgorithm):
if salt and keep_salt:
real_salt = salt[4:6]
else:
- real_salt = pwhash.h64_gen_salt(2)
+ real_salt = H64.randstr(2)
return "@sam%s%s" % (real_salt, hashlib.sha1(real_salt+secret).hexdigest())
#=========================================================
@@ -98,11 +99,9 @@ SECRETS = [
'',
' ',
'test',
- 'testa',
+ 'testtest',
'test test',
'test bcdef',
- 'testq'
- 'testtest',
'Compl3X AlphaNu3meric',
'4lpHa N|_|M3r1K W/ Cur51|\\|g: #$%(*)(*%#',
'Really Long Password (tm), which is all the rage nowadays with the cool kids'
@@ -120,7 +119,7 @@ class _CryptTestCase(TestCase):
negative_identify = () # list of hashses that shouldn't identify as this one
invalid_identify = () # list of this alg's hashes w/ typo
- def message_prefix(self):
+ def case_prefix(self):
return self.alg.name
secrets = SECRETS #list of default secrets to check
@@ -380,60 +379,6 @@ class PostgresMd5CryptTest(_CryptTestCase):
return self.alg().verify(secret, hash, user=user)
#=========================================================
-#UnixCrypt
-#=========================================================
-
-class UnixCryptTest(_CryptTestCase):
- "test UnixCrypt algorithm"
- alg = pwhash.UnixCrypt
- positive_knowns = (
- #secret, example hash which matches secret
- ('', 'OgAwTx2l6NADI'),
- (' ', '/Hk.VPuwQTXbc'),
- ('test', 'N1tQbOFcM5fpg'),
- ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'),
- ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'),
- ('AlOtBsOl', 'cEpWz5IUCShqM'),
- )
- invalid_identify = (
- #bad char in otherwise correctly formatted hash
- '!gAwTx2l6NADI',
- )
- negative_identify = (
- #hashes using other algs, which shouldn't match this algorithm
- '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc',
- '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'
- )
-
-class UnixCryptBackendTest(TestCase):
- "test builtin unix crypt backend"
- unix_crypt = pwhash.unix_crypt
-
- positive_knowns = UnixCryptTest.positive_knowns
-
- def test_knowns(self):
- "test lowlevel unix_crypt function"
- unix_crypt = self.unix_crypt
- for secret, result in self.positive_knowns:
- #make sure crypt verifies using salt
- out = unix_crypt(secret, result[:2])
- self.assertEqual(out, result)
- #make sure crypt verifies using partial hash
- out = unix_crypt(secret, result[:6])
- self.assertEqual(out, result)
- #make sure crypt verifies using whole hash
- out = unix_crypt(secret, result)
- self.assertEqual(out, result)
-
- #TODO: deal with border cases where host crypt & bps crypt differ
- # (none of which should impact the normal use cases)
- #border cases:
- # no salt given, empty salt given, 1 char salt
- # salt w/ non-b64 chars (linux crypt handles this _somehow_)
- #test that \x00 is NOT allowed
- #test that other chars _are_ allowed
-
-#=========================================================
#Md5Crypt
#=========================================================
class Md5CryptTest(_CryptTestCase):
@@ -457,95 +402,6 @@ class Md5CryptTest(_CryptTestCase):
)
#=========================================================
-#test raw sha-crypt functions
-#=========================================================
-class Sha256CryptTest(_CryptTestCase):
- alg = pwhash.Sha256Crypt
- positive_knowns = (
- ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
- (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'),
- ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'),
- ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'),
- ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'),
- )
- invalid_identify = (
- #bad char in otherwise correct hash
- '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe!ZGsGx2aBvxTvDFI613c3'
- )
- negative_identify = (
- #other hashes
- '!gAwTx2l6NADI',
- '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc',
- '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox',
- )
-
-class Sha512CryptTest(_CryptTestCase):
- alg = pwhash.Sha512Crypt
- positive_knowns = (
- ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'),
- (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'),
- ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'),
- ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'),
- ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'),
- )
- negative_identify = (
- #other hashes
- '!gAwTx2l6NADI',
- '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3',
- '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox',
- )
- invalid_identify = (
- #bad char in otherwise correct hash
- '$6$rounds=11021$KsvQipYPWpr9!wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
- )
-
-class Sha512BackendTest(TestCase):
- "test sha512-crypt backend against specification unittest"
- cases512 = [
- #salt-hash, secret, result -- taken from alg definition page
- ("$6$saltstring", "Hello world!",
- "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu"
- "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ),
-
- ( "$6$rounds=10000$saltstringsaltstring", "Hello world!",
- "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"
- "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ),
-
- ( "$6$rounds=5000$toolongsaltstring", "This is just a test",
- "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ"
- "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ),
-
- ( "$6$rounds=1400$anotherlongsaltstring",
- "a very much longer text to encrypt. This one even stretches over more"
- "than one line.",
- "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP"
- "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ),
-
- ( "$6$rounds=77777$short",
- "we have a short salt string but not a short password",
- "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g"
- "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ),
-
- ( "$6$rounds=123456$asaltof16chars..", "a short string",
- "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc"
- "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ),
-
- ( "$6$rounds=10$roundstoolow", "the minimum number is still observed",
- "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x"
- "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ),
- ]
- def test512(self):
- crypt = pwhash.Sha512Crypt()
- for hash, secret, result in self.cases512:
- rec = crypt._parse(hash)
- self.assertEqual(rec.alg, '6')
- out = crypt.encrypt(secret, hash, keep_salt=True)
- rec2 = crypt._parse(hash)
- self.assertEqual(rec2.salt, rec.salt, "hash=%r secret=%r" % (hash, secret))
- self.assertEqual(rec2.chk, rec.chk, "hash=%r secret=%r" % (hash, secret))
- self.assertEqual(out, result, "hash=%r secret=%r" % (hash, secret))
-
-#=========================================================
#BCrypt
#=========================================================
if enable_suite("bcrypt"):
@@ -609,571 +465,5 @@ class UtilsTest(TestCase):
self.assertEqual(SaltedAlg().has_salt, True)
#=========================================================
-#CryptContext
-#=========================================================
-
-CryptContext = pwhash.CryptContext
-
-class CryptContextTest(TestCase):
- "test CryptContext object's behavior"
-
- #=========================================================
- #0 constructor
- #=========================================================
- def test_00_constructor(self):
- "test CryptContext constructor using classes"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
-
- #parse
- a, b, c = cc
- self.assertIsInstance(a, UnsaltedAlg)
- self.assertIsInstance(b, SaltedAlg)
- self.assertIsInstance(c, SampleAlg)
-
- def test_01_constructor(self):
- "test CryptContext constructor using instances"
- #create crypt context
- a = UnsaltedAlg()
- b = SaltedAlg()
- c = SampleAlg()
- cc = CryptContext([a,b,c])
-
- #verify elements
- self.assertEquals(list(cc), [a, b, c])
-
- #=========================================================
- #1 list getters
- #=========================================================
- def test_10_getitem(self):
- "test CryptContext.__getitem__[idx]"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- #verify len
- self.assertEquals(len(cc), 3)
-
- #verify getitem
- self.assertEquals(cc[0], a)
- self.assertEquals(cc[1], b)
- self.assertEquals(cc[2], c)
- self.assertEquals(cc[-1], c)
- self.assertRaises(IndexError, cc.__getitem__, 3)
-
- def test_11_index(self):
- "test CryptContext.index(elem)"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- d = SampleAlg()
-
- self.assertEquals(cc.index(a), 0)
- self.assertEquals(cc.index(b), 1)
- self.assertEquals(cc.index(c), 2)
- self.assertEquals(cc.index(d), -1)
-
- def test_12_contains(self):
- "test CryptContext.__contains__(elem)"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- d = SampleAlg()
-
- self.assertEquals(a in cc, True)
- self.assertEquals(b in cc, True)
- self.assertEquals(c in cc, True)
- self.assertEquals(d in cc, False)
-
- #=========================================================
- #2 list setters
- #=========================================================
- def test_20_setitem(self):
- "test CryptContext.__setitem__"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- d = SampleAlg()
- self.assertIsNot(c, d)
- e = pwhash.Md5Crypt()
-
- #check baseline
- self.assertEquals(list(cc), [a, b, c])
-
- #replace 0 w/ d should raise error (SampleAlg already in list)
- self.assertRaises(KeyError, cc.__setitem__, 0, d)
- self.assertEquals(list(cc), [a, b, c])
-
- #replace 0 w/ e
- cc[0] = e
- self.assertEquals(list(cc), [e, b, c])
-
- #replace 2 w/ d
- cc[2] = d
- self.assertEquals(list(cc), [e, b, d])
-
- #replace -1 w/ c
- cc[-1] = c
- self.assertEquals(list(cc), [e, b, c])
-
- #replace -2 w/ d should raise error
- self.assertRaises(KeyError, cc.__setitem__, -2, d)
- self.assertEquals(list(cc), [e, b, c])
-
- def test_21_append(self):
- "test CryptContext.__setitem__"
- cc = CryptContext([UnsaltedAlg])
- a, = cc
- b = SaltedAlg()
- c = SampleAlg()
- d = SampleAlg()
-
- self.assertEquals(list(cc), [a])
-
- #try append
- cc.append(b)
- self.assertEquals(list(cc), [a, b])
-
- #and again
- cc.append(c)
- self.assertEquals(list(cc), [a, b, c])
-
- #try append dup
- self.assertRaises(KeyError, cc.append, d)
- self.assertEquals(list(cc), [a, b, c])
-
- def test_20_insert(self):
- "test CryptContext.insert"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- d = SampleAlg()
- self.assertIsNot(c, d)
- e = pwhash.Md5Crypt()
- f = pwhash.Sha512Crypt()
- g = pwhash.UnixCrypt()
-
- #check baseline
- self.assertEquals(list(cc), [a, b, c])
-
- #inserting d at 0 should raise error (SampleAlg already in list)
- self.assertRaises(KeyError, cc.insert, 0, d)
- self.assertEquals(list(cc), [a, b, c])
-
- #insert e at start
- cc.insert(0, e)
- self.assertEquals(list(cc), [e, a, b, c])
-
- #insert f at end
- cc.insert(-1, f)
- self.assertEquals(list(cc), [e, a, b, f, c])
-
- #insert g at end
- cc.insert(5, g)
- self.assertEquals(list(cc), [e, a, b, f, c, g])
-
- #=========================================================
- #3 list dellers
- #=========================================================
- def test_30_remove(self):
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- d = SampleAlg()
- self.assertIsNot(c, d)
-
- self.assertEquals(list(cc), [a, b, c])
-
- self.assertRaises(ValueError, cc.remove, d)
- self.assertEquals(list(cc), [a, b, c])
-
- cc.remove(a)
- self.assertEquals(list(cc), [b, c])
-
- self.assertRaises(ValueError, cc.remove, a)
- self.assertEquals(list(cc), [b, c])
-
- def test_31_discard(self):
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- d = SampleAlg()
- self.assertIsNot(c, d)
-
- self.assertEquals(list(cc), [a, b, c])
-
- self.assertEquals(cc.discard(d), False)
- self.assertEquals(list(cc), [a, b, c])
-
- self.assertEquals(cc.discard(a), True)
- self.assertEquals(list(cc), [b, c])
-
- self.assertEquals(cc.discard(a), False)
- self.assertEquals(list(cc), [b, c])
-
- #=========================================================
- #4 list composition
- #=========================================================
-
- def test_40_add(self, lsc=False):
- "test CryptContext + list"
- #build and join cc to list
- a = UnsaltedAlg()
- b = SaltedAlg()
- c = SampleAlg()
- cc = CryptContext([a, b, c])
- ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt]
- if lsc:
- ls = CryptContext(ls)
- cc2 = cc + ls
-
- #verify types
- self.assertIsInstance(cc, CryptContext)
- self.assertIsInstance(cc2, CryptContext)
- self.assertIsInstance(ls, CryptContext if lsc else list)
-
- #verify elements
- self.assertIsNot(cc, ls)
- self.assertIsNot(cc, cc2)
- self.assertIsNot(ls, cc2)
-
- #verify cc
- a, b, c = cc
- self.assertIsInstance(a, UnsaltedAlg)
- self.assertIsInstance(b, SaltedAlg)
- self.assertIsInstance(c, SampleAlg)
-
- #verify ls
- d, e = ls
- if lsc:
- self.assertIsInstance(d, pwhash.Md5Crypt)
- self.assertIsInstance(e, pwhash.Sha512Crypt)
- else:
- self.assertIs(d, pwhash.Md5Crypt)
- self.assertIs(e, pwhash.Sha512Crypt)
-
- #verify cc2
- a2, b2, c2, d2, e2 = cc2
- self.assertIs(a2, a)
- self.assertIs(b2, b)
- self.assertIs(c2, c)
- if lsc:
- self.assertIs(d2, d)
- self.assertIs(e2, e)
- else:
- self.assertIsInstance(d2, pwhash.Md5Crypt)
- self.assertIsInstance(e2, pwhash.Sha512Crypt)
-
- def test_41_add(self):
- "test CryptContext + CryptContext"
- self.test_40_add(lsc=True)
-
- def test_42_iadd(self, lsc=False):
- "test CryptContext += list"
- #build and join cc to list
- a = UnsaltedAlg()
- b = SaltedAlg()
- c = SampleAlg()
- cc = CryptContext([a, b, c])
- ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt]
- if lsc:
- ls = CryptContext(ls)
-
- #baseline
- self.assertEquals(list(cc), [a, b, c])
- self.assertIsInstance(cc, CryptContext)
- self.assertIsInstance(ls, CryptContext if lsc else list)
- if lsc:
- d, e = ls
- self.assertIsInstance(d, pwhash.Md5Crypt)
- self.assertIsInstance(e, pwhash.Sha512Crypt)
-
- #add
- cc += ls
-
- #verify types
- self.assertIsInstance(cc, CryptContext)
- self.assertIsInstance(ls, CryptContext if lsc else list)
-
- #verify elements
- self.assertIsNot(cc, ls)
-
- #verify cc
- a2, b2, c2, d2, e2 = cc
- self.assertIs(a2, a)
- self.assertIs(b2, b)
- self.assertIs(c2, c)
- if lsc:
- self.assertIs(d2, d)
- self.assertIs(e2, e)
- else:
- self.assertIsInstance(d2, pwhash.Md5Crypt)
- self.assertIsInstance(e2, pwhash.Sha512Crypt)
-
- #verify ls
- d, e = ls
- if lsc:
- self.assertIsInstance(d, pwhash.Md5Crypt)
- self.assertIsInstance(e, pwhash.Sha512Crypt)
- else:
- self.assertIs(d, pwhash.Md5Crypt)
- self.assertIs(e, pwhash.Sha512Crypt)
-
- def test_43_iadd(self):
- "test CryptContext += CryptContext"
- self.test_42_iadd(lsc=True)
-
- def test_44_extend(self):
- a = UnsaltedAlg()
- b = SaltedAlg()
- c = SampleAlg()
- cc = CryptContext([a, b, c])
- ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt]
-
- cc.extend(ls)
-
- a2, b2, c2, d2, e2 = cc
- self.assertIs(a2, a)
- self.assertIs(b2, b)
- self.assertIs(c2, c)
- self.assertIsInstance(d2, pwhash.Md5Crypt)
- self.assertIsInstance(e2, pwhash.Sha512Crypt)
-
- self.assertRaises(KeyError, cc.extend, [pwhash.Sha512Crypt ])
- self.assertRaises(KeyError, cc.extend, [pwhash.Sha512Crypt() ])
-
- #=========================================================
- #5 basic crypt interface
- #=========================================================
- def test_50_resolve(self):
- "test CryptContext.resolve()"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- self.assertEquals(cc.resolve('unsalted'), a)
- self.assertEquals(cc.resolve('salted'), b)
- self.assertEquals(cc.resolve('sample'), c)
- self.assertEquals(cc.resolve('md5-crypt'), None)
-
- self.assertEquals(cc.resolve(['unsalted']), a)
- self.assertEquals(cc.resolve(['md5-crypt']), None)
- self.assertEquals(cc.resolve(['unsalted', 'salted', 'md5-crypt']), b)
-
- def test_51_identify(self):
- "test CryptContext.identify"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- for crypt in (a, b, c):
- h = crypt.encrypt("test")
- self.assertEquals(cc.identify(h, resolve=True), crypt)
- self.assertEquals(cc.identify(h), crypt.name)
-
- self.assertEquals(cc.identify('$1$232323123$1287319827', resolve=True), None)
- self.assertEquals(cc.identify('$1$232323123$1287319827'), None)
-
- #make sure "None" is accepted
- self.assertEquals(cc.identify(None), None)
-
- def test_52_encrypt_and_verify(self):
- "test CryptContext.encrypt & verify"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- #check encrypt/id/verify pass for all algs
- for crypt in (a, b, c):
- h = cc.encrypt("test", alg=crypt.name)
- self.assertEquals(cc.identify(h, resolve=True), crypt)
- self.assertEquals(cc.verify('test', h), True)
- self.assertEquals(cc.verify('notest', h), False)
-
- #check default alg
- h = cc.encrypt("test")
- self.assertEquals(cc.identify(h, resolve=True), c)
-
- #check verify using algs
- self.assertEquals(cc.verify('test', h, alg='sample'), True)
- self.assertEquals(cc.verify('test', h, alg='salted'), False)
-
- def test_53_encrypt_salting(self):
- "test CryptContext.encrypt salting options"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
- self.assert_(c.has_salt)
-
- h = cc.encrypt("test")
- self.assertEquals(cc.identify(h, resolve=True), c)
-
- h2 = cc.encrypt("test", h)
- self.assertEquals(cc.identify(h2, resolve=True), c)
- self.assertNotEquals(h2, h)
-
- h3 = cc.encrypt("test", h, keep_salt=True)
- self.assertEquals(cc.identify(h3, resolve=True), c)
- self.assertEquals(h3, h)
-
- def test_54_verify_empty(self):
- "test CryptContext.verify allows hash=None"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- self.assertEquals(cc.verify('xxx', None), False)
- for crypt in cc:
- self.assertEquals(cc.verify('xxx', None, alg=crypt.name), False)
-
-#XXX: haven't decided if this should be part of protocol
-## def test_55_verify_empty_secret(self):
-## "test CryptContext.verify allows secret=None"
-## cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
-## h = cc.encrypt("test")
-## self.assertEquals(cc.verify(None,h), False)
-
- #=========================================================
- #6 crypt-enhanced list interface
- #=========================================================
- def test_60_getitem(self):
- "test CryptContext.__getitem__[algname]"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- #verify getitem
- self.assertEquals(cc['unsalted'], a)
- self.assertEquals(cc['salted'], b)
- self.assertEquals(cc['sample'], c)
- self.assertRaises(KeyError, cc.__getitem__, 'md5-crypt')
-
- def test_61_get(self):
- "test CryptContext.get(algname)"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- #verify getitem
- self.assertEquals(cc.get('unsalted'), a)
- self.assertEquals(cc.get('salted'), b)
- self.assertEquals(cc.get('sample'), c)
- self.assertEquals(cc.get('md5-crypt'), None)
-
- def test_62_index(self):
- "test CryptContext.index(algname)"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
-
- #verify getitem
- self.assertEquals(cc.index('unsalted'), 0)
- self.assertEquals(cc.index('salted'), 1)
- self.assertEquals(cc.index('sample'), 2)
- self.assertEquals(cc.index('md5-crypt'), -1)
-
- def test_63_contains(self):
- "test CryptContext.__contains__(algname)"
- #create crypt context
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- self.assertEquals('salted' in cc, True)
- self.assertEquals('unsalted' in cc, True)
- self.assertEquals('sample' in cc, True)
- self.assertEquals('md5-crypt' in cc, False)
-
- def test_64_keys(self):
- "test CryptContext.keys()"
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- self.assertEquals(cc.keys(), ['unsalted', 'salted', 'sample'])
-
- def test_65_remove(self):
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- self.assertEquals(list(cc), [a, b, c])
-
- self.assertRaises(KeyError, cc.remove, 'md5-crypt')
- self.assertEquals(list(cc), [a, b, c])
-
- cc.remove('unsalted')
- self.assertEquals(list(cc), [b, c])
-
- self.assertRaises(KeyError, cc.remove, 'unsalted')
- self.assertEquals(list(cc), [b, c])
-
- def test_66_discard(self):
- cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg])
- a, b, c = cc
-
- self.assertEquals(list(cc), [a, b, c])
-
- self.assertEquals(cc.discard('md5-crypt'), False)
- self.assertEquals(list(cc), [a, b, c])
-
- self.assertEquals(cc.discard('unsalted'), True)
- self.assertEquals(list(cc), [b, c])
-
- self.assertEquals(cc.discard('unsalted'), False)
- self.assertEquals(list(cc), [b, c])
- #=========================================================
- #eoc
- #=========================================================
-
-#=========================================================
-#quick access functions
-#=========================================================
-class QuickAccessTest(TestCase):
- "test quick access functions"
-
- crypt_cases = [ UnixCryptTest, Md5CryptTest, Sha256CryptTest]
- if BCryptTest:
- crypt_cases.append(BCryptTest)
- crypt_cases.extend([ Sha512CryptTest ])
-
- def test_00_identify(self):
- "test pwhash.identify()"
- identify = pwhash.identify
- for cc in self.crypt_cases:
- name = cc.alg.name
- for _, hash in cc.positive_knowns:
- self.assertEqual(identify(hash), name)
- for _, hash in cc.negative_knowns:
- self.assertEqual(identify(hash), name)
- for hash in cc.negative_identify:
- self.assertNotEqual(identify(hash), name)
- for hash in cc.invalid_identify:
- self.assertEqual(identify(hash), None)
-
- def test_01_verify(self):
- "test pwhash.verify()"
- verify = pwhash.verify
- for cc in self.crypt_cases:
- name = cc.alg.name
- for secret, hash in cc.positive_knowns[:3]:
- self.assert_(verify(secret, hash))
- self.assert_(verify(secret, hash, alg=name))
- for secret, hash in cc.negative_knowns[:3]:
- self.assert_(not verify(secret, hash))
- self.assert_(not verify(secret, hash, alg=name))
- for hash in cc.invalid_identify[:3]:
- #context should raise ValueError because can't be identified
- self.assertRaises(ValueError, verify, secret, hash)
-
- def test_02_encrypt(self):
- "test pwhash.encrypt()"
- identify = pwhash.identify
- verify = pwhash.verify
- encrypt = pwhash.encrypt
- for cc in self.crypt_cases:
- alg = cc.alg.name
- s = 'test'
- h = encrypt(s, alg=alg)
- self.assertEqual(identify(h), alg)
- self.assertEqual(verify(s, h), True)
- h2 = encrypt(s, h)
- self.assertEqual(identify(h2), alg)
- self.assertEqual(verify(s, h2, alg=alg), True)
-
- def test_04_default_context(self):
- "test pwhash.default_context contents"
- dc = pwhash.default_context
- for case in self.crypt_cases:
- self.assert_(case.alg.name in dc)
-
- last = 'sha512-crypt'
- self.assertEqual(dc.keys()[-1], last)
- h = dc.encrypt("test")
- self.assertEqual(dc.identify(h), last)
- self.assertEqual(dc.verify('test', h, alg=last), True)
-
-#=========================================================
#EOF
#=========================================================
diff --git a/passlib/tests/test_sha_crypt.py b/passlib/tests/test_sha_crypt.py
new file mode 100644
index 0000000..1e896a6
--- /dev/null
+++ b/passlib/tests/test_sha_crypt.py
@@ -0,0 +1,130 @@
+"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import hashlib
+import warnings
+from logging import getLogger
+#site
+#pkg
+from passlib import hash as pwhash
+from passlib.tests.utils import TestCase, enable_suite
+from passlib.tests.test_hash import _CryptTestCase as CryptTestCase
+from passlib.util import H64
+import passlib.hash.sha_crypt as mod
+#module
+log = getLogger(__name__)
+
+#=========================================================
+#test raw sha-crypt implementation
+#=========================================================
+class Sha512BackendTest(TestCase):
+ "test sha512-crypt backend against specification unittest"
+ case_prefix = "sha-crypt backend"
+
+ #NOTE: these test cases taken from spec definition at http://www.akkadia.org/drepper/SHA-crypt.txt
+ cases512 = [
+ #salt-hash, secret, result
+ ("$6$saltstring", "Hello world!",
+ "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu"
+ "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ),
+
+ ( "$6$rounds=10000$saltstringsaltstring", "Hello world!",
+ "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"
+ "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ),
+
+ ( "$6$rounds=5000$toolongsaltstring", "This is just a test",
+ "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ"
+ "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ),
+
+ ( "$6$rounds=1400$anotherlongsaltstring",
+ "a very much longer text to encrypt. This one even stretches over more"
+ "than one line.",
+ "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP"
+ "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ),
+
+ ( "$6$rounds=77777$short",
+ "we have a short salt string but not a short password",
+ "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g"
+ "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ),
+
+ ( "$6$rounds=123456$asaltof16chars..", "a short string",
+ "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc"
+ "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ),
+
+ ( "$6$rounds=10$roundstoolow", "the minimum number is still observed",
+ "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x"
+ "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ),
+ ]
+ def test512(self):
+ crypt = mod.Sha512Crypt
+ for hash, secret, result in self.cases512:
+
+ #parse hash
+ rec = crypt._parse(hash)
+
+ #ensure identifier read correctly
+ self.assertEqual(rec.alg, '6')
+
+ #encrypt secret, preserving rounds & salt
+ out = crypt.encrypt(secret, hash, keep_salt=True)
+
+ #make sure we got expected result back
+ self.assertEqual(out, result, "hash=%r secret=%r" % (hash, secret))
+
+ #parse result and check that salt was truncated to max 16 chars
+ rec2 = crypt._parse(out)
+ if len(rec.salt) > 16:
+ #spec sez we can truncate salt
+ self.assertEqual(rec2.salt, rec.salt[:16], "hash=%r secret=%r:" % (hash, secret))
+ else:
+ self.assertEqual(rec2.salt, rec.salt, "hash=%r secret=%r:" % (hash, secret))
+
+#=========================================================
+#test frontend classes
+#=========================================================
+class Sha256CryptTest(CryptTestCase):
+ alg = mod.Sha256Crypt
+ positive_knowns = (
+ ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
+ (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'),
+ ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'),
+ ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'),
+ ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'),
+ )
+ invalid_identify = (
+ #bad char in otherwise correct hash
+ '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe!ZGsGx2aBvxTvDFI613c3'
+ )
+ negative_identify = (
+ #other hashes
+ '!gAwTx2l6NADI',
+ '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc',
+ '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox',
+ )
+
+class Sha512CryptTest(CryptTestCase):
+ alg = mod.Sha512Crypt
+ positive_knowns = (
+ ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'),
+ (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'),
+ ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'),
+ ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'),
+ ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'),
+ )
+ negative_identify = (
+ #other hashes
+ '!gAwTx2l6NADI',
+ '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3',
+ '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox',
+ )
+ invalid_identify = (
+ #bad char in otherwise correct hash
+ '$6$rounds=11021$KsvQipYPWpr9!wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
+ )
+
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/tests/test_unix_crypt.py b/passlib/tests/test_unix_crypt.py
new file mode 100644
index 0000000..a8ad7e3
--- /dev/null
+++ b/passlib/tests/test_unix_crypt.py
@@ -0,0 +1,121 @@
+"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import hashlib
+import warnings
+from logging import getLogger
+#site
+#pkg
+from passlib.tests.utils import TestCase, enable_suite
+from passlib.util import H64
+from passlib._unix_crypt import crypt as builtin_crypt
+import passlib.hash.unix_crypt as mod
+from passlib.tests.test_hash import _CryptTestCase as CryptTestCase
+#module
+log = getLogger(__name__)
+
+#=========================================================
+#test frontend class
+#=========================================================
+class UnixCryptTest(CryptTestCase):
+ "test UnixCrypt algorithm"
+ alg = mod.UnixCrypt
+ positive_knowns = (
+ #secret, example hash which matches secret
+ ('', 'OgAwTx2l6NADI'),
+ (' ', '/Hk.VPuwQTXbc'),
+ ('test', 'N1tQbOFcM5fpg'),
+ ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'),
+ ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'),
+ ('AlOtBsOl', 'cEpWz5IUCShqM'),
+ (u'hell\u00D6', 'saykDgk3BPZ9E'),
+ )
+ invalid_identify = (
+ #bad char in otherwise correctly formatted hash
+ '!gAwTx2l6NADI',
+ )
+ negative_identify = (
+ #hashes using other algs, which shouldn't match this algorithm
+ '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc',
+ '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'
+ )
+
+#=========================================================
+#test activate backend (stored in mod._crypt)
+#=========================================================
+class UnixCryptBackendTest(TestCase):
+ "test builtin unix crypt backend"
+ case_prefix = "builtin crypt() backend"
+
+ def get_crypt(self):
+ return builtin_crypt
+
+ positive_knowns = UnixCryptTest.positive_knowns
+
+ def test_knowns(self):
+ "test known crypt results"
+ crypt = self.get_crypt()
+ for secret, result in self.positive_knowns:
+
+ #make sure crypt verifies preserving just salt
+ out = crypt(secret, result[:2])
+ self.assertEqual(out, result)
+
+ #make sure crypt verifies preseving salt + fragment of known hash
+ out = crypt(secret, result[:6])
+ self.assertEqual(out, result)
+
+ #make sure crypt verifies using whole known hash
+ out = crypt(secret, result)
+ self.assertEqual(out, result)
+
+ #TODO: deal with border cases where host crypt & bps crypt differ
+ # (none of which should impact the normal use cases)
+ #border cases:
+ # no salt given, empty salt given, 1 char salt
+ # salt w/ non-b64 chars (linux crypt handles this _somehow_)
+ #test that \x00 is NOT allowed
+ #test that other chars _are_ allowed
+
+ def test_null_in_key(self):
+ "test null chars in secret"
+ crypt = self.get_crypt()
+ #NOTE: this is done to match stdlib crypt behavior.
+ # would raise ValueError if otherwise had free choice
+ self.assertRaises(ValueError, crypt, "hello\x00world", "ab")
+
+ def test_invalid_salt(self):
+ "test invalid salts"
+ crypt = self.get_crypt()
+
+ #NOTE: stdlib crypt's behavior is to return "" in this case.
+ # passlib wraps stdlib crypt so it raises ValueError
+ self.assertRaises(ValueError, crypt, "fooey","")
+
+ #NOTE: stdlib crypt's behavior is rather bizarre in this case
+ # (see wrapper in passlib.hash.unix_crypt).
+ # passlib wraps stdlib crypt so it raises ValueError
+ self.assertRaises(ValueError, crypt, "fooey","f")
+
+ #FIXME: stdlib crypt does something unpredictable
+ #if passed salt chars outside of H64.CHARS range.
+ #not sure *what* it's algorithm is. should figure that out.
+ # until then, passlib wraps stdlib crypt so this causes ValueError
+ self.assertRaises(ValueError, crypt, "fooey", "a@")
+
+if mod.backend != "builtin":
+ #NOTE: this will generally be the stdlib implementation,
+ #which of course is correct, so doing this more to detect deviations in builtin implementation
+ class ActiveUnixCryptBackendTest(UnixCryptBackendTest):
+ "test active unix crypt backend"
+ case_prefix = mod.backend + " crypt() backend"
+
+ def get_crypt(self):
+ return mod.crypt
+
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/tests/test_util.py b/passlib/tests/test_util.py
index d8b872b..56ded3d 100644
--- a/passlib/tests/test_util.py
+++ b/passlib/tests/test_util.py
@@ -4,6 +4,7 @@
#=========================================================
#core
import sys
+import random
#site
#pkg
#module
@@ -87,5 +88,53 @@ class BytesTest(TestCase):
self.assertEqual(util.bytes_to_list('\x00\x00\x01', order="native"), [0, 0, 1])
#=========================================================
+#hash64
+#=========================================================
+class Test_H64(TestCase):
+ "test H64 codec functions"
+ case_prefix = "H64 codec"
+
+ def test_encode_1_offset(self):
+ self.assertFunctionResults(util.H64.encode_1_offset,[
+ ("z1", "\xff", 0),
+ ("..", "\x00", 0),
+ ])
+
+ def test_encode_2_offsets(self):
+ self.assertFunctionResults(util.H64.encode_2_offsets,[
+ (".wD", "\x00\xff", 0, 1),
+ ("z1.", "\xff\x00", 0, 1),
+ ("z1.", "\x00\xff", 1, 0),
+ ])
+
+ def test_encode_3_offsets(self):
+ self.assertFunctionResults(util.H64.encode_3_offsets,[
+ #move through each byte, keep offsets
+ ("..kz", "\x00\x00\xff", 0, 1, 2),
+ (".wD.", "\x00\xff\x00", 0, 1, 2),
+ ("z1..", "\xff\x00\x00", 0, 1, 2),
+
+ #move through each offset, keep bytes
+ (".wD.", "\x00\x00\xff", 0, 2, 1),
+ ("z1..", "\x00\x00\xff", 2, 0, 1),
+ ])
+
+ def test_randstr(self):
+ #override default rng so we can get predictable values
+ rng = random.Random()
+ def wrapper(*a, **k):
+ rng.seed(1234)
+ k['rng'] = rng
+ return util.H64.randstr(*a, **k)
+ self.assertFunctionResults(wrapper,[
+ ("", 0),
+ ("x", 1),
+ ("xQ", 2),
+ ("xQ.uwZe3lD/mKbb7", 16),
+ ("xQ.uwZe3lD/mKbb795.Tx2WRa3ZFXdSK", 32),
+ ])
+
+
+#=========================================================
#EOF
#=========================================================
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 5ca215e..1f56fc5 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -19,6 +19,15 @@ __all__ = [
#=========================================================
class Params(object):
"helper to represent params for function call"
+
+ @classmethod
+ def norm(cls, value):
+ if isinstance(value, cls):
+ return value
+ if isinstance(value, (list,tuple)):
+ return cls(*value)
+ return cls(**value)
+
def __init__(self, *args, **kwds):
self.args = args
self.kwds = kwds
@@ -43,17 +52,17 @@ class TestCase(unittest.TestCase):
this class mainly overriddes many of the common assert methods
so to give a default message which includes the values
- as well as the class-specific message_prefix string.
+ as well as the class-specific case_prefix string.
this latter bit makes the output of various test cases
easier to distinguish from eachother.
"""
- message_prefix = None
+ case_prefix = None
def __init__(self, *a, **k):
- #set the doc strings for all test messages to begin w/ message_prefix
+ #set the doc strings for all test messages to begin w/ case_prefix
#yes, this is incredibly hacked.
- prefix = self.message_prefix
+ prefix = self.case_prefix
if prefix:
if callable(prefix):
prefix = prefix()
@@ -120,6 +129,7 @@ class TestCase(unittest.TestCase):
and remaining args and kwds are passed to function.
"""
for elem in cases:
+ elem = Params.norm(elem)
correct = elem.args[0]
result = func(*elem.args[1:], **elem.kwds)
self.assertEqual(result, correct,
diff --git a/passlib/util.py b/passlib/util.py
index c8ea09e..492a851 100644
--- a/passlib/util.py
+++ b/passlib/util.py
@@ -270,5 +270,171 @@ def weighted_choice(rng, source):
raise RuntimeError, "failed to sum weights correctly"
#=================================================================================
+# "hash64" encoding
+#=================================================================================
+
+class H64:
+ """hash64 encoding helpers.
+
+ many of the password hash algorithms in this module
+ use a encoding that maps chunks of 3 bytes ->
+ chunks of 4 characters, in a manner similar (but not compatible with) base64.
+
+ this encoding system appears to have originated with unix-crypt,
+ but is used by md5-crypt, sha-xxx-crypt, and others.
+ this encoded is referred to (within passlib) as hash64 encoding,
+ due to it's use of a strict set of 64 ascii characters.
+
+ notably, bcrypt uses the same scheme, but with a different
+ ordering of the characters. bcrypt hashes cannot be decoded properly
+ with the H64 class.
+ """
+
+ #string of all h64 chars; position in string corresponds to value encoded
+ CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+ @classmethod
+ def randstr(cls, count, rng=srandom):
+ "generate random hash64 string containing specified # of chars; usually used as salt"
+ CHARS = cls.CHARS
+ return ''.join(rng.choice(CHARS) for idx in xrange(count))
+
+ @classmethod
+ def encode_3_offsets(cls, buffer, o1, o2, o3):
+ "do hash64 encode of three bytes at specified offsets in buffer; returns 4 chars"
+ #how 4 char output corresponds to 3 byte input:
+ #
+ #1st character: the six low bits of the first byte (0x3F)
+ #
+ #2nd character: four low bits from the second byte (0x0F) shift left 2
+ # the two high bits of the first byte (0xC0) shift right 6
+ #
+ #3rd character: the two low bits from the third byte (0x03) shift left 4
+ # the four high bits from the second byte (0xF0) shift right 4
+ #
+ #4th character: the six high bits from the third byte (0xFC) shift right 2
+ CHARS = cls.CHARS
+ v1 = ord(buffer[o1])
+ v2 = ord(buffer[o2])
+ v3 = ord(buffer[o3])
+ return CHARS[v1&0x3F] + \
+ CHARS[((v2&0x0F)<<2) + (v1>>6)] + \
+ CHARS[((v3&0x03)<<4) + (v2>>4)] + \
+ CHARS[v3>>2]
+
+ @classmethod
+ def encode_2_offsets(cls, buffer, o1, o2):
+ "do hash64 encode of two bytes at specified offsets in buffer; 2 missing msg set null; returns 3 chars"
+ CHARS = cls.CHARS
+ v1 = ord(buffer[o1])
+ v2 = ord(buffer[o2])
+ return CHARS[v1&0x3F] + \
+ CHARS[((v2&0x0F)<<2) + (v1>>6)] + \
+ CHARS[(v2>>4)]
+
+ @classmethod
+ def encode_1_offset(cls, buffer, o1):
+ "do hash64 encode of single byte at specified offset in buffer; 4 missing msb set null; returns 2 chars"
+ CHARS = cls.CHARS
+ v1 = ord(buffer[o1])
+ return CHARS[v1&0x3F] + CHARS[v1>>6]
+
+ #old code, never used...
+ ###reverse map of char -> value
+ ##CHARIDX = dict( (c,i) for i,c in enumerate(CHARS))
+ ##def _enc64(value, offset=0, num=False):
+ ## if num:
+ ## x, y, z = value[offset], value[offset+1], value[offset+2]
+ ## else:
+ ## x, y, z = ord(value[offset]), ord(value[offset+1]), ord(value[offset+2])
+ ## #xxxxxx xxyyyy yyyyzz zzzzzz
+ ## #aaaaaa bbbbbb cccccc dddddd
+ ## a = (x >> 2) # x [8..3]
+ ## b = ((x & 0x3) << 4) + (y>>4) # x[2..1] + y [8..5]
+ ## c = ((y & 0xf) << 2) + (z>>6) #y[4..1] + d[8..7]
+ ## d = z & 0x3f
+ ## return CHARS[a] + CHARS[b] + CHARS[c] + CHARS[d]
+ ##
+ ##def _dec64(value, offset=0, num=False):
+ ## a, b, c, d = CHARIDX[value[offset]], CHARIDX[value[offset+1]], \
+ ## CHARIDX[value[offset+2]], CHARIDX[value[offset+3]]
+ ## #aaaaaabb bbbbcccc ccdddddd
+ ## #xxxxxxxx yyyyyyyy zzzzzzzz
+ ## x = (a<<2) + (b >> 4) #a[6..1] + b[6..5]
+ ## y = ((b & 0xf) << 4) + (c >> 2) #b[4..1] + c[6..3]
+ ## z = ((c & 0x3) << 6) + d #c[2..1] + d[6..1]
+ ## if num:
+ ## return x, y, z
+ ## return chr(x) + chr(y) + chr(z)
+ ##
+ ##def h64_encode(value, pad=False, num=False):
+ ## "encode string of bytes into hash64 format"
+ ## if num:
+ ## value = list(value)
+ ## #pad value to align w/ 3 byte chunks
+ ## x = len(value) % 3
+ ## if x == 2:
+ ## if num:
+ ## value += [0]
+ ## else:
+ ## value += "\x00"
+ ## p = 1
+ ## elif x == 1:
+ ## if num:
+ ## value += [0, 0]
+ ## else:
+ ## value += "\x00\x00"
+ ## p = 2
+ ## else:
+ ## p = 0
+ ## assert len(value) % 3 == 0
+ ## out = "".join( _enc64(value, offset, num=num) for offset in xrange(0, len(value), 3))
+ ## assert len(out) % 4 == 0
+ ## if p:
+ ## if pad:
+ ## out = out[:-p] + "=" * p
+ ## else:
+ ## out = out[:-p]
+ ## return out
+ ##
+ ##def h64_decode(value, pad=False, num=False):
+ ## "decode string of bytes from hash64 format"
+ ## if value.endswith("="):
+ ## assert len(value) % 4 == 0, value
+ ## if value.endswith('=='):
+ ## p = 2
+ ## value = value[:-2] + '..'
+ ## else:
+ ## p = 1
+ ## value = value[:-1] + '.'
+ ## else:
+ ## #else add padding if needed
+ ## x = len(value) % 4
+ ## if x == 0:
+ ## p = 0
+ ## elif pad:
+ ## raise ValueError, "size must be multiple of 4"
+ ## elif x == 3:
+ ## p = 1
+ ## value += "."
+ ## elif x == 2:
+ ## p = 2
+ ## value += ".."
+ ## elif x == 1:
+ ## p = 3
+ ## value += "..."
+ ## assert len(value) % 4 == 0, value
+ ## if num:
+ ## out = []
+ ## for offset in xrange(0, len(value), 4):
+ ## out.extend(_dec64(value, offset, num=True))
+ ## else:
+ ## out = "".join( _dec64(value, offset) for offset in xrange(0, len(value), 4))
+ ## assert len(out) % 3 == 0
+ ## if p: #strip out garbage chars
+ ## out = out[:-p]
+ ## return out
+
+#=================================================================================
#eof
#=================================================================================