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 :

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