File: legacycrypt.py

package info (click to toggle)
legacycrypt 0.3-3
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 112 kB
  • sloc: python: 224; makefile: 4
file content (259 lines) | stat: -rw-r--r-- 7,714 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
"""Wrapper to the POSIX crypt library call and associated functionality."""

import ctypes as _ctypes
from ctypes.util import find_library as _find_library
import string as _string
from random import SystemRandom as _SystemRandom
from collections import namedtuple as _namedtuple

__version__ = "0.3"
__py_version__ = "3.7.3"


#
# ctypes replacement for _crypt
#

class _crypt_data_libcrypt(_ctypes.Structure):
    """struct crypt_data from glibc's crypt.h

    struct crypt_data
    {
        char keysched[16 * 8];
        char sb0[32768];
        char sb1[32768];
        char sb2[32768];
        char sb3[32768];
        /* end-of-aligment-critical-data */
        char crypt_3_buf[14];
        char current_salt[2];
        long int current_saltbits;
        int  direction, initialized;
    };
    """
    __slots__ = ()
    _fields_ = [
        ("keysched", _ctypes.c_char * 16 * 8),
        ("sb0", _ctypes.c_char * 32768),
        ("sb1", _ctypes.c_char * 32768),
        ("sb2", _ctypes.c_char * 32768),
        ("sb3", _ctypes.c_char * 32768),
        ("crypt_3_buf", _ctypes.c_char * 14),
        ("current_salt", _ctypes.c_char * 2),
        ("current_saltbits", _ctypes.c_int64),
        ("direction", _ctypes.c_int32),
        ("initialized", _ctypes.c_int32),
    ]


class _crypt_data_libxcrypt(_ctypes.Structure):
    """struct crypt_data from libxcrypt's crypt.h

    #define CRYPT_OUTPUT_SIZE 384
    #define CRYPT_MAX_PASSPHRASE_SIZE 512
    #define CRYPT_DATA_RESERVED_SIZE 767
    #define CRYPT_DATA_INTERNAL_SIZE 30720

    struct crypt_data
    {
        char output[CRYPT_OUTPUT_SIZE];
        char setting[CRYPT_OUTPUT_SIZE];
        char input[CRYPT_MAX_PASSPHRASE_SIZE];
        char reserved[CRYPT_DATA_RESERVED_SIZE];
        char initialized;
        char internal[CRYPT_DATA_INTERNAL_SIZE];
    };
    """
    __slots__ = ()
    CRYPT_OUTPUT_SIZE = 384
    CRYPT_MAX_PASSPHRASE_SIZE = 512
    CRYPT_DATA_RESERVED_SIZE = 767
    CRYPT_DATA_INTERNAL_SIZE = 30720
    _fields_ = [
        ("output", _ctypes.c_char * CRYPT_OUTPUT_SIZE),
        ("setting", _ctypes.c_char * CRYPT_OUTPUT_SIZE),
        ("input", _ctypes.c_char * CRYPT_MAX_PASSPHRASE_SIZE),
        ("reserved", _ctypes.c_char * CRYPT_DATA_RESERVED_SIZE),
        ("initialized", _ctypes.c_char),
        ("internal", _ctypes.c_char * CRYPT_DATA_INTERNAL_SIZE),
    ]


try:
    # prefer libxcrypt
    _libname = _find_library('xcrypt')
    if _libname is not None:
        _crypt_data = _crypt_data_libxcrypt
    else:
        # fallback to libcrypt
        _libname = _find_library('crypt')
        if _libname is not None:
            _crypt_data = _crypt_data_libcrypt
    if _libname is not None:
        _libcrypt = _ctypes.CDLL(_libname)
    else:
        raise OSError
except OSError:
    raise ImportError("libcrypt / libxcrypt missing") from None


_crypt_r_func = _crypt_func = None

if hasattr(_libcrypt, "crypt_r"):
    _crypt_r_func = _libcrypt.crypt_r
    _crypt_r_func.argtypes = (
        _ctypes.c_char_p,
        _ctypes.c_char_p,
        _ctypes.POINTER(_crypt_data)
    )
    _crypt_r_func.restype = _ctypes.c_char_p
else:
    _crypt_func = _libcrypt.crypt
    _crypt_func.argtypes = (
        _ctypes.c_char_p,
        _ctypes.c_char_p,
    )
    _crypt_func.restype = _ctypes.c_char_p


