comparison MoinMoin/user.py @ 3808:8c5fbc62dd1d

user storage: hash stored passwords, upgrade on use, remove charset magic
author Johannes Berg <johannes AT sipsolutions DOT net>
date Mon, 30 Jun 2008 21:41:04 +0200
parents cfbb31fbd6b7
children 88bec029f481
comparison
equal deleted inserted replaced
3807:2ae602072baf 3808:8c5fbc62dd1d
20 """ 20 """
21 21
22 # add names here to hide them in the cgitb traceback 22 # add names here to hide them in the cgitb traceback
23 unsafe_names = ("id", "key", "val", "user_data", "enc_password", "recoverpass_key") 23 unsafe_names = ("id", "key", "val", "user_data", "enc_password", "recoverpass_key")
24 24
25 import os, time, sha, codecs, hmac 25 import os, time, sha, codecs, hmac, base64
26 26
27 from MoinMoin import config, caching, wikiutil, i18n, events 27 from MoinMoin import config, caching, wikiutil, i18n, events
28 from MoinMoin.util import timefuncs, filesys, random_string 28 from MoinMoin.util import timefuncs, filesys, random_string
29 from MoinMoin.wikiutil import url_quote_plus 29 from MoinMoin.wikiutil import url_quote_plus
30 30
138 username = request.user.name 138 username = request.user.name
139 139
140 return username or (request.cfg.show_hosts and request.remote_addr) or _("<unknown>") 140 return username or (request.cfg.show_hosts and request.remote_addr) or _("<unknown>")
141 141
142 142
143 def encodePassword(pwd, charset='utf-8'): 143 def encodePassword(pwd):
144 """ Encode a cleartext password 144 """ Encode a cleartext password
145
146 Compatible to Apache htpasswd SHA encoding.
147
148 When using different encoding than 'utf-8', the encoding might fail
149 and raise UnicodeError.
150 145
151 @param pwd: the cleartext password, (unicode) 146 @param pwd: the cleartext password, (unicode)
152 @param charset: charset used to encode password, used only for 147 @param charset: charset used to encode password, used only for
153 compatibility with old passwords generated on moin-1.2. 148 compatibility with old passwords generated on moin-1.2.
154 @rtype: string 149 @rtype: string
155 @return: the password in apache htpasswd compatible SHA-encoding, 150 @return: the password in apache htpasswd compatible SHA-encoding,
156 or None 151 or None
157 """ 152 """
158 import base64 153 pwd = pwd.encode('utf-8')
159 154
160 # Might raise UnicodeError, but we can't do anything about it here, 155 salt = random_string(20)
161 # so let the caller handle it. 156 hash = sha.new(pwd)
162 pwd = pwd.encode(charset) 157 hash.update(salt)
163 158
164 pwd = sha.new(pwd).digest() 159 return '{SSHA}' + base64.b64encode(hash.digest() + salt).rstrip()
165 pwd = '{SHA}' + base64.encodestring(pwd).rstrip()
166 return pwd
167 160
168 161
169 def normalizeName(name): 162 def normalizeName(name):
170 """ Make normalized user name 163 """ Make normalized user name
171 164
314 setattr(self, key, self._cfg.user_checkbox_defaults.get(key, 0)) 307 setattr(self, key, self._cfg.user_checkbox_defaults.get(key, 0))
315 308
316 self.recoverpass_key = "" 309 self.recoverpass_key = ""
317 310
318 self.enc_password = "" 311 self.enc_password = ""
319 if password:
320 try:
321 self.enc_password = encodePassword(password)
322 except UnicodeError:
323 pass # Should never happen
324 312
325 #self.edit_cols = 80 313 #self.edit_cols = 80
326 self.tz_offset = int(float(self._cfg.tz_offset) * 3600) 314 self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
327 self.language = "" 315 self.language = ""
328 self.real_language = "" # In case user uses "Browser setting". For language-statistics 316 self.real_language = "" # In case user uses "Browser setting". For language-statistics
341 # attrs not saved to profile 329 # attrs not saved to profile
342 self._request = request 330 self._request = request
343 self._trail = [] 331 self._trail = []
344 332
345 # we got an already authenticated username: 333 # we got an already authenticated username:
346 check_pass = 0 334 check_password = None
347 if not self.id and self.auth_username: 335 if not self.id and self.auth_username:
348 self.id = getUserId(request, self.auth_username) 336 self.id = getUserId(request, self.auth_username)
349 if not password is None: 337 if not password is None:
350 check_pass = 1 338 check_password = password
351 if self.id: 339 if self.id:
352 self.load_from_id(check_pass) 340 self.load_from_id(check_password)
353 elif self.name: 341 elif self.name:
354 self.id = getUserId(self._request, self.name) 342 self.id = getUserId(self._request, self.name)
355 if self.id: 343 if self.id:
356 self.load_from_id(1) 344 # no password given should fail
345 self.load_from_id(password or u'')
357 else: 346 else:
358 self.id = self.make_id() 347 self.id = self.make_id()
359 else: 348 else:
360 self.id = self.make_id() 349 self.id = self.make_id()
361 350
405 @rtype: bool 394 @rtype: bool
406 @return: true, if we have a user account 395 @return: true, if we have a user account
407 """ 396 """
408 return os.path.exists(self.__filename()) 397 return os.path.exists(self.__filename())
409 398
410 def load_from_id(self, check_pass=0): 399 def load_from_id(self, password=None):
411 """ Load user account data from disk. 400 """ Load user account data from disk.
412 401
413 Can only load user data if the id number is already known. 402 Can only load user data if the id number is already known.
414 403
415 This loads all member variables, except "id" and "valid" and 404 This loads all member variables, except "id" and "valid" and
416 those starting with an underscore. 405 those starting with an underscore.
417 406
418 @param check_pass: If 1, then self.enc_password must match the 407 @param password: If not None, then the given password must match the
419 password in the user account file. 408 password in the user account file.
420 """ 409 """
421 if not self.exists(): 410 if not self.exists():
422 return 411 return
423 412
424 data = codecs.open(self.__filename(), "r", config.charset).readlines() 413 data = codecs.open(self.__filename(), "r", config.charset).readlines()
448 437
449 # Validate data from user file. In case we need to change some 438 # Validate data from user file. In case we need to change some
450 # values, we set 'changed' flag, and later save the user data. 439 # values, we set 'changed' flag, and later save the user data.
451 changed = 0 440 changed = 0
452 441
453 if check_pass: 442 if password is not None:
454 # If we have no password set, we don't accept login with username 443 # Check for a valid password, possibly changing storage
455 if not user_data['enc_password']:
456 return
457 # Check for a valid password, possibly changing encoding
458 valid, changed = self._validatePassword(user_data) 444 valid, changed = self._validatePassword(user_data)
459 if not valid: 445 if not valid:
460 return 446 return
461 447
462 # Remove ignored checkbox values from user data 448 # Remove ignored checkbox values from user data
500 # If user data has been changed, save fixed user data. 486 # If user data has been changed, save fixed user data.
501 if changed: 487 if changed:
502 self.save() 488 self.save()
503 489
504 def _validatePassword(self, data): 490 def _validatePassword(self, data):
505 """ Try to validate user password 491 """
492 Check user password.
506 493
507 This is a private method and should not be used by clients. 494 This is a private method and should not be used by clients.
508 495
509 In pre 1.3, the wiki used some 8 bit charset. The user password 496 @param data: dict with user data (from storage)
510 was entered in this 8 bit password and passed to
511 encodePassword. So old passwords can use any of the charset
512 used.
513
514 In 1.3, we use unicode internally, so we encode the password in
515 encodePassword using utf-8.
516
517 When we compare passwords we must compare with same encoding, or
518 the passwords will not match. We don't know what encoding the
519 password on the user file uses. We may ask the wiki admin to put
520 this into the config, but he may be wrong.
521
522 The way chosen is to try to encode and compare passwords using
523 all the encoding that were available on 1.2, until we get a
524 match, which means that the user is valid.
525
526 If we get a match, we replace the user password hash with the
527 utf-8 encoded version, and next time it will match on first try
528 as before. The user password did not change, this change is
529 completely transparent for the user. Only the sha digest will
530 change.
531
532 @param data: dict with user data
533 @rtype: 2 tuple (bool, bool) 497 @rtype: 2 tuple (bool, bool)
534 @return: password is valid, password did change 498 @return: password is valid, enc_password changed
535 """ 499 """
536 # First try with default encoded password. Match only non empty 500 epwd = data['enc_password']
537 # passwords. (require non empty enc_password) 501
538 if self.enc_password and self.enc_password == data['enc_password']: 502 # If we have no password set, we don't accept login with username
539 return True, False 503 if not epwd:
540 504 return False, False
541 # Try to match using one of pre 1.3 8 bit charsets
542 505
543 # Get the clear text password from the form (require non empty 506 # Get the clear text password from the form (require non empty
544 # password) 507 # password)
545 password = self._request.form.get('password', [None])[0] 508 password = self._request.form.get('password', [None])[0]
546 if not password: 509 if not password:
547 return False, False 510 return False, False
548 511
549 # First get all available pre13 charsets on this system 512 if epwd[:5] == '{SHA}':
550 pre13 = ['iso-8859-1', 'iso-8859-2', 'euc-jp', 'gb2312', 'big5', ] 513 enc = '{SHA}' + base64.encodestring(sha.new(password).digest()).rstrip()
551 available = [] 514 if epwd == enc:
552 for charset in pre13: 515 data['enc_password'] = encodePassword(password)
553 try:
554 encoder = codecs.getencoder(charset)
555 available.append(charset)
556 except LookupError:
557 pass # missing on this system
558
559 # Now try to match the password
560 for charset in available:
561 # Try to encode, failure is expected
562 try:
563 enc_password = encodePassword(password, charset=charset)
564 except UnicodeError:
565 continue
566
567 # And match (require non empty enc_password)
568 if enc_password and enc_password == data['enc_password']:
569 # User password match - replace the user password in the
570 # file with self.password
571 data['enc_password'] = self.enc_password
572 return True, True 516 return True, True
517 return False, False
518
519 if epwd[:6] == '{SSHA}':
520 print epwd[6:]
521 data = base64.b64decode(epwd[6:])
522 salt = data[20:]
523 hash = sha.new(password)
524 hash.update(salt)
525 return hash.digest() == data[:20], False
573 526
574 # No encoded password match, this must be wrong password 527 # No encoded password match, this must be wrong password
575 return False, False 528 return False, False
576 529
577 def persistent_items(self): 530 def persistent_items(self):