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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
|
"""passlib.handler - code for implementing handlers, and global registry for handlers"""
#=========================================================
#imports
#=========================================================
from __future__ import with_statement
#core
import inspect
import re
import hashlib
import logging; log = logging.getLogger(__name__)
import time
import os
from warnings import warn
#site
#libs
from passlib.utils import classproperty, h64, getrandstr, rng, is_crypt_handler
#pkg
#local
__all__ = [
#framework for implementing handlers
'BaseHash',
'ExtHash',
'StaticHash',
'BackendMixin',
'BackendExtHash',
'BackendStaticHash',
]
#=========================================================
#base handler
#=========================================================
class BaseHash(object):
"""helper for implementing password hash handler with minimal methods
hash implementations should fill out the following:
* all required class attributes: name, setting_kwds
* classmethods genconfig() and genhash()
many implementations will want to override the following:
* classmethod identify() can usually be done more efficiently
most implementations can use defaults for the following:
* encrypt(), verify()
note this class does not support context kwds of any type,
since that is a rare enough requirement inside passlib.
implemented subclasses may call cls.validate_class() to check attribute consistency
(usually only required in unittests, etc)
"""
#=====================================================
#required attributes
#=====================================================
name = None #required by subclass
setting_kwds = None #required by subclass
context_kwds = ()
#=====================================================
#init
#=====================================================
@classmethod
def validate_class(cls):
"helper to ensure class is configured property"
if not cls.name:
raise AssertionError, "class must have .name attribute set"
if cls.setting_kwds is None:
raise AssertionError, "class must have .setting_kwds attribute set"
#=====================================================
#init helpers
#=====================================================
@classproperty
def _has_settings(cls):
"attr for checking if class has ANY settings, memoizes itself on first use"
if cls.name is None:
#otherwise this would optimize itself away prematurely
raise RuntimeError, "_has_settings must be called on subclass only: %r" % (cls,)
value = cls._has_settings = bool(cls.setting_kwds)
return value
#=====================================================
#formatting (usually subclassed)
#=====================================================
@classmethod
def identify(cls, hash):
#NOTE: this relys on genhash throwing error for invalid hashes.
# this approach is bad because genhash may take a long time on valid hashes,
# so subclasses *really* should override this.
try:
cls.genhash('stub', hash)
return True
except ValueError:
return False
#=====================================================
#primary interface (must be subclassed)
#=====================================================
@classmethod
def genconfig(cls, **settings):
if cls._has_settings:
raise NotImplementedError, "%s subclass must implement genconfig()" % (cls,)
else:
if settings:
raise TypeError, "%s genconfig takes no kwds" % (cls.name,)
return None
@classmethod
def genhash(cls, secret, config):
raise NotImplementedError, "%s subclass must implement genhash()" % (cls,)
#=====================================================
#secondary interface (rarely subclassed)
#=====================================================
@classmethod
def encrypt(cls, secret, **settings):
config = cls.genconfig(**settings)
return cls.genhash(secret, config)
@classmethod
def verify(cls, secret, hash):
if not hash:
raise ValueError, "no hash specified"
return hash == cls.genhash(secret, hash)
#=====================================================
#eoc
#=====================================================
#=========================================================
# ExtHash
# rounds+salt+xtra phpass, sha256_crypt, sha512_crypt
# rounds+salt bcrypt, ext_des_crypt, sha1_crypt, sun_md5_crypt
# salt only apr_md5_crypt, des_crypt, md5_crypt
#=========================================================
class ExtHash(BaseHash):
"""helper class for implementing hash schemes
hash implementations should fill out the following:
* all required class attributes:
- name, setting_kwds
- max_salt_chars, min_salt_chars - only if salt is used
- max_rounds, min_rounds, default_rounds - only if rounds are used
* classmethod from_string()
* instancemethod to_string()
* instancemethod calc_checksum()
many implementations will want to override the following:
* classmethod identify() can usually be done more efficiently
* checksum_charset, checksum_chars attributes may prove helpful for validation
most implementations can use defaults for the following:
* genconfig(), genhash(), encrypt(), verify(), etc
* norm_checksum() usually only needs overriding if checksum has multiple encodings
* salt_charset, default_salt_charset, default_salt_chars - if does not match common case
note this class does not support context kwds of any type,
since that is a rare enough requirement inside passlib.
implemented subclasses may call cls.validate_class() to check attribute consistency
(usually only required in unittests, etc)
"""
#=========================================================
#class attributes
#=========================================================
#----------------------------------------------
#password hash api - required attributes
#----------------------------------------------
name = None #required by ExtHash
setting_kwds = None #required by ExtHash
context_kwds = ()
#----------------------------------------------
#checksum information
#----------------------------------------------
checksum_charset = None #if specified, norm_checksum() will validate this
checksum_chars = None #if specified, norm_checksum will require this length
#----------------------------------------------
#salt information
#----------------------------------------------
max_salt_chars = None #required by ExtHash.norm_salt()
@classproperty
def min_salt_chars(cls):
"min salt chars (defaults to max_salt_chars if not specified by subclass)"
return cls.max_salt_chars
@classproperty
def default_salt_chars(cls):
"default salt chars (defaults to max_salt_chars if not specified by subclass)"
return cls.max_salt_chars
salt_charset = h64.CHARS
@classproperty
def default_salt_charset(cls):
return cls.salt_charset
#----------------------------------------------
#rounds information
#----------------------------------------------
min_rounds = 0
max_rounds = None #required by ExtHash.norm_rounds()
default_rounds = None #if not specified, ExtHash.norm_rounds() will require explicit rounds value every time
rounds_cost = "linear" #common case
#----------------------------------------------
#misc ExtHash configuration
#----------------------------------------------
_strict_rounds_bounds = False #if true, always raises error if specified rounds values out of range - required by spec for some hashes
_extra_init_settings = () #settings that ExtHash.__init__ should handle by calling norm_<key>()
#=========================================================
#instance attributes
#=========================================================
checksum = None
salt = None
rounds = None
#=========================================================
#init
#=========================================================
#XXX: rename strict kwd to _strict ?
#XXX: for from_string() purposes, a strict_salt kwd to override strict, might also be useful
def __init__(self, checksum=None, salt=None, rounds=None, strict=False, **kwds):
self.checksum = self.norm_checksum(checksum, strict=strict)
self.salt = self.norm_salt(salt, strict=strict)
self.rounds = self.norm_rounds(rounds, strict=strict)
extra = self._extra_init_settings
if extra:
for key in extra:
value = kwds.pop(key, None)
norm = getattr(self, "norm_" + key)
value = norm(value, strict=strict)
setattr(self, key, value)
super(ExtHash, self).__init__(**kwds)
@classmethod
def validate_class(cls):
"helper to ensure class is configured property"
super(ExtHash, cls).validate_class()
if any(k not in cls.setting_kwds for k in cls._extra_init_settings):
raise AssertionError, "_extra_init_settings must be subset of setting_kwds"
if 'salt' in cls.setting_kwds:
if cls.min_salt_chars > cls.max_salt_chars:
raise AssertionError, "min salt chars too large"
if cls.default_salt_chars < cls.min_salt_chars:
raise AssertionError, "default salt chars too small"
if cls.default_salt_chars > cls.max_salt_chars:
raise AssertionError, "default salt chars too large"
if any(c not in cls.salt_charset for c in cls.default_salt_charset):
raise AssertionError, "default salt charset not subset of salt charset"
if 'rounds' in cls.setting_kwds:
if cls.max_rounds is None:
raise AssertionError, "max rounds not specified"
if cls.min_rounds > cls.max_rounds:
raise AssertionError, "min rounds too large"
if cls.default_rounds is not None:
if cls.default_rounds < cls.min_rounds:
raise AssertionError, "default rounds too small"
if cls.default_rounds > cls.max_rounds:
raise AssertionError, "default rounds too large"
if cls.rounds_cost not in ("linear", "log2"):
raise AssertionError, "unknown rounds cost function"
#=========================================================
#init helpers
#=========================================================
#---------------------------------------------------------
#internal tests for features
#---------------------------------------------------------
@classproperty
def _has_salt(cls):
"attr for checking if salts are supported, memoizes itself on first use"
if cls is ExtHash:
raise RuntimeError, "not allowed for ExtHash directly"
value = cls._has_salt = 'salt' in cls.setting_kwds
return value
@classproperty
def _has_rounds(cls):
"attr for checking if variable are supported, memoizes itself on first use"
if cls is ExtHash:
raise RuntimeError, "not allowed for ExtHash directly"
value = cls._has_rounds = 'rounds' in cls.setting_kwds
return value
#---------------------------------------------------------
#normalization/validation helpers
#---------------------------------------------------------
@classmethod
def norm_checksum(cls, checksum, strict=False):
if checksum is None:
return None
cc = cls.checksum_chars
if cc and len(checksum) != cc:
raise ValueError, "%s checksum must be %d characters" % (cls.name, cc)
cs = cls.checksum_charset
if cs and any(c not in cs for c in checksum):
raise ValueError, "invalid characters in %s checksum" % (cls.name,)
return checksum
@classmethod
def norm_salt(cls, salt, strict=False):
"""helper to normalize & validate user-provided salt string
:arg salt: salt string or ``None``
:param strict: enable strict checking (see below); disabled by default
:raises ValueError:
* if ``strict=True`` and no salt is provided
* if ``strict=True`` and salt contains greater than :attr:`max_salt_chars` characters
* if salt contains chars that aren't in :attr:`salt_charset`.
* if salt contains less than :attr:`min_salt_chars` characters.
if no salt provided and ``strict=False``, a random salt is generated
using :attr:`default_salt_chars` and :attr:`default_salt_charset`.
if the salt is longer than :attr:`max_salt_chars` and ``strict=False``,
the salt string is clipped to :attr:`max_salt_chars`.
:returns:
normalized or generated salt
"""
if not cls._has_salt:
#NOTE: special casing schemes which have no salt...
if salt is not None:
raise ValueError, "%s does not support ``salt`` parameter" % (cls.name,)
return None
if salt is None:
if strict:
raise ValueError, "no salt specified"
return getrandstr(rng, cls.default_salt_charset, cls.default_salt_chars)
#TODO: run salt_charset tests
sc = cls.salt_charset
if sc:
for c in salt:
if c not in sc:
raise ValueError, "invalid character in %s salt: %r" % (cls.name, c)
mn = cls.min_salt_chars
if mn and len(salt) < mn:
raise ValueError, "%s salt string must be at least %d characters" % (cls.name, mn)
mx = cls.max_salt_chars
if len(salt) > mx:
if strict:
raise ValueError, "%s salt string must be at most %d characters" % (cls.name, mx)
salt = salt[:mx]
return salt
@classmethod
def norm_rounds(cls, rounds, strict=False):
"""helper routine for normalizing rounds
:arg rounds: rounds integer or ``None``
:param strict: enable strict checking (see below); disabled by default
:raises ValueError:
* if rounds is ``None`` and ``strict=True``
* if rounds is ``None`` and no :attr:`default_rounds` are specified by class.
* if rounds is outside bounds of :attr:`min_rounds` and :attr:`max_rounds`, and ``strict=True``.
if rounds are not specified and ``strict=False``, uses :attr:`default_rounds`.
if rounds are outside bounds and ``strict=False``, rounds are clipped as appropriate,
but a warning is issued.
:returns:
normalized rounds value
"""
if not cls._has_rounds:
#NOTE: special casing schemes which don't have rounds
if rounds is not None:
raise ValueError, "%s does not support ``rounds``" % (cls.name,)
return None
if rounds is None:
if strict:
raise ValueError, "no rounds specified"
rounds = cls.default_rounds
if rounds is None:
raise ValueError, "%s rounds value must be specified explicitly" % (cls.name,)
return rounds
if cls._strict_rounds_bounds:
strict = True
mn = cls.min_rounds
if rounds < mn:
if strict:
raise ValueError, "%s rounds must be >= %d" % (cls.name, mn)
warn("%s does not allow less than %d rounds: %d" % (cls.name, mn, rounds))
rounds = mn
mx = cls.max_rounds
if rounds > mx:
if strict:
raise ValueError, "%s rounds must be <= %d" % (cls.name, mx)
warn("%s does not allow more than %d rounds: %d" % (cls.name, mx, rounds))
rounds = mx
return rounds
#=========================================================
#password hash api - formatting interface
#=========================================================
@classmethod
def identify(cls, hash):
#NOTE: subclasses may wish to use faster / simpler identify,
# and raise value errors only when an invalid (but identifiable) string is parsed
if not hash:
return False
try:
cls.from_string(hash)
return True
except ValueError:
return False
@classmethod
def from_string(cls, hash):
"return parsed instance from hash/configuration string; raising ValueError on invalid inputs"
raise NotImplementedError, "%s must implement from_string()" % (cls,)
def to_string(self):
"render instance to hash or configuration string (depending on if checksum attr is set)"
raise NotImplementedError, "%s must implement from_string()" % (type(self),)
##def to_config_string(self):
## "helper for generating configuration string (ignoring hash)"
## chk = self.checksum
## if chk:
## try:
## self.checksum = None
## return self.to_string()
## finally:
## self.checksum = chk
## else:
## return self.to_string()
#=========================================================
#password hash api - primary interface (default implementation)
#=========================================================
@classmethod
def genconfig(cls, **settings):
return cls(**settings).to_string()
@classmethod
def genhash(cls, secret, config):
self = cls.from_string(config)
self.checksum = self.calc_checksum(secret)
return self.to_string()
def calc_checksum(self, secret):
"given secret; calcuate and return encoded checksum portion of hash string, taking config from object state"
raise NotImplementedError, "%s must implement calc_checksum()" % (cls,)
#=========================================================
#password hash api - secondary interface (default implementation)
#=========================================================
@classmethod
def encrypt(cls, secret, **settings):
self = cls(**settings)
self.checksum = self.calc_checksum(secret)
return self.to_string()
@classmethod
def verify(cls, secret, hash):
#NOTE: classes with multiple checksum encodings (rare)
# may wish to either override this, or override norm_checksum
# to normalize any checksums provided by from_string()
self = cls.from_string(hash)
return self.checksum == self.calc_checksum(secret)
#=========================================================
#eoc
#=========================================================
#=========================================================
#static - mysql_323, mysql_41, nthash, postgres_md5
#=========================================================
class StaticHash(ExtHash):
"""helper class optimized for implementing hash schemes which have NO settings whatsoever.
the main thing this changes from ExtHash:
* :attr:`setting_kwds` must be an empty tuple (set by class)
* :meth:`genconfig` takes no kwds, and always returns ``None``.
* :meth:`genhash` accepts ``config=None``.
otherwise, this requires the same methods be implemented
as does ExtHash.
"""
#=========================================================
#class attr
#=========================================================
setting_kwds = ()
#=========================================================
#init
#=========================================================
@classmethod
def validate_class(cls):
"helper to validate that class has been configured properly"
if cls.setting_kwds:
raise AssertionError, "StaticHash subclasses must not have any settings, perhaps you want ExtHash?"
super(StaticHash, cls).validate_class()
#=========================================================
#primary interface
#=========================================================
@classmethod
def genconfig(cls):
return None
@classmethod
def genhash(cls, secret, config):
if config is None:
self = cls()
else:
#just to verify input is correctly formatted
self = cls.from_string(config)
self.checksum = self.calc_checksum(secret)
return self.to_string()
#=========================================================
#eoc
#=========================================================
#=========================================================
#helpful mixin which provides lazy-loading of different backends
#to be used for calc_checksum
#=========================================================
class BackendMixin(object):
#NOTE: subclass must provide:
# * attr 'backends' containing list of known backends (top priority backend first)
# * attr '_has_backend_xxx' for each backend 'xxx', indicating if backend is available on system
# * attr '_calc_checksum_xxx' for each backend 'xxx', containing calc_checksum implementation using that backend
_backend = None
@classmethod
def get_backend(cls):
"return name of active backend"
return cls._backend or cls.set_backend()
@classmethod
def has_backend(cls, name):
"check if specified class can be loaded"
return getattr(cls, "_has_backend_" + name)
@classmethod
def set_backend(cls, name=None):
"change class to use specified backend"
if not name or name == "default":
if not name:
name = cls._backend
if name:
return name
for name in cls.backends:
if cls.has_backend(name):
cls.calc_checksum = getattr(cls, "_calc_checksum_" + name)
cls._backend = name
return name
raise EnvironmentError, "no %s backends available" % (cls.name,)
else:
##if name not in cls.backends:
## raise ValueError, "unknown %s backend: %r" % (cls.name, name)
if not cls.has_backend(name):
raise ValueError, "%s backend not available: %r" % (cls.name, name)
cls.calc_checksum = getattr(cls, "_calc_checksum_" + name)
cls._backend = name
return name
def calc_checksum(self, secret):
"stub for calc_checksum(), default backend will be selected first time stub is called"
#backend not loaded - run detection and call replacement
assert not self._backend, "set_backend() failed to replace lazy loader"
self.set_backend()
assert self._backend, "set_backend() failed to load a default backend"
return self.calc_checksum(secret)
class BackendExtHash(BackendMixin, ExtHash):
pass
class BackendStaticHash(BackendMixin, StaticHash):
pass
#=========================================================
# eof
#=========================================================
|