Mercurial > p > roundup > code
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 |
