summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-03-10 17:57:38 -0500
committerEli Collins <elic@assurancetechnologies.com>2012-03-10 17:57:38 -0500
commitbef2baa6b37b3dc70e96b5b5b285a86f59a9220a (patch)
tree8705072ec5be7a56d3c4e97181ad9f0f15e2b4f1 /passlib
parentd3c7d16915f7ef3919245f211b8dab8ae35ade70 (diff)
downloadpasslib-bef2baa6b37b3dc70e96b5b5b285a86f59a9220a.tar.gz
cleanup of scram hash; improved norm_digest_name() and moved it to utils.pbkdf2
Diffstat (limited to 'passlib')
-rw-r--r--passlib/handlers/scram.py483
-rw-r--r--passlib/tests/test_handlers.py36
-rw-r--r--passlib/tests/test_utils.py44
-rw-r--r--passlib/utils/pbkdf2.py105
4 files changed, 345 insertions, 323 deletions
diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py
index 1c31562..5c9c65d 100644
--- a/passlib/handlers/scram.py
+++ b/passlib/handlers/scram.py
@@ -1,16 +1,4 @@
-"""passlib.handlers.scram - SCRAM hash
-
-Notes
-=====
-Working on hash to format to support storing SCRAM protocol information
-server-side.
-
-passlib issue - https://code.google.com/p/passlib/issues/detail?id=23
-
-scram protocol - http://tools.ietf.org/html/rfc5802
- http://tools.ietf.org/html/rfc5803
-
-"""
+"""passlib.handlers.scram - hash for SCRAM credential storage"""
#=========================================================
#imports
#=========================================================
@@ -28,7 +16,7 @@ from passlib.utils import ab64_decode, ab64_encode, consteq, saslprep, \
to_native_str, xor_bytes
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, \
itervalues, PY3, u, unicode
-from passlib.utils.pbkdf2 import pbkdf2, get_prf
+from passlib.utils.pbkdf2 import pbkdf2, get_prf, norm_hash_name
import passlib.utils.handlers as uh
#pkg
#local
@@ -37,109 +25,7 @@ __all__ = [
]
#=========================================================
-# helpers
-#=========================================================
-# set of known iana names --
-# http://www.iana.org/assignments/hash-function-text-names
-iana_digests = frozenset(["md2", "md5", "sha-1", "sha-224", "sha-256",
- "sha-384", "sha-512"])
-
-# cache for norm_digest_name()
-_ndn_cache = {}
-
-def norm_digest_name(name):
- """normalize digest names to IANA hash function name.
-
- :arg name:
- name can be a Python :mod:`~hashlib` digest name,
- a SCRAM mechanism name, etc; case insensitive.
-
- input can be either unicode or bytes.
-
- :returns:
- native string containing lower-case IANA hash function name.
- if IANA has not assigned one, this will make a guess as to
- what the IANA-style representation should be.
- """
- # check cache
- try:
- return _ndn_cache[name]
- except KeyError:
- pass
- key = name
-
- # normalize case
- name = name.strip().lower().replace("_","-")
-
- # extract digest from scram mechanism name
- if name.startswith("scram-"):
- name = name[6:]
- if name.endswith("-plus"):
- name = name[:-5]
-
- # handle some known aliases
- if name not in iana_digests:
- if name == "sha1":
- name = "sha-1"
- else:
- m = re.match("^sha-?2-(\d{3})$", name)
- if m:
- name = "sha-" + m.group(1)
-
- # run heuristics if not an official name
- if name not in iana_digests:
-
- # add hyphen between hash name and digest size;
- # e.g. "ripemd160" -> "ripemd-160"
- m = re.match("^([a-z]+)(\d{3,4})$", name)
- if m:
- name = m.group(1) + "-" + m.group(2)
-
- # remove hyphen between hash name & version (e.g. MD-5 -> MD5,
- # FOO-2-256 -> FOO2-256). note that SHA-1 is an exception to this,
- # but that's taken care of above.
- m = re.match("^([a-z]+)-(\d)(-\d{3,4})?$", name)
- if m:
- name = m.group(1) + m.group(2) + (m.group(3) or '')
-
- # check for invalid chars
- if re.search("[^a-z0-9-]", name):
- raise ValueError("invalid characters in digest name: %r" % (name,))
-
- # issue warning if not in the expected format,
- # this might be a sign of some strange input
- # (and digest probably won't be found)
- m = re.match("^([a-z]{2,}\d?)(-\d{3,4})?$", name)
- if not m:
- warn("encountered oddly named digest: %r" % (name,),
- PasslibWarning)
-
- # store in cache
- _ndn_cache[key] = name
- return name
-
-def iana_to_hashlib(name):
- "adapt iana hash name -> hashlib hash name"
- # NOTE: assumes this has been run through norm_digest_name()
- # XXX: this works for all known cases for now, might change in future.
- return name.replace("-","")
-
-_gds_cache = {}
-
-def _get_digest_size(name):
- "get size of digest"
- try:
- return _gds_cache[name]
- except KeyError:
- pass
- key = name
- name = iana_to_hashlib(norm_digest_name(name))
- value = hashlib.new(name).digest_size
- _gds_cache[key] = value
- return value
-
-#=========================================================
-#
+# scram credentials hash
#=========================================================
class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"""This class provides a format for storing SCRAM passwords, and follows
@@ -214,7 +100,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
default_algs = ["sha-1", "sha-256", "sha-512"]
# list of algs verify prefers to use, in order.
- _verify_algs = ["sha-256", "sha-512", "sha-384", "sha-224", "sha-1"]
+ _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"]
#=========================================================
# instance attrs
@@ -232,15 +118,15 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
#=========================================================
@classmethod
def extract_digest_info(cls, hash, alg):
- """given scram hash & hash alg, extracts salt, rounds and digest.
+ """return (salt, rounds, digest) for specific hash algorithm.
:arg hash:
- Scram hash stored for desired user
+ :class:`!scram` hash stored for desired user
:arg alg:
Name of digest algorithm (e.g. ``"sha-1"``) requested by client.
- This value is run through :func:`norm_digest_name`,
+ This value is run through :func:`~passlib.utils.pbkdf2.norm_hash_name`,
so it is case-insensitive, and can be the raw SCRAM
mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name,
or the hashlib name.
@@ -251,12 +137,15 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
:returns:
A tuple containing ``(salt, rounds, digest)``,
- where *digest* matches the raw bytes return by
+ where *digest* matches the raw bytes returned by
SCRAM's :func:`Hi` function for the stored password,
the provided *salt*, and the iteration count (*rounds*).
*salt* and *digest* are both raw (unencoded) bytes.
"""
- alg = norm_digest_name(alg)
+ # XXX: this could be sped up by writing custom parsing routine
+ # that just picks out relevant digest, and doesn't bother
+ # with full structure validation each time it's called.
+ alg = norm_hash_name(alg, 'iana')
self = cls.from_string(hash)
chkmap = self.checksum
if not chkmap:
@@ -264,26 +153,29 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
return self.salt, self.rounds, chkmap[alg]
@classmethod
- def extract_digest_algs(cls, hash, hashlib=False):
+ def extract_digest_algs(cls, hash, format="iana"):
"""Return names of all algorithms stored in a given hash.
:arg hash:
- The scram hash to parse
+ The :class:`!scram` hash to parse
- :param hashlib:
- By default this returns a list of IANA compatible names.
- if this is set to `True`, hashlib-compatible names will
- be returned instead.
+ :param format:
+ This changes the naming convention used by the
+ returned algorithm names. By default the names
+ are IANA-compatible; see :func:`~passlib.utils.pbkdf2.norm_hash_name`
+ for possible values.
:returns:
- Returns a list of digest algorithms; e.g. ``["sha-1"]``,
- or ``["sha1"]`` if ``hashlib=True``.
+ Returns a list of digest algorithms; e.g. ``["sha-1"]``
"""
+ # XXX: this could be sped up by writing custom parsing routine
+ # that just picks out relevant names, and doesn't bother
+ # with full structure validation each time it's called.
algs = cls.from_string(hash).algs
- if hashlib:
- return [iana_to_hashlib(alg) for alg in algs]
- else:
+ if format == "iana":
return algs
+ else:
+ return [norm_hash_name(alg, format) for alg in algs]
@classmethod
def derive_digest(cls, password, salt, rounds, alg):
@@ -296,7 +188,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
:arg password: password as unicode or utf-8 encoded bytes.
:arg salt: raw salt as bytes.
:arg rounds: number of iterations.
- :arg alg: name of digest to use (e.g. ``"SHA-1"``).
+ :arg alg: name of digest to use (e.g. ``"sha-1"``).
:returns:
raw bytes of ``SaltedPassword``
@@ -308,7 +200,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
raise TypeError("salt must be bytes")
if rounds < 1:
raise ValueError("rounds must be >= 1")
- alg = iana_to_hashlib(norm_digest_name(alg))
+ alg = norm_hash_name(alg, "hashlib")
return pbkdf2(password, salt, rounds, -1, "hmac-" + alg)
#=========================================================
@@ -389,7 +281,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
if checksum is None:
return None
for alg, digest in iteritems(checksum):
- if alg != norm_digest_name(alg):
+ if alg != norm_hash_name(alg, 'iana'):
raise ValueError("malformed algorithm name in scram hash: %r" %
(alg,))
if len(alg) > 9:
@@ -397,6 +289,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"9 characters: %r" % (alg,))
if not isinstance(digest, bytes):
raise TypeError("digests must be raw bytes")
+ # TODO: verify digest size (if digest is known)
if 'sha-1' not in checksum:
# NOTE: required because of SCRAM spec.
raise ValueError("sha-1 must be in algorithm list of scram hash")
@@ -420,11 +313,11 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
# parse args value
if isinstance(algs, str):
algs = algs.split(",")
- algs = sorted(norm_digest_name(alg) for alg in algs)
+ algs = sorted(norm_hash_name(alg, 'iana') for alg in algs)
if any(len(alg)>9 for alg in algs):
raise ValueError("SCRAM limits alg names to max of 9 characters")
if 'sha-1' not in algs:
- # NOTE: required because of SCRAM spec.
+ # NOTE: required because of SCRAM spec (rfc 5802)
raise ValueError("sha-1 must be in algorithm list of scram hash")
return algs
@@ -457,7 +350,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
)
@classmethod
- def verify(cls, secret, hash, full_verify=False):
+ def verify(cls, secret, hash, full=False):
self = cls.from_string(hash)
chkmap = self.checksum
if not chkmap:
@@ -465,9 +358,9 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
(cls.name, cls.name))
# NOTE: to make the verify method efficient, we just calculate hash
- # of shortest digest by default. apps can pass in "full_verify=True" to
+ # of shortest digest by default. apps can pass in "full=True" to
# check entire hash for consistency.
- if full_verify:
+ if full:
correct = failed = False
for alg, digest in iteritems(chkmap):
other = self._calc_checksum(secret, alg)
@@ -483,7 +376,8 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
else:
failed = True
if correct and failed:
- warning("scram hash verified inconsistently, may be corrupted")
+ warning("scram hash verified inconsistently, may be corrupted",
+ PasslibHashWarning)
return False
else:
return correct
@@ -501,159 +395,158 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
#=========================================================
#=========================================================
-# code used for testing scram against protocol examples
-# during development, not part of public api or used internally.
+# code used for testing scram against protocol examples during development.
#=========================================================
-def _test_reference_scram():
- "quick hack testing scram reference vectors"
- # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
- from passlib.utils.compat import print_
-
- engine = _scram_engine(
- alg="sha-1",
- salt='QSXCR+Q6sek8bf92'.decode("base64"),
- rounds=4096,
- password=u("pencil"),
- )
- print_(engine.digest.encode("base64").rstrip())
-
- msg = engine.format_auth_msg(
- username="user",
- client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
- server_nonce = "3rfcNHYJY1ZVvWVs7j",
- header='c=biws',
- )
-
- cp = engine.get_encoded_client_proof(msg)
- assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
-
- ss = engine.get_encoded_server_sig(msg)
- assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
-
-class _scram_engine(object):
- """helper class for verifying scram hash behavior
- against SCRAM protocol examples. not officially part of Passlib.
-
- takes in alg, salt, rounds, and a digest or password.
-
- can calculate the various keys & messages of the scram protocol.
-
- """
- #=========================================================
- # init
- #=========================================================
-
- @classmethod
- def from_string(cls, hash, alg):
- "create record from scram hash, for given alg"
- return cls(alg, *scram.extract_digest_info(hash, alg))
-
- def __init__(self, alg, salt, rounds, digest=None, password=None):
- self.alg = norm_digest_name(alg)
- self.salt = salt
- self.rounds = rounds
- self.password = password
- if password:
- data = scram.derive_digest(password, salt, rounds, alg)
- if digest and data != digest:
- raise ValueError("password doesn't match digest")
- else:
- digest = data
- elif not digest:
- raise TypeError("must provide password or digest")
- self.digest = digest
-
- #=========================================================
- # frontend methods
- #=========================================================
- def get_hash(self, data):
- "return hash of raw data"
- return hashlib.new(iana_to_hashlib(self.alg), data).digest()
-
- def get_client_proof(self, msg):
- "return client proof of specified auth msg text"
- return xor_bytes(self.client_key, self.get_client_sig(msg))
-
- def get_encoded_client_proof(self, msg):
- return self.get_client_proof(msg).encode("base64").rstrip()
-
- def get_client_sig(self, msg):
- "return client signature of specified auth msg text"
- return self.get_hmac(self.stored_key, msg)
-
- def get_server_sig(self, msg):
- "return server signature of specified auth msg text"
- return self.get_hmac(self.server_key, msg)
-
- def get_encoded_server_sig(self, msg):
- return self.get_server_sig(msg).encode("base64").rstrip()
-
- def format_server_response(self, client_nonce, server_nonce):
- return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
- client_nonce=client_nonce,
- server_nonce=server_nonce,
- rounds=self.rounds,
- salt=self.encoded_salt,
- )
-
- def format_auth_msg(self, username, client_nonce, server_nonce,
- header='c=biws'):
- return (
- 'n={username},r={client_nonce}'
- ','
- 'r={client_nonce}{server_nonce},s={salt},i={rounds}'
- ','
- '{header},r={client_nonce}{server_nonce}'
- ).format(
- username=username,
- client_nonce=client_nonce,
- server_nonce=server_nonce,
- salt=self.encoded_salt,
- rounds=self.rounds,
- header=header,
- )
-
- #=========================================================
- # helpers to calculate & cache constant data
- #=========================================================
- def _calc_get_hmac(self):
- return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
-
- def _calc_client_key(self):
- return self.get_hmac(self.digest, b("Client Key"))
-
- def _calc_stored_key(self):
- return self.get_hash(self.client_key)
-
- def _calc_server_key(self):
- return self.get_hmac(self.digest, b("Server Key"))
-
- def _calc_encoded_salt(self):
- return self.salt.encode("base64").rstrip()
-
- #=========================================================
- # hacks for calculated attributes
- #=========================================================
-
- def __getattr__(self, attr):
- if not attr.startswith("_"):
- f = getattr(self, "_calc_" + attr, None)
- if f:
- value = f()
- setattr(self, attr, value)
- return value
- raise AttributeError("attribute not found")
-
- def __dir__(self):
- cdir = dir(self.__class__)
- attrs = set(cdir)
- attrs.update(self.__dict__)
- attrs.update(attr[6:] for attr in cdir
- if attr.startswith("_calc_"))
- return sorted(attrs)
- #=========================================================
- # eoc
- #=========================================================
+#def _test_reference_scram():
+# "quick hack testing scram reference vectors"
+# # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
+# from passlib.utils.compat import print_
+#
+# engine = _scram_engine(
+# alg="sha-1",
+# salt='QSXCR+Q6sek8bf92'.decode("base64"),
+# rounds=4096,
+# password=u("pencil"),
+# )
+# print_(engine.digest.encode("base64").rstrip())
+#
+# msg = engine.format_auth_msg(
+# username="user",
+# client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
+# server_nonce = "3rfcNHYJY1ZVvWVs7j",
+# header='c=biws',
+# )
+#
+# cp = engine.get_encoded_client_proof(msg)
+# assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
+#
+# ss = engine.get_encoded_server_sig(msg)
+# assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
+#
+#class _scram_engine(object):
+# """helper class for verifying scram hash behavior
+# against SCRAM protocol examples. not officially part of Passlib.
+#
+# takes in alg, salt, rounds, and a digest or password.
+#
+# can calculate the various keys & messages of the scram protocol.
+#
+# """
+# #=========================================================
+# # init
+# #=========================================================
+#
+# @classmethod
+# def from_string(cls, hash, alg):
+# "create record from scram hash, for given alg"
+# return cls(alg, *scram.extract_digest_info(hash, alg))
+#
+# def __init__(self, alg, salt, rounds, digest=None, password=None):
+# self.alg = norm_hash_name(alg)
+# self.salt = salt
+# self.rounds = rounds
+# self.password = password
+# if password:
+# data = scram.derive_digest(password, salt, rounds, alg)
+# if digest and data != digest:
+# raise ValueError("password doesn't match digest")
+# else:
+# digest = data
+# elif not digest:
+# raise TypeError("must provide password or digest")
+# self.digest = digest
+#
+# #=========================================================
+# # frontend methods
+# #=========================================================
+# def get_hash(self, data):
+# "return hash of raw data"
+# return hashlib.new(iana_to_hashlib(self.alg), data).digest()
+#
+# def get_client_proof(self, msg):
+# "return client proof of specified auth msg text"
+# return xor_bytes(self.client_key, self.get_client_sig(msg))
+#
+# def get_encoded_client_proof(self, msg):
+# return self.get_client_proof(msg).encode("base64").rstrip()
+#
+# def get_client_sig(self, msg):
+# "return client signature of specified auth msg text"
+# return self.get_hmac(self.stored_key, msg)
+#
+# def get_server_sig(self, msg):
+# "return server signature of specified auth msg text"
+# return self.get_hmac(self.server_key, msg)
+#
+# def get_encoded_server_sig(self, msg):
+# return self.get_server_sig(msg).encode("base64").rstrip()
+#
+# def format_server_response(self, client_nonce, server_nonce):
+# return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
+# client_nonce=client_nonce,
+# server_nonce=server_nonce,
+# rounds=self.rounds,
+# salt=self.encoded_salt,
+# )
+#
+# def format_auth_msg(self, username, client_nonce, server_nonce,
+# header='c=biws'):
+# return (
+# 'n={username},r={client_nonce}'
+# ','
+# 'r={client_nonce}{server_nonce},s={salt},i={rounds}'
+# ','
+# '{header},r={client_nonce}{server_nonce}'
+# ).format(
+# username=username,
+# client_nonce=client_nonce,
+# server_nonce=server_nonce,
+# salt=self.encoded_salt,
+# rounds=self.rounds,
+# header=header,
+# )
+#
+# #=========================================================
+# # helpers to calculate & cache constant data
+# #=========================================================
+# def _calc_get_hmac(self):
+# return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
+#
+# def _calc_client_key(self):
+# return self.get_hmac(self.digest, b("Client Key"))
+#
+# def _calc_stored_key(self):
+# return self.get_hash(self.client_key)
+#
+# def _calc_server_key(self):
+# return self.get_hmac(self.digest, b("Server Key"))
+#
+# def _calc_encoded_salt(self):
+# return self.salt.encode("base64").rstrip()
+#
+# #=========================================================
+# # hacks for calculated attributes
+# #=========================================================
+#
+# def __getattr__(self, attr):
+# if not attr.startswith("_"):
+# f = getattr(self, "_calc_" + attr, None)
+# if f:
+# value = f()
+# setattr(self, attr, value)
+# return value
+# raise AttributeError("attribute not found")
+#
+# def __dir__(self):
+# cdir = dir(self.__class__)
+# attrs = set(cdir)
+# attrs.update(self.__dict__)
+# attrs.update(attr[6:] for attr in cdir
+# if attr.startswith("_calc_"))
+# return sorted(attrs)
+# #=========================================================
+# # eoc
+# #=========================================================
#=========================================================
#eof
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index 2ab9837..9008406 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -1647,6 +1647,11 @@ class scram_test(HandlerCase):
]
+ # silence norm_hash_name() warning
+ def setUp(self):
+ super(scram_test, self).setUp()
+ warnings.filterwarnings("ignore", r"norm_hash_name\(\): unknown hash")
+
def test_90_algs(self):
"test parsing of 'algs' setting"
def parse(algs, **kwds):
@@ -1764,9 +1769,9 @@ class scram_test(HandlerCase):
self.assertTrue(c2.hash_needs_update(h))
def test_96_full_verify(self):
- "test full_verify flag"
+ "test verify(full=True) flag"
def vfull(s, h):
- return self.handler.verify(s, h, full_verify=True)
+ return self.handler.verify(s, h, full=True)
# reference
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
@@ -1780,7 +1785,7 @@ class scram_test(HandlerCase):
# catch truncated digests.
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
- 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY' # -1 char
+ 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhV,' # -1 char
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ')
self.assertRaises(ValueError, vfull, 'pencil', h)
@@ -1788,7 +1793,7 @@ class scram_test(HandlerCase):
# catch padded digests.
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
- 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,a' # +1 char
+ 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVYa,' # +1 char
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ')
self.assertRaises(ValueError, vfull, 'pencil', h)
@@ -1802,29 +1807,6 @@ class scram_test(HandlerCase):
self.assertRaises(ValueError, vfull, 'pencil', h)
self.assertRaises(ValueError, vfull, 'tape', h)
- ndn_values = [
- # normalized name, unnormalized names
-
- # IANA assigned names
- ("md5", "MD-5"),
- ("sha-1", "SHA1"),
- ("sha-256", "SHA_256", "sha2-256", "sha-2-256"),
-
- # heuristic for unassigned names
- ("abc6", "aBc-6"),
- ("abc6-256", "aBc-6-256"),
- ("ripemd", "RIPEMD"),
- ("ripemd-160", "RIPEmd160"),
- ]
-
- def test_97_norm_digest_name(self):
- "test norm_digest_name helper"
- from passlib.handlers.scram import norm_digest_name
- for row in self.ndn_values:
- result = row[0]
- for value in row:
- self.assertEqual(norm_digest_name(value), result)
-
#=========================================================
# (netbsd's) sha1 crypt
#=========================================================
diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py
index 957ceb4..0ce76bf 100644
--- a/passlib/tests/test_utils.py
+++ b/passlib/tests/test_utils.py
@@ -940,6 +940,50 @@ import hmac
from passlib.utils import pbkdf2
#TODO: should we bother testing hmac_sha1() function? it's verified via sha1_crypt testing.
+class CryptoTest(TestCase):
+ "test various crypto functions"
+
+ ndn_formats = ["hashlib", "iana"]
+ ndn_values = [
+ # (iana name, hashlib name, ... other unnormalized names)
+ ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"),
+ ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"),
+ ("sha256", "sha-256", "SHA_256", "sha2-256"),
+ ("ripemd", "ripemd", "SCRAM-RIPEMD", "RIPEMD"),
+ ("ripemd160", "ripemd-160",
+ "SCRAM-RIPEMD-160", "RIPEmd160"),
+ ("test128", "test-128", "TEST128"),
+ ("test2", "test2", "TEST-2"),
+ ("test3128", "test3-128", "TEST-3-128"),
+ ]
+
+ def test_norm_hash_name(self):
+ "test norm_hash_name()"
+ from itertools import chain
+ from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names
+
+ # test formats
+ for format in self.ndn_formats:
+ norm_hash_name("md4", format)
+ self.assertRaises(ValueError, norm_hash_name, "md4", None)
+ self.assertRaises(ValueError, norm_hash_name, "md4", "fake")
+
+ # test types
+ self.assertEqual(norm_hash_name(u("MD4")), "md4")
+ self.assertEqual(norm_hash_name(b("MD4")), "md4")
+ self.assertRaises(TypeError, norm_hash_name, None)
+
+ # test selected results
+ with catch_warnings():
+ warnings.filterwarnings("ignore", 'encountered unknown hash')
+ for row in chain(_nhn_hash_names, self.ndn_values):
+ for idx, format in enumerate(self.ndn_formats):
+ correct = row[idx]
+ for value in row:
+ result = norm_hash_name(value, format)
+ self.assertEqual(result, correct,
+ "name=%r, format=%r:" % (value,
+ format))
class KdfTest(TestCase):
"test kdf helpers"
diff --git a/passlib/utils/pbkdf2.py b/passlib/utils/pbkdf2.py
index 9592a81..6388388 100644
--- a/passlib/utils/pbkdf2.py
+++ b/passlib/utils/pbkdf2.py
@@ -20,7 +20,8 @@ try:
except ImportError:
_EVP = None
#pkg
-from passlib.utils import to_bytes, xor_bytes
+from passlib.exc import PasslibRuntimeWarning
+from passlib.utils import to_bytes, xor_bytes, to_native_str
from passlib.utils.compat import b, bytes, BytesIO, irange, callable, int_types
#local
__all__ = [
@@ -30,6 +31,108 @@ __all__ = [
"pbkdf2",
]
+#=============================================================================
+# hash helpers
+#=============================================================================
+
+# known hash names
+_nhn_formats = dict(hashlib=0, iana=1)
+_nhn_hash_names = [
+ # (hashlib/ssl name, iana name or standin, ... other known aliases)
+
+ # hashes with official IANA-assigned names
+ # (as of 2012-03 - http://www.iana.org/assignments/hash-function-text-names)
+ ("md2", "md2"),
+ ("md5", "md5"),
+ ("sha1", "sha-1"),
+ ("sha224", "sha-224", "sha2-224"),
+ ("sha256", "sha-256", "sha2-256"),
+ ("sha384", "sha-384", "sha2-384"),
+ ("sha512", "sha-512", "sha2-512"),
+
+ # hashlib/ssl-supported hashes without official IANA names,
+ # hopefully compatible stand-ins have been chosen.
+ ("md4", "md4"),
+ ("sha", "sha-0", "sha0"),
+ ("ripemd", "ripemd"),
+ ("ripemd160", "ripemd-160"),
+]
+
+# cache for norm_hash_name()
+_nhn_cache = {}
+
+def norm_hash_name(name, format="hashlib"):
+ """normalize hash function name
+
+ :arg name:
+ un-normalized hash function name.
+
+ this name can be a Python :mod:`~hashlib` digest name,
+ a SCRAM mechanism name, IANA assigned hash name, etc;
+ case is ignored, underscores converted to hyphens.
+
+ :param format:
+ naming convention to normalize hash names to.
+ possible values are:
+
+ * ``"hashlib"`` (the default) - normalizes name to be compatible
+ with Python's :mod:`!hashlib`.
+
+ * ``"iana"`` - normalizes name to IANA-assigned hash function name.
+ for hashes which IANA hasn't assigned a name for, issues a warning,
+ and then uses a heuristic to give a "best guess".
+
+ :returns:
+ hash name, returned as native string.
+ """
+ # check cache
+ try:
+ idx = _nhn_formats[format]
+ except KeyError:
+ raise ValueError("unknown format: %r" % (format,))
+ try:
+ return _nhn_cache[name][idx]
+ except KeyError:
+ pass
+ orig = name
+
+ # normalize input
+ if not isinstance(name, str):
+ name = to_native_str(name, 'utf-8', 'hash name')
+ name = re.sub("[_ /]", "-", name.strip().lower())
+ if name.startswith("scram-"):
+ name = name[6:]
+ if name.endswith("-plus"):
+ name = name[:-5]
+
+ # look through standard names and known aliases
+ def check_table(name):
+ for row in _nhn_hash_names:
+ if name in row:
+ _nhn_cache[orig] = row
+ return row[idx]
+ result = check_table(name)
+ if result:
+ return result
+
+ # try to clean name up, and recheck table
+ m = re.match("^(?P<name>[a-z]+)-?(?P<rev>\d)?-?(?P<size>\d{3,4})?$", name)
+ if m:
+ name, rev, size = m.group("name", "rev", "size")
+ if rev:
+ name += rev
+ if size:
+ name += "-" + size
+ result = check_table(name)
+ if result:
+ return result
+
+ # else we've done what we can
+ warn("norm_hash_name(): unknown hash: %r" % (orig,), PasslibRuntimeWarning)
+ name2 = name.replace("-", "")
+ row = _nhn_cache[orig] = (name2, name)
+ return row[idx]
+
#=================================================================================
#quick hmac_sha1 implementation used various places
#=================================================================================