1"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""
2#=============================================================================
3# imports
4#=============================================================================
5# core
6import re
7import logging; log = logging.getLogger(__name__)
8from warnings import warn
9# site
10# pkg
11from passlib.utils import safe_crypt, test_crypt, to_unicode
12from passlib.utils.binary import h64, h64big
13from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause
14from passlib.crypto.des import des_encrypt_int_block
15import passlib.utils.handlers as uh
16# local
17__all__ = [
18    "des_crypt",
19    "bsdi_crypt",
20    "bigcrypt",
21    "crypt16",
22]
23
24#=============================================================================
25# pure-python backend for des_crypt family
26#=============================================================================
27_BNULL = b'\x00'
28
29def _crypt_secret_to_key(secret):
30    """convert secret to 64-bit DES key.
31
32    this only uses the first 8 bytes of the secret,
33    and discards the high 8th bit of each byte at that.
34    a null parity bit is inserted after every 7th bit of the output.
35    """
36    # NOTE: this would set the parity bits correctly,
37    #       but des_encrypt_int_block() would just ignore them...
38    ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8)
39    ##           for i, c in enumerate(secret[:8]))
40    return sum((byte_elem_value(c) & 0x7f) << (57-i*8)
41               for i, c in enumerate(secret[:8]))
42
43def _raw_des_crypt(secret, salt):
44    """pure-python backed for des_crypt"""
45    assert len(salt) == 2
46
47    # NOTE: some OSes will accept non-HASH64 characters in the salt,
48    #       but what value they assign these characters varies wildy,
49    #       so just rejecting them outright.
50    #       the same goes for single-character salts...
51    #       some OSes duplicate the char, some insert a '.' char,
52    #       and openbsd does (something) which creates an invalid hash.
53    salt_value = h64.decode_int12(salt)
54
55    # gotta do something - no official policy since this predates unicode
56    if isinstance(secret, unicode):
57        secret = secret.encode("utf-8")
58    assert isinstance(secret, bytes)
59
60    # forbidding NULL char because underlying crypt() rejects them too.
61    if _BNULL in secret:
62        raise uh.exc.NullPasswordError(des_crypt)
63
64    # convert first 8 bytes of secret string into an integer
65    key_value = _crypt_secret_to_key(secret)
66
67    # run data through des using input of 0
68    result = des_encrypt_int_block(key_value, 0, salt_value, 25)
69
70    # run h64 encode on result
71    return h64big.encode_int64(result)
72
73def _bsdi_secret_to_key(secret):
74    """convert secret to DES key used by bsdi_crypt"""
75    key_value = _crypt_secret_to_key(secret)
76    idx = 8
77    end = len(secret)
78    while idx < end:
79        next = idx + 8
80        tmp_value = _crypt_secret_to_key(secret[idx:next])
81        key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value
82        idx = next
83    return key_value
84
85def _raw_bsdi_crypt(secret, rounds, salt):
86    """pure-python backend for bsdi_crypt"""
87
88    # decode salt
89    salt_value = h64.decode_int24(salt)
90
91    # gotta do something - no official policy since this predates unicode
92    if isinstance(secret, unicode):
93        secret = secret.encode("utf-8")
94    assert isinstance(secret, bytes)
95
96    # forbidding NULL char because underlying crypt() rejects them too.
97    if _BNULL in secret:
98        raise uh.exc.NullPasswordError(bsdi_crypt)
99
100    # convert secret string into an integer
101    key_value = _bsdi_secret_to_key(secret)
102
103    # run data through des using input of 0
104    result = des_encrypt_int_block(key_value, 0, salt_value, rounds)
105
106    # run h64 encode on result
107    return h64big.encode_int64(result)
108
109#=============================================================================
110# handlers
111#=============================================================================
112class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
113    """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`.
114
115    It supports a fixed-length salt.
116
117    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
118
119    :type salt: str
120    :param salt:
121        Optional salt string.
122        If not specified, one will be autogenerated (this is recommended).
123        If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
124
125    :param bool truncate_error:
126        By default, des_crypt will silently truncate passwords larger than 8 bytes.
127        Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
128        to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
129
130        .. versionadded:: 1.7
131
132    :type relaxed: bool
133    :param relaxed:
134        By default, providing an invalid value for one of the other
135        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
136        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
137        will be issued instead. Correctable errors include
138        ``salt`` strings that are too long.
139
140        .. versionadded:: 1.6
141    """
142    #===================================================================
143    # class attrs
144    #===================================================================
145
146    #--------------------
147    # PasswordHash
148    #--------------------
149    name = "des_crypt"
150    setting_kwds = ("salt", "truncate_error")
151
152    #--------------------
153    # GenericHandler
154    #--------------------
155    checksum_chars = uh.HASH64_CHARS
156    checksum_size = 11
157
158    #--------------------
159    # HasSalt
160    #--------------------
161    min_salt_size = max_salt_size = 2
162    salt_chars = uh.HASH64_CHARS
163
164    #--------------------
165    # TruncateMixin
166    #--------------------
167    truncate_size = 8
168
169    #===================================================================
170    # formatting
171    #===================================================================
172    # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
173
174    _hash_regex = re.compile(u(r"""
175        ^
176        (?P<salt>[./a-z0-9]{2})
177        (?P<chk>[./a-z0-9]{11})?
178        $"""), re.X|re.I)
179
180    @classmethod
181    def from_string(cls, hash):
182        hash = to_unicode(hash, "ascii", "hash")
183        salt, chk = hash[:2], hash[2:]
184        return cls(salt=salt, checksum=chk or None)
185
186    def to_string(self):
187        hash = u("%s%s") % (self.salt, self.checksum)
188        return uascii_to_str(hash)
189
190    #===================================================================
191    # digest calculation
192    #===================================================================
193    def _calc_checksum(self, secret):
194        # check for truncation (during .hash() calls only)
195        if self.use_defaults:
196            self._check_truncate_policy(secret)
197
198        return self._calc_checksum_backend(secret)
199
200    #===================================================================
201    # backend
202    #===================================================================
203    backends = ("os_crypt", "builtin")
204
205    #---------------------------------------------------------------
206    # os_crypt backend
207    #---------------------------------------------------------------
208    @classmethod
209    def _load_backend_os_crypt(cls):
210        if test_crypt("test", 'abgOeLfPimXQo'):
211            cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
212            return True
213        else:
214            return False
215
216    def _calc_checksum_os_crypt(self, secret):
217        # NOTE: we let safe_crypt() encode unicode secret -> utf8;
218        #       no official policy since des-crypt predates unicode
219        hash = safe_crypt(secret, self.salt)
220        if hash:
221            assert hash.startswith(self.salt) and len(hash) == 13
222            return hash[2:]
223        else:
224            # py3's crypt.crypt() can't handle non-utf8 bytes.
225            # fallback to builtin alg, which is always available.
226            return self._calc_checksum_builtin(secret)
227
228    #---------------------------------------------------------------
229    # builtin backend
230    #---------------------------------------------------------------
231    @classmethod
232    def _load_backend_builtin(cls):
233        cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
234        return True
235
236    def _calc_checksum_builtin(self, secret):
237        return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")
238
239    #===================================================================
240    # eoc
241    #===================================================================
242
243class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
244    """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
245
246    It supports a fixed-length salt, and a variable number of rounds.
247
248    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
249
250    :type salt: str
251    :param salt:
252        Optional salt string.
253        If not specified, one will be autogenerated (this is recommended).
254        If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
255
256    :type rounds: int
257    :param rounds:
258        Optional number of rounds to use.
259        Defaults to 5001, must be between 1 and 16777215, inclusive.
260
261    :type relaxed: bool
262    :param relaxed:
263        By default, providing an invalid value for one of the other
264        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
265        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
266        will be issued instead. Correctable errors include ``rounds``
267        that are too small or too large, and ``salt`` strings that are too long.
268
269        .. versionadded:: 1.6
270
271    .. versionchanged:: 1.6
272        :meth:`hash` will now issue a warning if an even number of rounds is used
273        (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).
274    """
275    #===================================================================
276    # class attrs
277    #===================================================================
278    #--GenericHandler--
279    name = "bsdi_crypt"
280    setting_kwds = ("salt", "rounds")
281    checksum_size = 11
282    checksum_chars = uh.HASH64_CHARS
283
284    #--HasSalt--
285    min_salt_size = max_salt_size = 4
286    salt_chars = uh.HASH64_CHARS
287
288    #--HasRounds--
289    default_rounds = 5001
290    min_rounds = 1
291    max_rounds = 16777215 # (1<<24)-1
292    rounds_cost = "linear"
293
294    # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
295    # but that seems to be an OS policy, not a algorithm limitation.
296
297    #===================================================================
298    # parsing
299    #===================================================================
300    _hash_regex = re.compile(u(r"""
301        ^
302        _
303        (?P<rounds>[./a-z0-9]{4})
304        (?P<salt>[./a-z0-9]{4})
305        (?P<chk>[./a-z0-9]{11})?
306        $"""), re.X|re.I)
307
308    @classmethod
309    def from_string(cls, hash):
310        hash = to_unicode(hash, "ascii", "hash")
311        m = cls._hash_regex.match(hash)
312        if not m:
313            raise uh.exc.InvalidHashError(cls)
314        rounds, salt, chk = m.group("rounds", "salt", "chk")
315        return cls(
316            rounds=h64.decode_int24(rounds.encode("ascii")),
317            salt=salt,
318            checksum=chk,
319        )
320
321    def to_string(self):
322        hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"),
323                               self.salt, self.checksum)
324        return uascii_to_str(hash)
325
326    #===================================================================
327    # validation
328    #===================================================================
329
330    # NOTE: keeping this flag for admin/choose_rounds.py script.
331    #       want to eventually expose rounds logic to that script in better way.
332    _avoid_even_rounds = True
333
334    @classmethod
335    def using(cls, **kwds):
336        subcls = super(bsdi_crypt, cls).using(**kwds)
337        if not subcls.default_rounds & 1:
338            # issue warning if caller set an even 'rounds' value.
339            warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys",
340                 uh.exc.PasslibSecurityWarning)
341        return subcls
342
343    @classmethod
344    def _generate_rounds(cls):
345        rounds = super(bsdi_crypt, cls)._generate_rounds()
346        # ensure autogenerated rounds are always odd
347        # NOTE: doing this even for default_rounds so needs_update() doesn't get
348        #       caught in a loop.
349        # FIXME: this technically might generate a rounds value 1 larger
350        # than the requested upper bound - but better to err on side of safety.
351        return rounds|1
352
353    #===================================================================
354    # migration
355    #===================================================================
356
357    def _calc_needs_update(self, **kwds):
358        # mark bsdi_crypt hashes as deprecated if they have even rounds.
359        if not self.rounds & 1:
360            return True
361        # hand off to base implementation
362        return super(bsdi_crypt, self)._calc_needs_update(**kwds)
363
364    #===================================================================
365    # backends
366    #===================================================================
367    backends = ("os_crypt", "builtin")
368
369    #---------------------------------------------------------------
370    # os_crypt backend
371    #---------------------------------------------------------------
372    @classmethod
373    def _load_backend_os_crypt(cls):
374        if test_crypt("test", '_/...lLDAxARksGCHin.'):
375            cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
376            return True
377        else:
378            return False
379
380    def _calc_checksum_os_crypt(self, secret):
381        config = self.to_string()
382        hash = safe_crypt(secret, config)
383        if hash:
384            assert hash.startswith(config[:9]) and len(hash) == 20
385            return hash[-11:]
386        else:
387            # py3's crypt.crypt() can't handle non-utf8 bytes.
388            # fallback to builtin alg, which is always available.
389            return self._calc_checksum_builtin(secret)
390
391    #---------------------------------------------------------------
392    # builtin backend
393    #---------------------------------------------------------------
394    @classmethod
395    def _load_backend_builtin(cls):
396        cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
397        return True
398
399    def _calc_checksum_builtin(self, secret):
400        return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
401
402    #===================================================================
403    # eoc
404    #===================================================================
405
406class bigcrypt(uh.HasSalt, uh.GenericHandler):
407    """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`.
408
409    It supports a fixed-length salt.
410
411    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
412
413    :type salt: str
414    :param salt:
415        Optional salt string.
416        If not specified, one will be autogenerated (this is recommended).
417        If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
418
419    :type relaxed: bool
420    :param relaxed:
421        By default, providing an invalid value for one of the other
422        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
423        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
424        will be issued instead. Correctable errors include
425        ``salt`` strings that are too long.
426
427        .. versionadded:: 1.6
428    """
429    #===================================================================
430    # class attrs
431    #===================================================================
432    #--GenericHandler--
433    name = "bigcrypt"
434    setting_kwds = ("salt",)
435    checksum_chars = uh.HASH64_CHARS
436    # NOTE: checksum chars must be multiple of 11
437
438    #--HasSalt--
439    min_salt_size = max_salt_size = 2
440    salt_chars = uh.HASH64_CHARS
441
442    #===================================================================
443    # internal helpers
444    #===================================================================
445    _hash_regex = re.compile(u(r"""
446        ^
447        (?P<salt>[./a-z0-9]{2})
448        (?P<chk>([./a-z0-9]{11})+)?
449        $"""), re.X|re.I)
450
451    @classmethod
452    def from_string(cls, hash):
453        hash = to_unicode(hash, "ascii", "hash")
454        m = cls._hash_regex.match(hash)
455        if not m:
456            raise uh.exc.InvalidHashError(cls)
457        salt, chk = m.group("salt", "chk")
458        return cls(salt=salt, checksum=chk)
459
460    def to_string(self):
461        hash = u("%s%s") % (self.salt, self.checksum)
462        return uascii_to_str(hash)
463
464    def _norm_checksum(self, checksum, relaxed=False):
465        checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed)
466        if len(checksum) % 11:
467            raise uh.exc.InvalidHashError(self)
468        return checksum
469
470    #===================================================================
471    # backend
472    #===================================================================
473    def _calc_checksum(self, secret):
474        if isinstance(secret, unicode):
475            secret = secret.encode("utf-8")
476        chk = _raw_des_crypt(secret, self.salt.encode("ascii"))
477        idx = 8
478        end = len(secret)
479        while idx < end:
480            next = idx + 8
481            chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])
482            idx = next
483        return chk.decode("ascii")
484
485    #===================================================================
486    # eoc
487    #===================================================================
488
489class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler):
490    """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`.
491
492    It supports a fixed-length salt.
493
494    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
495
496    :type salt: str
497    :param salt:
498        Optional salt string.
499        If not specified, one will be autogenerated (this is recommended).
500        If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
501
502    :param bool truncate_error:
503        By default, crypt16 will silently truncate passwords larger than 16 bytes.
504        Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
505        to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
506
507        .. versionadded:: 1.7
508
509    :type relaxed: bool
510    :param relaxed:
511        By default, providing an invalid value for one of the other
512        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
513        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
514        will be issued instead. Correctable errors include
515        ``salt`` strings that are too long.
516
517        .. versionadded:: 1.6
518    """
519    #===================================================================
520    # class attrs
521    #===================================================================
522
523    #--------------------
524    # PasswordHash
525    #--------------------
526    name = "crypt16"
527    setting_kwds = ("salt", "truncate_error")
528
529    #--------------------
530    # GenericHandler
531    #--------------------
532    checksum_size = 22
533    checksum_chars = uh.HASH64_CHARS
534
535    #--------------------
536    # HasSalt
537    #--------------------
538    min_salt_size = max_salt_size = 2
539    salt_chars = uh.HASH64_CHARS
540
541    #--------------------
542    # TruncateMixin
543    #--------------------
544    truncate_size = 16
545
546    #===================================================================
547    # internal helpers
548    #===================================================================
549    _hash_regex = re.compile(u(r"""
550        ^
551        (?P<salt>[./a-z0-9]{2})
552        (?P<chk>[./a-z0-9]{22})?
553        $"""), re.X|re.I)
554
555    @classmethod
556    def from_string(cls, hash):
557        hash = to_unicode(hash, "ascii", "hash")
558        m = cls._hash_regex.match(hash)
559        if not m:
560            raise uh.exc.InvalidHashError(cls)
561        salt, chk = m.group("salt", "chk")
562        return cls(salt=salt, checksum=chk)
563
564    def to_string(self):
565        hash = u("%s%s") % (self.salt, self.checksum)
566        return uascii_to_str(hash)
567
568    #===================================================================
569    # backend
570    #===================================================================
571    def _calc_checksum(self, secret):
572        if isinstance(secret, unicode):
573            secret = secret.encode("utf-8")
574
575        # check for truncation (during .hash() calls only)
576        if self.use_defaults:
577            self._check_truncate_policy(secret)
578
579        # parse salt value
580        try:
581            salt_value = h64.decode_int12(self.salt.encode("ascii"))
582        except ValueError: # pragma: no cover - caught by class
583            raise suppress_cause(ValueError("invalid chars in salt"))
584
585        # convert first 8 byts of secret string into an integer,
586        key1 = _crypt_secret_to_key(secret)
587
588        # run data through des using input of 0
589        result1 = des_encrypt_int_block(key1, 0, salt_value, 20)
590
591        # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars)
592        key2 = _crypt_secret_to_key(secret[8:16])
593
594        # run data through des using input of 0
595        result2 = des_encrypt_int_block(key2, 0, salt_value, 5)
596
597        # done
598        chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)
599        return chk.decode("ascii")
600
601    #===================================================================
602    # eoc
603    #===================================================================
604
605#=============================================================================
606# eof
607#=============================================================================
608