comparison roundup/password.py @ 7830:1b326a3d76b4

chore(lint): cleanups from ruff.
author John Rouillard <rouilj@ieee.org>
date Sun, 24 Mar 2024 15:16:02 -0400
parents 897c23876e9f
children 6bd11a73f2ed
comparison
equal deleted inserted replaced
7829:7b33365ccb2a 7830:1b326a3d76b4
22 import os 22 import os
23 import re 23 import re
24 import string 24 import string
25 import sys 25 import sys
26 import warnings 26 import warnings
27 27 from base64 import b64decode, b64encode
28 from base64 import b64encode, b64decode
29 from hashlib import md5, sha1, sha512 28 from hashlib import md5, sha1, sha512
30 29
31 from roundup.anypy import random_ 30 from roundup.anypy import random_
32 31 from roundup.anypy.strings import b2s, s2b, us2s
33 from roundup.anypy.strings import us2s, b2s, s2b
34 from roundup.exceptions import RoundupException 32 from roundup.exceptions import RoundupException
35 33
36 try: 34 try:
37 with warnings.catch_warnings(): 35 with warnings.catch_warnings():
38 warnings.filterwarnings("ignore", category=DeprecationWarning) 36 warnings.filterwarnings("ignore", category=DeprecationWarning)
47 class ConfigNotSet(RoundupException): 45 class ConfigNotSet(RoundupException):
48 pass 46 pass
49 47
50 48
51 def bchr(c): 49 def bchr(c):
52 if bytes == str: 50 if bytes is str:
53 # Python 2. 51 # Python 2.
54 return chr(c) 52 return chr(c)
55 else: 53 else:
56 # Python 3. 54 # Python 3.
57 return bytes((c,)) 55 return bytes((c,))
58 56
59 57
60 def bord(c): 58 def bord(c):
61 if bytes == str: 59 if bytes is str:
62 # Python 2. 60 # Python 2.
63 return ord(c) 61 return ord(c)
64 else: 62 else:
65 # Python 3. Elements of bytes are integers. 63 # Python 3. Elements of bytes are integers.
66 return c 64 return c
95 93
96 def _pbkdf2_sha512(password, salt, rounds, keylen): 94 def _pbkdf2_sha512(password, salt, rounds, keylen):
97 return pbkdf2_hmac('sha512', password, salt, rounds, keylen) 95 return pbkdf2_hmac('sha512', password, salt, rounds, keylen)
98 except ImportError: 96 except ImportError:
99 # no hashlib.pbkdf2_hmac - make our own pbkdf2 function 97 # no hashlib.pbkdf2_hmac - make our own pbkdf2 function
98 from hmac import HMAC
100 from struct import pack 99 from struct import pack
101 from hmac import HMAC
102 100
103 def xor_bytes(left, right): 101 def xor_bytes(left, right):
104 "perform bitwise-xor of two byte-strings" 102 "perform bitwise-xor of two byte-strings"
105 return _bjoin(bchr(bord(l) ^ bord(r)) 103 return _bjoin(bchr(bord(l) ^ bord(r))
106 for l, r in zip(left, right)) # noqa: E741 104 for l, r in zip(left, right)) # noqa: E741
112 if sha == sha512: 110 if sha == sha512:
113 digest_size = 64 # sha512 generates 64-byte blocks. 111 digest_size = 64 # sha512 generates 64-byte blocks.
114 else: 112 else:
115 digest_size = 20 # sha1 generates 20-byte blocks 113 digest_size = 20 # sha1 generates 20-byte blocks
116 114
117 total_blocks = int((keylen+digest_size-1)/digest_size) 115 total_blocks = int((keylen + digest_size - 1) / digest_size)
118 hmac_template = HMAC(password, None, sha) 116 hmac_template = HMAC(password, None, sha)
119 out = _bempty 117 out = _bempty
120 for i in range(1, total_blocks+1): 118 for i in range(1, total_blocks + 1):
121 hmac = hmac_template.copy() 119 hmac = hmac_template.copy()
122 hmac.update(salt + pack(">L", i)) 120 hmac.update(salt + pack(">L", i))
123 block = tmp = hmac.digest() 121 block = tmp = hmac.digest()
124 for _j in range(rounds-1): 122 for _j in range(rounds - 1):
125 hmac = hmac_template.copy() 123 hmac = hmac_template.copy()
126 hmac.update(tmp) 124 hmac.update(tmp)
127 tmp = hmac.digest() 125 tmp = hmac.digest()
128 # TODO: need to speed up this call 126 # TODO: need to speed up this call
129 block = xor_bytes(block, tmp) 127 block = xor_bytes(block, tmp)
137 def ssha(password, salt): 135 def ssha(password, salt):
138 ''' Make ssha digest from password and salt. 136 ''' Make ssha digest from password and salt.
139 Based on code of Roberto Aguilar <roberto@baremetal.io> 137 Based on code of Roberto Aguilar <roberto@baremetal.io>
140 https://gist.github.com/rca/7217540 138 https://gist.github.com/rca/7217540
141 ''' 139 '''
142 shaval = sha1(password) # nosec 140 shaval = sha1(password) # noqa: S324
143 shaval.update(salt) 141 shaval.update(salt)
144 ssha_digest = b2s(b64encode(shaval.digest() + salt).strip()) 142 ssha_digest = b2s(b64encode(shaval.digest() + salt).strip())
145 return ssha_digest 143 return ssha_digest
146 144
147 145
230 """ 228 """
231 if plaintext is None: 229 if plaintext is None:
232 plaintext = "" 230 plaintext = ""
233 if scheme in ["PBKDF2", "PBKDF2S5"]: # all PBKDF schemes 231 if scheme in ["PBKDF2", "PBKDF2S5"]: # all PBKDF schemes
234 if other: 232 if other:
235 rounds, salt, raw_salt, digest = pbkdf2_unpack(other) 233 rounds, salt, raw_salt, _digest = pbkdf2_unpack(other)
236 else: 234 else:
237 raw_salt = random_.token_bytes(20) 235 raw_salt = random_.token_bytes(20)
238 salt = h64encode(raw_salt) 236 salt = h64encode(raw_salt)
239 if config: 237 if config:
240 rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS 238 rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
249 # wants the config number by setting 247 # wants the config number by setting
250 # PYTEST_USE_CONFIG. Using the production 248 # PYTEST_USE_CONFIG. Using the production
251 # rounds value of 2,000,000 (for sha1) makes 249 # rounds value of 2,000,000 (for sha1) makes
252 # testing increase from 12 minutes to 1 hour in CI. 250 # testing increase from 12 minutes to 1 hour in CI.
253 rounds = 1000 251 rounds = 1000
252 elif ("pytest" in sys.modules and
253 "PYTEST_CURRENT_TEST" in os.environ):
254 # Set rounds to 1000 if no config is passed and
255 # we are running within a pytest test.
256 rounds = 1000
254 else: 257 else:
255 if ("pytest" in sys.modules and 258 import logging
256 "PYTEST_CURRENT_TEST" in os.environ): 259 # Log and abort. Initialize rounds and log (which
257 # Set rounds to 1000 if no config is passed and 260 # will probably be ignored) with traceback in case
258 # we are running within a pytest test. 261 # ConfigNotSet exception is removed in the
259 rounds = 1000 262 # future.
263 rounds = 2000000
264 logger = logging.getLogger('roundup')
265 if sys.version_info[0] > 2:
266 logger.critical(
267 "encodePassword called without config.",
268 stack_info=True)
260 else: 269 else:
261 import logging 270 import inspect
262 # Log and abort. Initialize rounds and log (which 271 import traceback
263 # will probably be ignored) with traceback in case 272
264 # ConfigNotSet exception is removed in the 273 where = inspect.currentframe()
265 # future. 274 trace = traceback.format_stack(where)
266 rounds = 2000000 275 logger.critical(
267 logger = logging.getLogger('roundup') 276 "encodePassword called without config. %s",
268 if sys.version_info[0] > 2: 277 trace[:-1]
269 logger.critical( 278 )
270 "encodePassword called without config.", 279 raise ConfigNotSet("encodePassword called without config.")
271 stack_info=True)
272 else:
273 import inspect, traceback # noqa: E401
274 where = inspect.currentframe()
275 trace = traceback.format_stack(where)
276 logger.critical(
277 "encodePassword called without config. %s",
278 trace[:-1]
279 )
280 raise ConfigNotSet("encodePassword called without config.")
281 280
282 if rounds < 1000: 281 if rounds < 1000:
283 raise PasswordValueError("invalid PBKDF2 hash (rounds too low)") 282 raise PasswordValueError("invalid PBKDF2 hash (rounds too low)")
284 if scheme == "PBKDF2S5": 283 if scheme == "PBKDF2S5":
285 raw_digest = pbkdf2_sha512(plaintext, raw_salt, rounds, 64) 284 raw_digest = pbkdf2_sha512(plaintext, raw_salt, rounds, 64)
291 raw_other = b64decode(other) 290 raw_other = b64decode(other)
292 salt = raw_other[20:] 291 salt = raw_other[20:]
293 else: 292 else:
294 # new password 293 # new password
295 # variable salt length 294 # variable salt length
296 salt_len = random_.randbelow(52-36) + 36 295 salt_len = random_.randbelow(52 - 36) + 36
297 salt = random_.token_bytes(salt_len) 296 salt = random_.token_bytes(salt_len)
298 s = ssha(s2b(plaintext), salt) 297 s = ssha(s2b(plaintext), salt)
299 elif scheme == 'SHA': 298 elif scheme == 'SHA':
300 s = sha1(s2b(plaintext)).hexdigest() # nosec 299 s = sha1(s2b(plaintext)).hexdigest() # noqa: S324
301 elif scheme == 'MD5': 300 elif scheme == 'MD5':
302 s = md5(s2b(plaintext)).hexdigest() # nosec 301 s = md5(s2b(plaintext)).hexdigest() # noqa: S324
303 elif scheme == 'crypt': 302 elif scheme == 'crypt':
304 if crypt is None: 303 if crypt is None:
305 raise PasswordValueError( 304 raise PasswordValueError(
306 'Unsupported encryption scheme %r' % scheme) 305 'Unsupported encryption scheme %r' % scheme)
307 if other is not None: 306 if other is not None:
308 salt = other 307 salt = other
309 else: 308 else:
310 saltchars = './0123456789'+string.ascii_letters 309 saltchars = './0123456789' + string.ascii_letters
311 salt = random_.choice(saltchars) + random_.choice(saltchars) 310 salt = random_.choice(saltchars) + random_.choice(saltchars)
312 s = crypt.crypt(plaintext, salt) 311 s = crypt.crypt(plaintext, salt)
313 elif scheme == 'plaintext': 312 elif scheme == 'plaintext':
314 s = plaintext 313 s = plaintext
315 else: 314 else:
316 raise PasswordValueError('Unknown encryption scheme %r' % scheme) 315 raise PasswordValueError('Unknown encryption scheme %r' % scheme)
317 return s 316 return s
318 317
319 318
320 def generatePassword(length=12): 319 def generatePassword(length=12):
321 chars = string.ascii_letters+string.digits 320 chars = string.ascii_letters + string.digits
322 password = [random_.choice(chars) for x in range(length - 1)] 321 password = [random_.choice(chars) for x in range(length - 1)]
323 # make sure there is at least one digit 322 # make sure there is at least one digit
324 digitidx = random_.randbelow(length) 323 digitidx = random_.randbelow(length)
325 password[digitidx:digitidx] = [random_.choice(string.digits)] 324 password[digitidx:digitidx] = [random_.choice(string.digits)]
326 return ''.join(password) 325 return ''.join(password)
424 """ Password has insecure scheme or other insecure parameters 423 """ Password has insecure scheme or other insecure parameters
425 and needs migration to new password scheme 424 and needs migration to new password scheme
426 """ 425 """
427 if self.scheme in self.deprecated_schemes: 426 if self.scheme in self.deprecated_schemes:
428 return True 427 return True
429 rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password) 428
429 rounds, _salt, _raw_salt, _digest = pbkdf2_unpack(self.password)
430
430 if rounds < 1000: 431 if rounds < 1000:
431 return True 432 return True
433
432 if (self.scheme == "PBKDF2"): 434 if (self.scheme == "PBKDF2"):
433 new_rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS 435 new_rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
434 if ("pytest" in sys.modules and 436 if ("pytest" in sys.modules and
435 "PYTEST_CURRENT_TEST" in os.environ): 437 "PYTEST_CURRENT_TEST" in os.environ):
436 if ("PYTEST_USE_CONFIG" in os.environ): 438 if ("PYTEST_USE_CONFIG" in os.environ):
472 raise ValueError('Password not set') 474 raise ValueError('Password not set')
473 return '{%s}%s' % (self.scheme, self.password) 475 return '{%s}%s' % (self.scheme, self.password)
474 476
475 477
476 def test_missing_crypt(config=None): 478 def test_missing_crypt(config=None):
477 p = encodePassword('sekrit', 'crypt') # noqa: F841 - test only 479 _p = encodePassword('sekrit', 'crypt', config=config)
478 480
479 481
480 def test(config=None): 482 def test(config=None):
483 # ruff: noqa: S101 SIM300 - asserts are ok
481 # SHA 484 # SHA
482 p = Password('sekrit', config=config) 485 p = Password('sekrit', config=config)
483 assert Password(encrypted=str(p)) == 'sekrit' 486 assert Password(encrypted=str(p)) == 'sekrit'
484 assert 'sekrit' == Password(encrypted=str(p)) 487 assert 'sekrit' == Password(encrypted=str(p))
485 assert p == 'sekrit' 488 assert p == 'sekrit'
486 assert p != 'not sekrit' 489 assert p != 'not sekrit'
487 assert 'sekrit' == p 490 assert 'sekrit' == p
488 assert 'not sekrit' != p 491 assert 'not sekrit' != p
489 492
490 # MD5 493 # MD5
491 p = Password('sekrit', 'MD5', config=config) 494 p = Password('sekrit', 'MD5', config=config)
492 assert Password(encrypted=str(p)) == 'sekrit' 495 assert Password(encrypted=str(p)) == 'sekrit'
493 assert 'sekrit' == Password(encrypted=str(p)) 496 assert 'sekrit' == Password(encrypted=str(p))
494 assert p == 'sekrit' 497 assert p == 'sekrit'
495 assert p != 'not sekrit' 498 assert p != 'not sekrit'
496 assert 'sekrit' == p 499 assert 'sekrit' == p
497 assert 'not sekrit' != p 500 assert 'not sekrit' != p
498 501
499 # crypt 502 # crypt
500 if crypt: # not available on Windows 503 if crypt: # not available on Windows
501 p = Password('sekrit', 'crypt', config=config) 504 p = Password('sekrit', 'crypt', config=config)
502 assert Password(encrypted=str(p)) == 'sekrit' 505 assert Password(encrypted=str(p)) == 'sekrit'
503 assert 'sekrit' == Password(encrypted=str(p)) 506 assert 'sekrit' == Password(encrypted=str(p))
504 assert p == 'sekrit' 507 assert p == 'sekrit'
505 assert p != 'not sekrit' 508 assert p != 'not sekrit'
506 assert 'sekrit' == p 509 assert 'sekrit' == p
507 assert 'not sekrit' != p 510 assert 'not sekrit' != p
508 511
509 # SSHA 512 # SSHA
510 p = Password('sekrit', 'SSHA', config=config) 513 p = Password('sekrit', 'SSHA', config=config)
511 assert Password(encrypted=str(p)) == 'sekrit' 514 assert Password(encrypted=str(p)) == 'sekrit'
512 assert 'sekrit' == Password(encrypted=str(p)) 515 assert 'sekrit' == Password(encrypted=str(p))
513 assert p == 'sekrit' 516 assert p == 'sekrit'
514 assert p != 'not sekrit' 517 assert p != 'not sekrit'
515 assert 'sekrit' == p 518 assert 'sekrit' == p

Roundup Issue Tracker: http://roundup-tracker.org/