Index: repoze/who/plugins/tests/test_sql.py
===================================================================
--- repoze/who/plugins/tests/test_sql.py	(revision 4767)
+++ repoze/who/plugins/tests/test_sql.py	(working copy)
@@ -80,18 +80,62 @@
             from sha import new as sha1
         return sha1(clear).hexdigest()
 
-    def test_shaprefix_success(self):
+    def _get_sha_b64_digest(self, clear='password'):
+        try:
+            from hashlib import sha1
+        except ImportError:
+            from sha import new as sha1
+        from base64 import urlsafe_b64encode
+        return urlsafe_b64encode(sha1(clear).digest())
+
+    def _get_ssha_b64_digest(self, clear='password'):
+        try:
+            from hashlib import sha1
+        except ImportError:
+            from sha import new as sha1
+        from os import urandom
+        from base64 import urlsafe_b64encode
+        salt = urandom(4)
+        hasher = sha1(clear)
+        hasher.update(salt)
+        return "{SSHA}%s" % urlsafe_b64encode(hasher.digest() + salt)
+
+    def test_shahex_prefix_success(self):
         stored = '{SHA}' +  self._get_sha_hex_digest()
         compare = self._getFUT()
         result = compare('password', stored)
         self.assertEqual(result, True)
 
-    def test_shaprefix_fail(self):
+    def test_shahex_prefix_fail(self):
         stored = '{SHA}' + self._get_sha_hex_digest()
         compare = self._getFUT()
         result = compare('notpassword', stored)
         self.assertEqual(result, False)
 
+    def test_shab64_prefix_success(self):
+        stored = '{SHA}' +  self._get_sha_b64_digest()
+        compare = self._getFUT()
+        result = compare('password', stored)
+        self.assertEqual(result, True)
+
+    def test_shab64_prefix_fail(self):
+        stored = '{SHA}' + self._get_sha_b64_digest()
+        compare = self._getFUT()
+        result = compare('notpassword', stored)
+        self.assertEqual(result, False)
+
+    def test_sshaprefix_success(self):
+        stored = self._get_ssha_b64_digest()
+        compare = self._getFUT()
+        result = compare('password', stored)
+        self.assertEqual(result, True)
+
+    def test_sshaprefix_fail(self):
+        stored = self._get_ssha_b64_digest()
+        compare = self._getFUT()
+        result = compare('notpassword', stored)
+        self.assertEqual(result, False)
+
     def test_noprefix_success(self):
         stored = 'password'
         compare = self._getFUT()
@@ -104,6 +148,51 @@
         result = compare('notpassword', stored)
         self.assertEqual(result, False)
 
+class TestDefaultPasswordHash(unittest.TestCase):
+
+    def _getFUT(self):
+        from repoze.who.plugins.sql import default_password_hash
+        return default_password_hash
+
+    def test_scheme_sha(self):
+        hasher = self._getFUT()
+        password = 'password'
+        digest = hasher(password, scheme='SHA')
+        self.failUnless(digest.startswith('{SHA}'))
+        self.failUnless(digest.endswith('='))
+
+    def test_scheme_ssha(self):
+        hasher = self._getFUT()
+        password = 'password'
+        digest = hasher(password, scheme='SSHA')
+        self.failUnless(digest.startswith('{SSHA}'))
+
+    def test_scheme_none(self):
+        hasher = self._getFUT()
+        password = 'password'
+        digest = hasher(password, scheme=None)
+        self.assertEqual(digest, password)
+
+    def test_scheme_unimplemented(self):
+        hasher = self._getFUT()
+        password = 'password'
+        self.assertRaises(NotImplementedError, hasher, password, 'noscheme')
+
+class TestDefaultPasswordRoundTrip(unittest.TestCase):
+
+    def _roundtrip(self, scheme, cleartext_password='password'):
+        from repoze.who.plugins.sql import default_password_compare, default_password_hash
+        return default_password_compare(cleartext_password, default_password_hash(cleartext_password, scheme=scheme))
+        
+    def test_sha_scheme(self):
+        self.assertTrue(self._roundtrip('SHA'))
+
+    def test_ssha_scheme(self):
+        self.assertTrue(self._roundtrip('SSHA'))
+
+    def test_no_scheme(self):
+        self.assertTrue(self._roundtrip(None))
+
 class TestSQLMetadataProviderPlugin(unittest.TestCase):
 
     def _getTargetClass(self):
