summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-03-10 12:18:00 -0500
committerEli Collins <elic@assurancetechnologies.com>2012-03-10 12:18:00 -0500
commit557d17ba4e0123bce7e1659002270aa8dedb2f24 (patch)
tree3289f0a408220aec701d33102294d03fa75cc084 /passlib
parentb9de1a4221ef709b7ad39aba49b1ee43c318bebd (diff)
downloadpasslib-557d17ba4e0123bce7e1659002270aa8dedb2f24.tar.gz
added mssql 2000/2005 hashes; enhanced HandlerCase's password case sensitive test
Diffstat (limited to 'passlib')
-rw-r--r--passlib/handlers/mssql.py226
-rw-r--r--passlib/registry.py2
-rw-r--r--passlib/tests/test_handlers.py180
-rw-r--r--passlib/tests/utils.py20
4 files changed, 425 insertions, 3 deletions
diff --git a/passlib/handlers/mssql.py b/passlib/handlers/mssql.py
new file mode 100644
index 0000000..36bff13
--- /dev/null
+++ b/passlib/handlers/mssql.py
@@ -0,0 +1,226 @@
+"""passlib.handlers.mssql - MS-SQL Password Hash
+
+Notes
+=====
+MS-SQL has used a number of hash algs over the years,
+most of which were exposed through the undocumented
+'pwdencrypt' and 'pwdcompare' sql functions.
+
+Known formats
+-------------
+6.5
+ snefru hash, ascii encoded password
+ no examples found
+
+7.0
+ snefru hash, unicode (what encoding?)
+ saw ref that these blobs were 16 bytes in size
+ no examples found
+
+2000
+ byte string using displayed as 0x hex, using 0x0100 prefix.
+ contains hashes of password and upper-case password.
+
+2007
+ same as 2000, but without the upper-case hash.
+
+refs
+----------
+https://blogs.msdn.com/b/lcris/archive/2007/04/30/sql-server-2005-about-login-password-hashes.aspx?Redirected=true
+http://us.generation-nt.com/securing-passwords-hash-help-35429432.html
+http://forum.md5decrypter.co.uk/topic230-mysql-and-mssql-get-password-hashes.aspx
+http://www.theregister.co.uk/2002/07/08/cracking_ms_sql_server_passwords/
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from binascii import hexlify, unhexlify
+from hashlib import sha1
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+#pkg
+from passlib.utils import to_unicode, consteq
+from passlib.utils.compat import b, bytes, bascii_to_str, unicode, u
+import passlib.utils.handlers as uh
+#local
+__all__ = [
+ "mssql2000",
+ "mssql2005",
+]
+
+#=========================================================
+# mssql 2000
+#=========================================================
+def _raw_mssql(secret, salt):
+ assert isinstance(secret, unicode)
+ assert isinstance(salt, bytes)
+ return sha1(secret.encode("utf-16-le") + salt).digest()
+
+BIDENT = b("0x0100")
+##BIDENT2 = b("\x01\x00")
+UIDENT = u("0x0100")
+
+def _ident_mssql(hash, csize, bsize):
+ "common identify for mssql 2000/2005"
+ if not hash:
+ return False
+ if isinstance(hash, unicode):
+ if len(hash) == csize and hash.startswith(UIDENT):
+ return True
+ else:
+ assert isinstance(hash, bytes)
+ if len(hash) == csize and hash.startswith(BIDENT):
+ return True
+ ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
+ ## return True
+ return False
+
+def _parse_mssql(hash, csize, bsize, name):
+ "common parser for mssql 2000/2005; returns 4 byte salt + checksum"
+ if not hash:
+ raise ValueError("no hash specified")
+ if isinstance(hash, unicode):
+ if len(hash) == csize and hash.startswith(UIDENT):
+ try:
+ return unhexlify(hash[6:].encode("utf-8"))
+ except TypeError: # throw when bad char found
+ pass
+ else:
+ # assumes ascii-compat encoding
+ assert isinstance(hash, bytes)
+ if len(hash) == csize and hash.startswith(BIDENT):
+ try:
+ return unhexlify(hash[6:])
+ except TypeError: # throw when bad char found
+ pass
+ ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
+ ## return hash[2:]
+ raise ValueError("invalid %s hash" % name)
+
+class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
+ """This class implements the password hash used by MS-SQL 2000, and follows the :ref:`password-hash-api`.
+
+ It supports a fixed-length salt.
+
+ The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords:
+
+ :param salt:
+ Optional salt string.
+ If not specified, one will be autogenerated (this is recommended).
+ If specified, it must be 4 bytes in length.
+ """
+ #=========================================================
+ #algorithm information
+ #=========================================================
+ name = "mssql2000"
+ setting_kwds = ("salt",)
+ checksum_size = 40
+ min_salt_size = max_salt_size = 4
+ _stub_checksum = b("\x00") * 40
+
+ #=========================================================
+ #formatting
+ #=========================================================
+
+ # 0100 - 2 byte identifier
+ # 4 byte salt
+ # 20 byte checksum
+ # 20 byte checksum
+ # = 46 bytes
+ # encoded '0x' + 92 chars = 94
+
+ @classmethod
+ def identify(cls, hash):
+ return _ident_mssql(hash, 94, 46)
+
+ @classmethod
+ def from_string(cls, hash):
+ data = _parse_mssql(hash, 94, 46, cls.name)
+ return cls(salt=data[:4], checksum=data[4:])
+
+ def to_string(self):
+ raw = self.salt + (self.checksum or self._stub_checksum)
+ # raw bytes format - BIDENT2 + raw
+ return "0x0100" + bascii_to_str(hexlify(raw).upper())
+
+ def _calc_checksum(self, secret):
+ secret = to_unicode(secret, 'utf-8', errname='secret')
+ salt = self.salt
+ return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt)
+
+ @classmethod
+ def verify(cls, secret, hash):
+ # NOTE: we only compare against the upper-case hash
+ # XXX: add 'full' just to verify both checksums?
+ self = cls.from_string(hash)
+ chk = self.checksum
+ if chk is None:
+ raise uh.MissingDigestError(cls)
+ secret = to_unicode(secret, 'utf-8', errname='secret')
+ result = _raw_mssql(secret.upper(), self.salt)
+ return consteq(result, chk[20:])
+
+#=========================================================
+#handler
+#=========================================================
+class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
+ """This class implements the password hash used by MS-SQL 2005, and follows the :ref:`password-hash-api`.
+
+ It supports a fixed-length salt.
+
+ The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords:
+
+ :param salt:
+ Optional salt string.
+ If not specified, one will be autogenerated (this is recommended).
+ If specified, it must be 4 bytes in length.
+ """
+ #=========================================================
+ #algorithm information
+ #=========================================================
+ name = "mssql2005"
+ setting_kwds = ("salt",)
+
+ checksum_size = 20
+ min_salt_size = max_salt_size = 4
+ _stub_checksum = b("\x00") * 20
+
+ #=========================================================
+ #formatting
+ #=========================================================
+
+ # 0x0100 - 2 byte identifier
+ # 4 byte salt
+ # 20 byte checksum
+ # = 26 bytes
+ # encoded '0x' + 52 chars = 54
+
+ @classmethod
+ def identify(cls, hash):
+ return _ident_mssql(hash, 54, 26)
+
+ @classmethod
+ def from_string(cls, hash):
+ data = _parse_mssql(hash, 54, 26, cls.name)
+ return cls(salt=data[:4], checksum=data[4:])
+
+ def to_string(self):
+ raw = self.salt + (self.checksum or self._stub_checksum)
+ # raw bytes format - BIDENT2 + raw
+ return "0x0100" + bascii_to_str(hexlify(raw)).upper()
+
+ def _calc_checksum(self, secret):
+ secret = to_unicode(secret, 'utf-8', errname='secret')
+ return _raw_mssql(secret, self.salt)
+
+ #=========================================================
+ #eoc
+ #=========================================================
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/registry.py b/passlib/registry.py
index a73cebb..9db9415 100644
--- a/passlib/registry.py
+++ b/passlib/registry.py
@@ -122,6 +122,8 @@ _handler_locations = {
"ldap_pbkdf2_sha512":
("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha512"),
"md5_crypt": ("passlib.handlers.md5_crypt", "md5_crypt"),
+ "mssql2000": ("passlib.handlers.mssql", "mssql2000"),
+ "mssql2005": ("passlib.handlers.mssql", "mssql2005"),
"mysql323": ("passlib.handlers.mysql", "mysql323"),
"mysql41": ("passlib.handlers.mysql", "mysql41"),
"nthash": ("passlib.handlers.nthash", "nthash"),
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index 72e6486..6172039 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -805,6 +805,186 @@ os_crypt_md5_crypt_test = create_backend_case(_md5_crypt_test, "os_crypt")
builtin_md5_crypt_test = create_backend_case(_md5_crypt_test, "builtin")
#=========================================================
+# mssql 2000 & 2005
+#=========================================================
+class mssql2000_test(HandlerCase):
+ handler = hash.mssql2000
+ secret_case_insensitive = "verify-only"
+
+ known_correct_hashes = [
+ #
+ # http://hkashfi.blogspot.com/2007/08/breaking-sql-server-2005-hashes.html
+ #
+ ('Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED2503412FD54D6119FFF04129A1D72E7C3194F7284A7F3A'),
+ ('TEST', '0x010034767D5C2FD54D6119FFF04129A1D72E7C3194F7284A7F3A2FD54D6119FFF04129A1D72E7C3194F7284A7F3A'),
+
+ #
+ # http://www.sqlmag.com/forums/aft/68438
+ #
+ ('x', '0x010086489146C46DD7318D2514D1AC706457CBF6CD3DF8407F071DB4BBC213939D484BF7A766E974F03C96524794'),
+
+ #
+ # http://stackoverflow.com/questions/173329/how-to-decrypt-a-password-from-sql-server
+ #
+ ('AAAA', '0x0100CF465B7B12625EF019E157120D58DD46569AC7BF4118455D12625EF019E157120D58DD46569AC7BF4118455D'),
+
+ #
+ # http://msmvps.com/blogs/gladchenko/archive/2005/04/06/41083.aspx
+ #
+ ('123', '0x01002D60BA07FE612C8DE537DF3BFCFA49CD9968324481C1A8A8FE612C8DE537DF3BFCFA49CD9968324481C1A8A8'),
+
+ #
+ # http://www.simple-talk.com/sql/t-sql-programming/temporarily-changing-an-unknown-password-of-the-sa-account-/
+ #
+ ('12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'),
+
+ #
+ # XXX: sample is incomplete, password unknown
+ # https://anthonystechblog.wordpress.com/2011/04/20/password-encryption-in-sql-server-how-to-tell-if-a-user-is-using-a-weak-password/
+ # (????, '0x0100813F782D66EF15E40B1A3FDF7AB88B322F51401A87D8D3E3A8483C4351A3D96FC38499E6CDD2B6F?????????'),
+ #
+
+ #
+ # from JTR 1.7.9
+ #
+ ('foo', '0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8B6D6261460D3F53B279CC6913CE747006A2E3254'),
+ ('bar', '0x01000508513EADDF6DB7DDD270CCA288BF097F2FF69CC2DB74FBB9644D6901764F999BAB9ECB80DE578D92E3F80D'),
+ ('canard', '0x01008408C523CF06DCB237835D701C165E68F9460580132E28ED8BC558D22CEDF8801F4503468A80F9C52A12C0A3'),
+ ('lapin', '0x0100BF088517935FC9183FE39FDEC77539FD5CB52BA5F5761881E5B9638641A79DBF0F1501647EC941F3355440A2'),
+
+ #
+ # custom
+ #
+
+ # ensures utf-8 used for unicode
+ (UPASS_USD, '0x0100624C0961B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5'),
+ (UPASS_TABLE, '0x010083104228FAD559BE52477F2131E538BE9734E5C4B0ADEFD7F6D784B03C98585DC634FE2B8CA3A6DFFEC729B4'),
+
+ ]
+
+ known_correct_configs = [
+ ('0x010034767D5C00000000000000000000000000000000000000000000000000000000000000000000000000000000',
+ 'Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED2503412FD54D6119FFF04129A1D72E7C3194F7284A7F3A'),
+ ]
+
+ known_alternate_hashes = [
+ # lower case hex
+ ('0x01005b20054332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b3',
+ '12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'),
+ ]
+
+ known_unidentified_hashes = [
+ # malformed start
+ '0X01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3',
+
+ # wrong magic value
+ '0x02005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3',
+
+ # wrong size
+ '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3',
+ '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3AF',
+
+ # mssql2005
+ '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3',
+ ]
+
+ known_malformed_hashes = [
+ # non-hex char ---\/
+ '0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3',
+ ]
+
+class mssql2005_test(HandlerCase):
+ handler = hash.mssql2005
+
+ known_correct_hashes = [
+ #
+ # http://hkashfi.blogspot.com/2007/08/breaking-sql-server-2005-hashes.html
+ #
+ ('TEST', '0x010034767D5C2FD54D6119FFF04129A1D72E7C3194F7284A7F3A'),
+
+ #
+ # http://www.openwall.com/lists/john-users/2009/07/14/2
+ #
+ ('toto', '0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908'),
+
+ #
+ # http://msmvps.com/blogs/gladchenko/archive/2005/04/06/41083.aspx
+ #
+ ('123', '0x01004A335DCEDB366D99F564D460B1965B146D6184E4E1025195'),
+ ('123', '0x0100E11D573F359629B344990DCD3D53DE82CF8AD6BBA7B638B6'),
+
+ #
+ # XXX: password unknown
+ # http://www.simple-talk.com/sql/t-sql-programming/temporarily-changing-an-unknown-password-of-the-sa-account-/
+ # (???, '0x01004086CEB6301EEC0A994E49E30DA235880057410264030797'),
+ #
+
+ #
+ # http://therelentlessfrontend.com/2010/03/26/encrypting-and-decrypting-passwords-in-sql-server/
+ #
+ ('AAAA', '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30'),
+
+ #
+ # from JTR 1.7.9
+ #
+ ("toto", "0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908"),
+ ("titi", "0x01004086CEB60ED526885801C23B366965586A43D3DEAC6DD3FD"),
+ ("foo", "0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8"),
+ ("bar", "0x01000508513EADDF6DB7DDD270CCA288BF097F2FF69CC2DB74FB"),
+ ("canard", "0x01008408C523CF06DCB237835D701C165E68F9460580132E28ED"),
+ ("lapin", "0x0100BF088517935FC9183FE39FDEC77539FD5CB52BA5F5761881"),
+
+ #
+ # adapted from mssql2000.known_correct_hashes (above)
+ #
+ ('Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED250341'),
+ ('Test', '0x0100993BF2315F36CC441485B35C4D84687DC02C78B0E680411F'),
+ ('x', '0x010086489146C46DD7318D2514D1AC706457CBF6CD3DF8407F07'),
+ ('AAAA', '0x0100CF465B7B12625EF019E157120D58DD46569AC7BF4118455D'),
+ ('123', '0x01002D60BA07FE612C8DE537DF3BFCFA49CD9968324481C1A8A8'),
+ ('12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'),
+
+ #
+ # custom
+ #
+
+ # ensures utf-8 used for unicode
+ (UPASS_USD, '0x0100624C0961B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5'),
+ (UPASS_TABLE, '0x010083104228FAD559BE52477F2131E538BE9734E5C4B0ADEFD7'),
+ ]
+
+ known_correct_configs = [
+ ('0x010034767D5C0000000000000000000000000000000000000000',
+ 'Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED250341'),
+ ]
+
+ known_alternate_hashes = [
+ # lower case hex
+ ('0x01005b20054332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b3',
+ '12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'),
+ ]
+
+ known_unidentified_hashes = [
+ # malformed start
+ '0X010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30',
+
+ # wrong magic value
+ '0x020036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30',
+
+ # wrong size
+ '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F',
+ '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F3012',
+
+ # mssql2000
+ '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3',
+ ]
+
+ known_malformed_hashes = [
+ # non-hex char --\/
+ '0x010036D726AE86G34E97F20B198ACD219D60B446AC5E48C54F30',
+ ]
+
+#=========================================================
# mysql 323 & 41
#=========================================================
class mysql323_test(HandlerCase):
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 8c10918..4b9ca99 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -517,6 +517,8 @@ class HandlerCase(TestCase):
secret_size = None
# whether hash is case insensitive
+ # True, False, or special value "verify-only" (which indicates
+ # hash contains case-sensitive portion, but verifies is case-insensitive)
secret_case_insensitive = False
# flag if scheme accepts ALL hash strings (e.g. plaintext)
@@ -1132,15 +1134,27 @@ class HandlerCase(TestCase):
def test_61_case_sensitive(self):
"test password case sensitivity"
+ hash_insensitive = self.secret_case_insensitive is True
+ verify_insensitive = self.secret_case_insensitive in [True,
+ "verify-only"]
+
lower = 'test'
upper = 'TEST'
h1 = self.do_encrypt(lower)
- if self.secret_case_insensitive:
+ if verify_insensitive:
self.assertTrue(self.do_verify(upper, h1),
- "hash should not be case sensitive")
+ "verify() should not be case sensitive")
else:
self.assertFalse(self.do_verify(upper, h1),
- "hash should be case sensitive")
+ "verify() should be case sensitive")
+
+ h2 = self.do_genhash(upper, h1)
+ if hash_insensitive:
+ self.assertEqual(h2, h1,
+ "genhash() should not be case sensitive")
+ else:
+ self.assertNotEqual(h2, h1,
+ "genhash() should be case sensitive")
def test_62_null(self):
"test password=None"