1"""passlib.handlers.mssql - MS-SQL Password Hash
2
3Notes
4=====
5MS-SQL has used a number of hash algs over the years,
6most of which were exposed through the undocumented
7'pwdencrypt' and 'pwdcompare' sql functions.
8
9Known formats
10-------------
116.5
12    snefru hash, ascii encoded password
13    no examples found
14
157.0
16    snefru hash, unicode (what encoding?)
17    saw ref that these blobs were 16 bytes in size
18    no examples found
19
202000
21    byte string using displayed as 0x hex, using 0x0100 prefix.
22    contains hashes of password and upper-case password.
23
242007
25    same as 2000, but without the upper-case hash.
26
27refs
28----------
29https://blogs.msdn.com/b/lcris/archive/2007/04/30/sql-server-2005-about-login-password-hashes.aspx?Redirected=true
30http://us.generation-nt.com/securing-passwords-hash-help-35429432.html
31http://forum.md5decrypter.co.uk/topic230-mysql-and-mssql-get-password-hashes.aspx
32http://www.theregister.co.uk/2002/07/08/cracking_ms_sql_server_passwords/
33"""
34#=============================================================================
35# imports
36#=============================================================================
37# core
38from binascii import hexlify, unhexlify
39from hashlib import sha1
40import re
41import logging; log = logging.getLogger(__name__)
42from warnings import warn
43# site
44# pkg
45from passlib.utils import consteq
46from passlib.utils.compat import bascii_to_str, unicode, u
47import passlib.utils.handlers as uh
48# local
49__all__ = [
50    "mssql2000",
51    "mssql2005",
52]
53
54#=============================================================================
55# mssql 2000
56#=============================================================================
57def _raw_mssql(secret, salt):
58    assert isinstance(secret, unicode)
59    assert isinstance(salt, bytes)
60    return sha1(secret.encode("utf-16-le") + salt).digest()
61
62BIDENT = b"0x0100"
63##BIDENT2 = b("\x01\x00")
64UIDENT = u("0x0100")
65
66def _ident_mssql(hash, csize, bsize):
67    """common identify for mssql 2000/2005"""
68    if isinstance(hash, unicode):
69        if len(hash) == csize and hash.startswith(UIDENT):
70            return True
71    elif isinstance(hash, bytes):
72        if len(hash) == csize and hash.startswith(BIDENT):
73            return True
74        ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
75        ##    return True
76    else:
77        raise uh.exc.ExpectedStringError(hash, "hash")
78    return False
79
80def _parse_mssql(hash, csize, bsize, handler):
81    """common parser for mssql 2000/2005; returns 4 byte salt + checksum"""
82    if isinstance(hash, unicode):
83        if len(hash) == csize and hash.startswith(UIDENT):
84            try:
85                return unhexlify(hash[6:].encode("utf-8"))
86            except TypeError: # throw when bad char found
87                pass
88    elif isinstance(hash, bytes):
89        # assumes ascii-compat encoding
90        assert isinstance(hash, bytes)
91        if len(hash) == csize and hash.startswith(BIDENT):
92            try:
93                return unhexlify(hash[6:])
94            except TypeError: # throw when bad char found
95                pass
96        ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
97        ##    return hash[2:]
98    else:
99        raise uh.exc.ExpectedStringError(hash, "hash")
100    raise uh.exc.InvalidHashError(handler)
101
102class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
103    """This class implements the password hash used by MS-SQL 2000, and follows the :ref:`password-hash-api`.
104
105    It supports a fixed-length salt.
106
107    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
108
109    :type salt: bytes
110    :param salt:
111        Optional salt string.
112        If not specified, one will be autogenerated (this is recommended).
113        If specified, it must be 4 bytes in length.
114
115    :type relaxed: bool
116    :param relaxed:
117        By default, providing an invalid value for one of the other
118        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
119        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
120        will be issued instead. Correctable errors include
121        ``salt`` strings that are too long.
122    """
123    #===================================================================
124    # algorithm information
125    #===================================================================
126    name = "mssql2000"
127    setting_kwds = ("salt",)
128    checksum_size = 40
129    min_salt_size = max_salt_size = 4
130
131    #===================================================================
132    # formatting
133    #===================================================================
134
135    # 0100 - 2 byte identifier
136    # 4 byte salt
137    # 20 byte checksum
138    # 20 byte checksum
139    # = 46 bytes
140    # encoded '0x' + 92 chars = 94
141
142    @classmethod
143    def identify(cls, hash):
144        return _ident_mssql(hash, 94, 46)
145
146    @classmethod
147    def from_string(cls, hash):
148        data = _parse_mssql(hash, 94, 46, cls)
149        return cls(salt=data[:4], checksum=data[4:])
150
151    def to_string(self):
152        raw = self.salt + self.checksum
153        # raw bytes format - BIDENT2 + raw
154        return "0x0100" + bascii_to_str(hexlify(raw).upper())
155
156    def _calc_checksum(self, secret):
157        if isinstance(secret, bytes):
158            secret = secret.decode("utf-8")
159        salt = self.salt
160        return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt)
161
162    @classmethod
163    def verify(cls, secret, hash):
164        # NOTE: we only compare against the upper-case hash
165        # XXX: add 'full' just to verify both checksums?
166        uh.validate_secret(secret)
167        self = cls.from_string(hash)
168        chk = self.checksum
169        if chk is None:
170            raise uh.exc.MissingDigestError(cls)
171        if isinstance(secret, bytes):
172            secret = secret.decode("utf-8")
173        result = _raw_mssql(secret.upper(), self.salt)
174        return consteq(result, chk[20:])
175
176#=============================================================================
177# handler
178#=============================================================================
179class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
180    """This class implements the password hash used by MS-SQL 2005, and follows the :ref:`password-hash-api`.
181
182    It supports a fixed-length salt.
183
184    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
185
186    :type salt: bytes
187    :param salt:
188        Optional salt string.
189        If not specified, one will be autogenerated (this is recommended).
190        If specified, it must be 4 bytes in length.
191
192    :type relaxed: bool
193    :param relaxed:
194        By default, providing an invalid value for one of the other
195        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
196        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
197        will be issued instead. Correctable errors include
198        ``salt`` strings that are too long.
199    """
200    #===================================================================
201    # algorithm information
202    #===================================================================
203    name = "mssql2005"
204    setting_kwds = ("salt",)
205
206    checksum_size = 20
207    min_salt_size = max_salt_size = 4
208
209    #===================================================================
210    # formatting
211    #===================================================================
212
213    # 0x0100 - 2 byte identifier
214    # 4 byte salt
215    # 20 byte checksum
216    # = 26 bytes
217    # encoded '0x' + 52 chars = 54
218
219    @classmethod
220    def identify(cls, hash):
221        return _ident_mssql(hash, 54, 26)
222
223    @classmethod
224    def from_string(cls, hash):
225        data = _parse_mssql(hash, 54, 26, cls)
226        return cls(salt=data[:4], checksum=data[4:])
227
228    def to_string(self):
229        raw = self.salt + self.checksum
230        # raw bytes format - BIDENT2 + raw
231        return "0x0100" + bascii_to_str(hexlify(raw)).upper()
232
233    def _calc_checksum(self, secret):
234        if isinstance(secret, bytes):
235            secret = secret.decode("utf-8")
236        return _raw_mssql(secret, self.salt)
237
238    #===================================================================
239    # eoc
240    #===================================================================
241
242#=============================================================================
243# eof
244#=============================================================================
245