Mercurial > p > roundup > code
comparison roundup/password.py @ 6007:e27a240430b8
flake8 formatting changes.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sat, 28 Dec 2019 14:44:54 -0500 |
| parents | 6c3826600610 |
| children | 01e9634b81a4 |
comparison
equal
deleted
inserted
replaced
| 6006:cf800f1febe6 | 6007:e27a240430b8 |
|---|---|
| 32 crypt = None | 32 crypt = None |
| 33 | 33 |
| 34 _bempty = b"" | 34 _bempty = b"" |
| 35 _bjoin = _bempty.join | 35 _bjoin = _bempty.join |
| 36 | 36 |
| 37 | |
| 37 def bchr(c): | 38 def bchr(c): |
| 38 if bytes == str: | 39 if bytes == str: |
| 39 # Python 2. | 40 # Python 2. |
| 40 return chr(c) | 41 return chr(c) |
| 41 else: | 42 else: |
| 42 # Python 3. | 43 # Python 3. |
| 43 return bytes((c,)) | 44 return bytes((c,)) |
| 44 | 45 |
| 46 | |
| 45 def bord(c): | 47 def bord(c): |
| 46 if bytes == str: | 48 if bytes == str: |
| 47 # Python 2. | 49 # Python 2. |
| 48 return ord(c) | 50 return ord(c) |
| 49 else: | 51 else: |
| 50 # Python 3. Elements of bytes are integers. | 52 # Python 3. Elements of bytes are integers. |
| 51 return c | 53 return c |
| 52 | 54 |
| 53 #NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size, | 55 |
| 56 # NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size, | |
| 54 # and have charset that's compatible w/ unix crypt variants | 57 # and have charset that's compatible w/ unix crypt variants |
| 55 def h64encode(data): | 58 def h64encode(data): |
| 56 """encode using variant of base64""" | 59 """encode using variant of base64""" |
| 57 return b2s(b64encode(data, b"./").strip(b"=\n")) | 60 return b2s(b64encode(data, b"./").strip(b"=\n")) |
| 61 | |
| 58 | 62 |
| 59 def h64decode(data): | 63 def h64decode(data): |
| 60 """decode using variant of base64""" | 64 """decode using variant of base64""" |
| 61 data = s2b(data) | 65 data = s2b(data) |
| 62 off = len(data) % 4 | 66 off = len(data) % 4 |
| 67 elif off == 2: | 71 elif off == 2: |
| 68 return b64decode(data + b"==", b"./") | 72 return b64decode(data + b"==", b"./") |
| 69 else: | 73 else: |
| 70 return b64decode(data + b"=", b"./") | 74 return b64decode(data + b"=", b"./") |
| 71 | 75 |
| 76 | |
| 72 try: | 77 try: |
| 73 from hashlib import pbkdf2_hmac | 78 from hashlib import pbkdf2_hmac |
| 79 | |
| 74 def _pbkdf2(password, salt, rounds, keylen): | 80 def _pbkdf2(password, salt, rounds, keylen): |
| 75 return pbkdf2_hmac('sha1', password, salt, rounds, keylen) | 81 return pbkdf2_hmac('sha1', password, salt, rounds, keylen) |
| 76 except ImportError: | 82 except ImportError: |
| 77 #no hashlib.pbkdf2_hmac - make our own pbkdf2 function | 83 # no hashlib.pbkdf2_hmac - make our own pbkdf2 function |
| 78 from struct import pack | 84 from struct import pack |
| 79 from hmac import HMAC | 85 from hmac import HMAC |
| 80 | 86 |
| 81 def xor_bytes(left, right): | 87 def xor_bytes(left, right): |
| 82 "perform bitwise-xor of two byte-strings" | 88 "perform bitwise-xor of two byte-strings" |
| 83 return _bjoin(bchr(bord(l) ^ bord(r)) for l, r in zip(left, right)) | 89 return _bjoin(bchr(bord(l) ^ bord(r)) for l, r in zip(left, right)) |
| 84 | 90 |
| 85 def _pbkdf2(password, salt, rounds, keylen): | 91 def _pbkdf2(password, salt, rounds, keylen): |
| 86 digest_size = 20 # sha1 generates 20-byte blocks | 92 digest_size = 20 # sha1 generates 20-byte blocks |
| 87 total_blocks = int((keylen+digest_size-1)/digest_size) | 93 total_blocks = int((keylen+digest_size-1)/digest_size) |
| 88 hmac_template = HMAC(password, None, sha1) | 94 hmac_template = HMAC(password, None, sha1) |
| 89 out = _bempty | 95 out = _bempty |
| 90 for i in range(1, total_blocks+1): | 96 for i in range(1, total_blocks+1): |
| 91 hmac = hmac_template.copy() | 97 hmac = hmac_template.copy() |
| 92 hmac.update(salt + pack(">L",i)) | 98 hmac.update(salt + pack(">L", i)) |
| 93 block = tmp = hmac.digest() | 99 block = tmp = hmac.digest() |
| 94 for j in range(rounds-1): | 100 for _j in range(rounds-1): |
| 95 hmac = hmac_template.copy() | 101 hmac = hmac_template.copy() |
| 96 hmac.update(tmp) | 102 hmac.update(tmp) |
| 97 tmp = hmac.digest() | 103 tmp = hmac.digest() |
| 98 #TODO: need to speed up this call | 104 # TODO: need to speed up this call |
| 99 block = xor_bytes(block, tmp) | 105 block = xor_bytes(block, tmp) |
| 100 out += block | 106 out += block |
| 101 return out[:keylen] | 107 return out[:keylen] |
| 108 | |
| 102 | 109 |
| 103 def ssha(password, salt): | 110 def ssha(password, salt): |
| 104 ''' Make ssha digest from password and salt. | 111 ''' Make ssha digest from password and salt. |
| 105 Based on code of Roberto Aguilar <roberto@baremetal.io> | 112 Based on code of Roberto Aguilar <roberto@baremetal.io> |
| 106 https://gist.github.com/rca/7217540 | 113 https://gist.github.com/rca/7217540 |
| 107 ''' | 114 ''' |
| 108 shaval = sha1(password) # nosec | 115 shaval = sha1(password) # nosec |
| 109 shaval.update( salt ) | 116 shaval.update(salt) |
| 110 ssha_digest = b64encode( shaval.digest() + salt ).strip() | 117 ssha_digest = b64encode(shaval.digest() + salt).strip() |
| 111 return ssha_digest | 118 return ssha_digest |
| 119 | |
| 112 | 120 |
| 113 def pbkdf2(password, salt, rounds, keylen): | 121 def pbkdf2(password, salt, rounds, keylen): |
| 114 """pkcs#5 password-based key derivation v2.0 | 122 """pkcs#5 password-based key derivation v2.0 |
| 115 | 123 |
| 116 :arg password: passphrase to use to generate key (if unicode, converted to utf-8) | 124 :arg password: passphrase to use to generate key (if unicode, |
| 125 converted to utf-8) | |
| 117 :arg salt: salt bytes to use when generating key | 126 :arg salt: salt bytes to use when generating key |
| 118 :param rounds: number of rounds to use to generate key | 127 :param rounds: number of rounds to use to generate key |
| 119 :arg keylen: number of bytes to generate | 128 :arg keylen: number of bytes to generate |
| 120 | 129 |
| 121 If hashlib supports pbkdf2, uses it's implementation as backend. | 130 If hashlib supports pbkdf2, uses it's implementation as backend. |
| 123 :returns: | 132 :returns: |
| 124 raw bytes of generated key | 133 raw bytes of generated key |
| 125 """ | 134 """ |
| 126 password = s2b(us2s(password)) | 135 password = s2b(us2s(password)) |
| 127 if keylen > 40: | 136 if keylen > 40: |
| 128 #NOTE: pbkdf2 allows up to (2**31-1)*20 bytes, | 137 # NOTE: pbkdf2 allows up to (2**31-1)*20 bytes, |
| 129 # but m2crypto has issues on some platforms above 40, | 138 # but m2crypto has issues on some platforms above 40, |
| 130 # and such sizes aren't needed for a password hash anyways... | 139 # and such sizes aren't needed for a password hash anyways... |
| 131 raise ValueError("key length too large") | 140 raise ValueError("key length too large") |
| 132 if rounds < 1: | 141 if rounds < 1: |
| 133 raise ValueError("rounds must be positive number") | 142 raise ValueError("rounds must be positive number") |
| 134 return _pbkdf2(password, salt, rounds, keylen) | 143 return _pbkdf2(password, salt, rounds, keylen) |
| 135 | 144 |
| 145 | |
| 136 class PasswordValueError(ValueError): | 146 class PasswordValueError(ValueError): |
| 137 """ The password value is not valid """ | 147 """ The password value is not valid """ |
| 138 pass | 148 pass |
| 149 | |
| 139 | 150 |
| 140 def pbkdf2_unpack(pbkdf2): | 151 def pbkdf2_unpack(pbkdf2): |
| 141 """ unpack pbkdf2 encrypted password into parts, | 152 """ unpack pbkdf2 encrypted password into parts, |
| 142 assume it has format "{rounds}${salt}${digest} | 153 assume it has format "{rounds}${salt}${digest} |
| 143 """ | 154 """ |
| 144 pbkdf2 = us2s(pbkdf2) | 155 pbkdf2 = us2s(pbkdf2) |
| 145 try: | 156 try: |
| 146 rounds, salt, digest = pbkdf2.split("$") | 157 rounds, salt, digest = pbkdf2.split("$") |
| 147 except ValueError: | 158 except ValueError: |
| 148 raise PasswordValueError("invalid PBKDF2 hash (wrong number of separators)") | 159 raise PasswordValueError("invalid PBKDF2 hash (wrong number of " |
| 160 "separators)") | |
| 149 if rounds.startswith("0"): | 161 if rounds.startswith("0"): |
| 150 raise PasswordValueError("invalid PBKDF2 hash (zero-padded rounds)") | 162 raise PasswordValueError("invalid PBKDF2 hash (zero-padded rounds)") |
| 151 try: | 163 try: |
| 152 rounds = int(rounds) | 164 rounds = int(rounds) |
| 153 except ValueError: | 165 except ValueError: |
| 154 raise PasswordValueError("invalid PBKDF2 hash (invalid rounds)") | 166 raise PasswordValueError("invalid PBKDF2 hash (invalid rounds)") |
| 155 raw_salt = h64decode(salt) | 167 raw_salt = h64decode(salt) |
| 156 return rounds, salt, raw_salt, digest | 168 return rounds, salt, raw_salt, digest |
| 169 | |
| 157 | 170 |
| 158 def encodePassword(plaintext, scheme, other=None, config=None): | 171 def encodePassword(plaintext, scheme, other=None, config=None): |
| 159 """Encrypt the plaintext password. | 172 """Encrypt the plaintext password. |
| 160 """ | 173 """ |
| 161 if plaintext is None: | 174 if plaintext is None: |
| 177 elif scheme == 'SSHA': | 190 elif scheme == 'SSHA': |
| 178 if other: | 191 if other: |
| 179 raw_other = b64decode(other) | 192 raw_other = b64decode(other) |
| 180 salt = raw_other[20:] | 193 salt = raw_other[20:] |
| 181 else: | 194 else: |
| 182 #new password | 195 # new password |
| 183 # variable salt length | 196 # variable salt length |
| 184 salt_len = random_.randbelow(52-36) + 36 | 197 salt_len = random_.randbelow(52-36) + 36 |
| 185 salt = random_.token_bytes(salt_len) | 198 salt = random_.token_bytes(salt_len) |
| 186 s = ssha(s2b(plaintext), salt) | 199 s = ssha(s2b(plaintext), salt) |
| 187 elif scheme == 'SHA': | 200 elif scheme == 'SHA': |
| 196 salt = random_.choice(saltchars) + random_.choice(saltchars) | 209 salt = random_.choice(saltchars) + random_.choice(saltchars) |
| 197 s = crypt.crypt(plaintext, salt) | 210 s = crypt.crypt(plaintext, salt) |
| 198 elif scheme == 'plaintext': | 211 elif scheme == 'plaintext': |
| 199 s = plaintext | 212 s = plaintext |
| 200 else: | 213 else: |
| 201 raise PasswordValueError('Unknown encryption scheme %r'%scheme) | 214 raise PasswordValueError('Unknown encryption scheme %r' % scheme) |
| 202 return s | 215 return s |
| 216 | |
| 203 | 217 |
| 204 def generatePassword(length=12): | 218 def generatePassword(length=12): |
| 205 chars = string.ascii_letters+string.digits | 219 chars = string.ascii_letters+string.digits |
| 206 password = [random_.choice(chars) for x in range(length - 1)] | 220 password = [random_.choice(chars) for x in range(length - 1)] |
| 207 # make sure there is at least one digit | 221 # make sure there is at least one digit |
| 208 digitidx = random_.randbelow(length) | 222 digitidx = random_.randbelow(length) |
| 209 password[digitidx:digitidx] = [random_.choice(string.digits)] | 223 password[digitidx:digitidx] = [random_.choice(string.digits)] |
| 210 return ''.join(password) | 224 return ''.join(password) |
| 225 | |
| 211 | 226 |
| 212 class JournalPassword: | 227 class JournalPassword: |
| 213 """ Password dummy instance intended for journal operation. | 228 """ Password dummy instance intended for journal operation. |
| 214 We do not store passwords in the journal any longer. The dummy | 229 We do not store passwords in the journal any longer. The dummy |
| 215 version only reads the encryption scheme from the given | 230 version only reads the encryption scheme from the given |
| 216 encrypted password. | 231 encrypted password. |
| 217 """ | 232 """ |
| 218 default_scheme = 'PBKDF2' # new encryptions use this scheme | 233 default_scheme = 'PBKDF2' # new encryptions use this scheme |
| 219 pwre = re.compile(r'{(\w+)}(.+)') | 234 pwre = re.compile(r'{(\w+)}(.+)') |
| 220 | 235 |
| 221 def __init__ (self, encrypted=''): | 236 def __init__(self, encrypted=''): |
| 222 if isinstance(encrypted, self.__class__): | 237 if isinstance(encrypted, self.__class__): |
| 223 self.scheme = encrypted.scheme or self.default_scheme | 238 self.scheme = encrypted.scheme or self.default_scheme |
| 224 else: | 239 else: |
| 225 m = self.pwre.match(encrypted) | 240 m = self.pwre.match(encrypted) |
| 226 if m: | 241 if m: |
| 247 | 262 |
| 248 # assume password is plaintext | 263 # assume password is plaintext |
| 249 if self.password is None: | 264 if self.password is None: |
| 250 raise ValueError('Password not set') | 265 raise ValueError('Password not set') |
| 251 return self.password == encodePassword(other, self.scheme, | 266 return self.password == encodePassword(other, self.scheme, |
| 252 self.password or None) | 267 self.password or None) |
| 253 | 268 |
| 254 def __ne__(self, other): | 269 def __ne__(self, other): |
| 255 return not self.__eq__(other) | 270 return not self.__eq__(other) |
| 271 | |
| 256 | 272 |
| 257 class Password(JournalPassword): | 273 class Password(JournalPassword): |
| 258 """The class encapsulates a Password property type value in the database. | 274 """The class encapsulates a Password property type value in the database. |
| 259 | 275 |
| 260 The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'. | 276 The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'. |
| 274 >>> 'sekrit' == p | 290 >>> 'sekrit' == p |
| 275 1 | 291 1 |
| 276 >>> 'not sekrit' != p | 292 >>> 'not sekrit' != p |
| 277 1 | 293 1 |
| 278 """ | 294 """ |
| 279 #TODO: code to migrate from old password schemes. | 295 # TODO: code to migrate from old password schemes. |
| 280 | 296 |
| 281 deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"] | 297 deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"] |
| 282 known_schemes = ["PBKDF2", "SSHA"] + deprecated_schemes | 298 known_schemes = ["PBKDF2", "SSHA"] + deprecated_schemes |
| 283 | 299 |
| 284 def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False, config=None): | 300 def __init__(self, plaintext=None, scheme=None, encrypted=None, |
| 301 strict=False, config=None): | |
| 285 """Call setPassword if plaintext is not None.""" | 302 """Call setPassword if plaintext is not None.""" |
| 286 if scheme is None: | 303 if scheme is None: |
| 287 scheme = self.default_scheme | 304 scheme = self.default_scheme |
| 288 if plaintext is not None: | 305 if plaintext is not None: |
| 289 self.setPassword (plaintext, scheme, config=config) | 306 self.setPassword(plaintext, scheme, config=config) |
| 290 elif encrypted is not None: | 307 elif encrypted is not None: |
| 291 self.unpack(encrypted, scheme, strict=strict, config=config) | 308 self.unpack(encrypted, scheme, strict=strict, config=config) |
| 292 else: | 309 else: |
| 293 self.scheme = self.default_scheme | 310 self.scheme = self.default_scheme |
| 294 self.password = None | 311 self.password = None |
| 319 self.plaintext = None | 336 self.plaintext = None |
| 320 else: | 337 else: |
| 321 # currently plaintext - encrypt | 338 # currently plaintext - encrypt |
| 322 self.setPassword(encrypted, scheme, config=config) | 339 self.setPassword(encrypted, scheme, config=config) |
| 323 if strict and self.scheme not in self.known_schemes: | 340 if strict and self.scheme not in self.known_schemes: |
| 324 raise PasswordValueError("Unknown encryption scheme: %r" % (self.scheme,)) | 341 raise PasswordValueError("Unknown encryption scheme: %r" % |
| 342 (self.scheme,)) | |
| 325 | 343 |
| 326 def setPassword(self, plaintext, scheme=None, config=None): | 344 def setPassword(self, plaintext, scheme=None, config=None): |
| 327 """Sets encrypts plaintext.""" | 345 """Sets encrypts plaintext.""" |
| 328 if scheme is None: | 346 if scheme is None: |
| 329 scheme = self.default_scheme | 347 scheme = self.default_scheme |
| 333 | 351 |
| 334 def __str__(self): | 352 def __str__(self): |
| 335 """Stringify the encrypted password for database storage.""" | 353 """Stringify the encrypted password for database storage.""" |
| 336 if self.password is None: | 354 if self.password is None: |
| 337 raise ValueError('Password not set') | 355 raise ValueError('Password not set') |
| 338 return '{%s}%s'%(self.scheme, self.password) | 356 return '{%s}%s' % (self.scheme, self.password) |
| 357 | |
| 339 | 358 |
| 340 def test(): | 359 def test(): |
| 341 # SHA | 360 # SHA |
| 342 p = Password('sekrit') | 361 p = Password('sekrit') |
| 343 assert p == 'sekrit' | 362 assert p == 'sekrit' |
| 381 assert p == 'sekrit' | 400 assert p == 'sekrit' |
| 382 assert p != 'not sekrit' | 401 assert p != 'not sekrit' |
| 383 assert 'sekrit' == p | 402 assert 'sekrit' == p |
| 384 assert 'not sekrit' != p | 403 assert 'not sekrit' != p |
| 385 | 404 |
| 405 | |
| 386 if __name__ == '__main__': | 406 if __name__ == '__main__': |
| 387 test() | 407 test() |
| 388 | 408 |
| 389 # vim: set filetype=python sts=4 sw=4 et si : | 409 # vim: set filetype=python sts=4 sw=4 et si : |