def _crypt_crypt(word, salt):
    """Hash a *word* with the given *salt* and return the hashed password.

    [clinic input]
        crypt.crypt

        word: str
        salt: str
        /

    *word* will usually be a user's password.  *salt* (either a random 2 or 16
    character string, possibly prefixed with $digit$ to indicate the method)
    will be used to perturb the encryption algorithm and produce distinct
    results for a given *word*.

    returns Py_BuildValue("s", crypt_result)
    """
    if isinstance(word, str):
        word = word.encode('utf-8')
    else:
        raise TypeError(
            f"crypt() argument 1 must be str, not {word.__class__.__name__}"
        )
    if isinstance(salt, str):
        salt = salt.encode('utf-8')
    else:
        raise TypeError(
            f"crypt() argument 2 must be str, not {salt.__class__.__name__}"
        )

    if _crypt_r_func is not None:
        data = _crypt_data()
        crypt_result = _crypt_r_func(word, salt, data)
        # poor man's memory wiping
        _ctypes.memset(_ctypes.byref(data), 0, _ctypes.sizeof(data))
        del data
    else:
        crypt_result = _crypt_func(word, salt)

    return crypt_result.decode('utf-8') if crypt_result else None


#
# original crypt module
#

_saltchars = _string.ascii_letters + _string.digits + './'
_sr = _SystemRandom()


class _Method(_namedtuple('_Method', 'name ident salt_chars total_size')):

    """Class representing a salt method per the Modular Crypt Format or the
    legacy 2-character crypt method."""

    def __repr__(self):
        return '<crypt.METHOD_{}>'.format(self.name)


def mksalt(method=None, *, rounds=None):
    """Generate a salt for the specified method.

    If not specified, the strongest available method will be used.

    """
    if method is None:
        method = methods[0]
    if rounds is not None and not isinstance(rounds, int):
        raise TypeError(f'{rounds.__class__.__name__} object cannot be '
                        f'interpreted as an integer')
    if not method.ident:  # traditional
        s = ''
    else:  # modular
        s = f'${method.ident}$'

    if method.ident and method.ident[0] == '2':  # Blowfish variants
        if rounds is None:
            log_rounds = 12
        else:
            log_rounds = int.bit_length(rounds-1)
            if rounds != 1 << log_rounds:
                raise ValueError('rounds must be a power of 2')
            if not 4 <= log_rounds <= 31:
                raise ValueError('rounds out of the range 2**4 to 2**31')
        s += f'{log_rounds:02d}$'
    elif method.ident in ('5', '6'):  # SHA-2
        if rounds is not None:
            if not 1000 <= rounds <= 999999999:
                raise ValueError('rounds out of the range 1000 to 999_999_999')
            s += f'rounds={rounds}$'
    elif rounds is not None:
        raise ValueError(f"{method} doesn't support the rounds argument")

    s += ''.join(_sr.choice(_saltchars) for char in range(method.salt_chars))
    return s


def crypt(word, salt=None):
    """Return a string representing the one-way hash of a password, with a salt
    prepended.

    If ``salt`` is not specified or is ``None``, the strongest
    available method will be selected and a salt generated.  Otherwise,
    ``salt`` may be one of the ``crypt.METHOD_*`` values, or a string as
    returned by ``crypt.mksalt()``.

    """
    if salt is None or isinstance(salt, _Method):
        salt = mksalt(salt)
    return _crypt_crypt(word, salt)


#  available salting/crypto methods
methods = []

def _add_method(name, *args, rounds=None):
    method = _Method(name, *args)
    globals()['METHOD_' + name] = method
    salt = mksalt(method, rounds=rounds)
    result = crypt('', salt)
    if result and len(result) == method.total_size:
        methods.append(method)
        return True
    return False

_add_method('SHA512', '6', 16, 106)
_add_method('SHA256', '5', 16, 63)

# Choose the strongest supported version of Blowfish hashing.
# Early versions have flaws.  Version 'a' fixes flaws of
# the initial implementation, 'b' fixes flaws of 'a'.
# 'y' is the same as 'b', for compatibility
# with openwall crypt_blowfish.
for _v in 'b', 'y', 'a', '':
    if _add_method('BLOWFISH', '2' + _v, 22, 59 + len(_v), rounds=1<<4):
        break

_add_method('MD5', '1', 8, 34)
_add_method('CRYPT', None, 2, 13)

del _v, _add_method