Code

- fix handling of traceback mails to the roundup admin
[roundup.git] / roundup / password.py
index 545baeb66690603808961c02c6617cc8ea84e03b..006c591be99cf39f6960b973bd3b48b8fa68b023 100644 (file)
@@ -23,7 +23,7 @@ __docformat__ = 'restructuredtext'
 
 import re, string, random
 from base64 import b64encode, b64decode
-from roundup.anypy.hashlib_ import md5, sha1
+from roundup.anypy.hashlib_ import md5, sha1, shamodule
 try:
     import crypt
 except ImportError:
@@ -59,10 +59,6 @@ except ImportError:
     #no m2crypto - make our own pbkdf2 function
     from struct import pack
     from hmac import HMAC
-    try:
-        from hashlib import sha1
-    except ImportError:
-        from sha import new as sha1
 
     def xor_bytes(left, right):
         "perform bitwise-xor of two byte-strings"
@@ -71,7 +67,7 @@ except ImportError:
     def _pbkdf2(password, salt, rounds, keylen):
         digest_size = 20 # sha1 generates 20-byte blocks
         total_blocks = int((keylen+digest_size-1)/digest_size)
-        hmac_template = HMAC(password, None, sha1)
+        hmac_template = HMAC(password, None, shamodule)
         out = _bempty
         for i in xrange(1, total_blocks+1):
             hmac = hmac_template.copy()
@@ -116,33 +112,40 @@ class PasswordValueError(ValueError):
     """ The password value is not valid """
     pass
 
-def encodePassword(plaintext, scheme, other=None):
+def pbkdf2_unpack(pbkdf2):
+    """ unpack pbkdf2 encrypted password into parts,
+        assume it has format "{rounds}${salt}${digest}
+    """
+    if isinstance(pbkdf2, unicode):
+        pbkdf2 = pbkdf2.encode("ascii")
+    try:
+        rounds, salt, digest = pbkdf2.split("$")
+    except ValueError:
+        raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
+    if rounds.startswith("0"):
+        raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
+    try:
+        rounds = int(rounds)
+    except ValueError:
+        raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
+    raw_salt = h64decode(salt)
+    return rounds, salt, raw_salt, digest
+
+def encodePassword(plaintext, scheme, other=None, config=None):
     """Encrypt the plaintext password.
     """
     if plaintext is None:
         plaintext = ""
     if scheme == "PBKDF2":
         if other:
-            #assume it has format "{rounds}${salt}${digest}"
-            if isinstance(other, unicode):
-                other = other.encode("ascii")
-            try:
-                rounds, salt, digest = other.split("$")
-            except ValueError:
-                raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
-            if rounds.startswith("0"):
-                raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
-            try:
-                rounds = int(rounds)
-            except ValueError:
-                raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
-            raw_salt = h64decode(salt)
+            rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
         else:
             raw_salt = getrandbytes(20)
             salt = h64encode(raw_salt)
-            #FIXME: find way to access config, so default rounds
-            # can be altered for faster/slower hosts via config.ini
-            rounds = 10000
+            if config:
+                rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
+            else:
+                rounds = 10000
         if rounds < 1000:
             raise PasswordValueError, "invalid PBKDF2 hash (rounds too low)"
         raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20)
@@ -234,22 +237,34 @@ class Password(JournalPassword):
     """
     #TODO: code to migrate from old password schemes.
 
-    known_schemes = [ "PBKDF2", "SHA", "MD5", "crypt", "plaintext" ]
+    deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"]
+    known_schemes = ["PBKDF2"] + deprecated_schemes
 
-    def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False):
+    def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False, config=None):
         """Call setPassword if plaintext is not None."""
         if scheme is None:
             scheme = self.default_scheme
         if plaintext is not None:
-            self.setPassword (plaintext, scheme)
+            self.setPassword (plaintext, scheme, config=config)
         elif encrypted is not None:
-            self.unpack(encrypted, scheme, strict=strict)
+            self.unpack(encrypted, scheme, strict=strict, config=config)
         else:
             self.scheme = self.default_scheme
             self.password = None
             self.plaintext = None
 
-    def unpack(self, encrypted, scheme=None, strict=False):
+    def needs_migration(self):
+        """ Password has insecure scheme or other insecure parameters
+            and needs migration to new password scheme
+        """
+        if self.scheme in self.deprecated_schemes:
+            return True
+        rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password)
+        if rounds < 1000:
+            return True
+        return False
+
+    def unpack(self, encrypted, scheme=None, strict=False, config=None):
         """Set the password info from the scheme:<encryted info> string
            (the inverse of __str__)
         """
@@ -260,16 +275,16 @@ class Password(JournalPassword):
             self.plaintext = None
         else:
             # currently plaintext - encrypt
-            self.setPassword(encrypted, scheme)
+            self.setPassword(encrypted, scheme, config=config)
         if strict and self.scheme not in self.known_schemes:
             raise PasswordValueError, "unknown encryption scheme: %r" % (self.scheme,)
 
-    def setPassword(self, plaintext, scheme=None):
+    def setPassword(self, plaintext, scheme=None, config=None):
         """Sets encrypts plaintext."""
         if scheme is None:
             scheme = self.default_scheme
         self.scheme = scheme
-        self.password = encodePassword(plaintext, scheme)
+        self.password = encodePassword(plaintext, scheme, config=config)
         self.plaintext = plaintext
 
     def __str__(self):