Index: repoze/who/plugins/sql.py
===================================================================
--- repoze/who/plugins/sql.py	(revision 4767)
+++ repoze/who/plugins/sql.py	(working copy)
@@ -8,13 +8,36 @@
         from hashlib import sha1
     except ImportError: # Python < 2.5 #pragma NO COVERAGE
         from sha import new as sha1    #pragma NO COVERAGE
+    from base64 import urlsafe_b64encode, urlsafe_b64decode
 
     # the stored password is stored as '{SHA}<SHA hexdigest>'.
     # or as a cleartext password (no {SHA} prefix)
 
     if stored_password_hash.startswith('{SHA}'):
-        stored_password_hash = stored_password_hash[5:]
-        digest = sha1(cleartext_password).hexdigest()
+        stored_password_hash = stored_password_hash[len('{SHA}'):]
+        if stored_password_hash.endswith('='):
+            # The hash has been base64 encoded
+            digest = urlsafe_b64encode(sha1(cleartext_password).digest())
+        else:
+            # The hash is using the nonstandard hex encoding.
+            digest = sha1(cleartext_password).hexdigest()
+    elif stored_password_hash.startswith('{SSHA}'):
+        stored_password_hash = stored_password_hash[len('{SSHA}'):]
+        try:
+            challenge_bytes = urlsafe_b64decode(stored_password_hash)
+        except TypeError:
+            # Could happen if the site saves cleartext passwords (I hope not)
+            # and the user supplies an invalid SSHA hash as their cleartext
+            # password.  For safety sake, I think we need to reject this
+            # case...
+            return False
+        # SHA-1 Digests are 160 bits long (20 bytes or 40 hexbytes)
+        stored_digest, salt = challenge_bytes[:20], challenge_bytes[20:]
+
+        hasher = sha1(cleartext_password)
+        hasher.update(salt)
+        return stored_digest == hasher.digest()
+        
     else:
         digest = cleartext_password
         
@@ -23,6 +46,33 @@
 
     return False
 
+def default_password_hash(cleartext_password, scheme='SSHA'):
+    try:
+        from hashlib import sha1
+    except ImportError: # Python < 2.5 #pragma NO COVERAGE
+        from sha import new as sha1    #pragma NO COVERAGE
+    from base64 import urlsafe_b64encode
+    from os import urandom
+
+    # We support three schemes, SSHA (the default) or Salted SHA, SHA, and
+    # none.  In the case where we use SSHA, the stored format is:
+    # '{SSHA}<base64 encoding of 20 bytes of SHA digest followed by salt> for
+    # SHA, it's stored as '{SHA}<28 characters of base64 SHA digest, which ends
+    # with "=">'.  The final possibility is a cleartext password (no {SHA}
+    # prefix)
+    if scheme == 'SSHA':
+        salt = urandom(4)
+        hasher = sha1(cleartext_password)
+        hasher.update(salt)
+        return "{SSHA}%s" % urlsafe_b64encode(hasher.digest() + salt)
+    elif scheme == 'SHA':
+        hasher = sha1(cleartext_password)
+        return "{SHA}%s" % urlsafe_b64encode(hasher.digest())
+    elif not scheme:
+        return cleartext_password
+    else:
+        raise NotImplementedError
+
 def make_psycopg_conn_factory(**kw):
     # convenience (I always seem to use Postgres)
     def conn_factory(): #pragma NO COVERAGE
Index: CHANGES.txt
===================================================================
--- CHANGES.txt	(revision 4767)
+++ CHANGES.txt	(working copy)
@@ -10,6 +10,14 @@
 
 - One-hundred percent unit test coverage.
 
+- Added 'default_password_hash' which provides a default hash implementation
+  that matches the default_password_compare.
+
+- Added salted SHA (SSHA) support to the default_password_compare.
+
+- Changed the default hash storage to use base64 encoding, which is more
+  standards compliant.  Older hex based storage is supported as well.
+
 1.0.13 (2009/4/24)
 ==================
 
