diff --git a/src/authentic2/hashers.py b/src/authentic2/hashers.py index 579354c22..c72f6ae8c 100644 --- a/src/authentic2/hashers.py +++ b/src/authentic2/hashers.py @@ -235,3 +235,49 @@ class JoomlaPasswordHasher(CommonPasswordHasher): return '%s:%s' % (_hash, salt.decode('hex')) else: return _hash + + +class PloneSHA1PasswordHasher(hashers.SHA1PasswordHasher): + # from https://www.fourdigits.nl/blog/converting-plone-data-to-django/ + """ + The SHA1 password hashing algorithm used by Plone. + + Plone uses `password + salt`, Django has `salt + password`. + """ + + algorithm = "plonesha1" + _prefix = '{SSHA}' + + def encode(self, password, salt): + """Encode a plain text password into a plonesha1 style hash.""" + assert password is not None + assert salt + password = force_bytes(password) + salt = force_bytes(salt) + + hashed = base64.b64encode(hashlib.sha1(password + salt).digest() + salt) + return "%s$%s%s" % (self.algorithm, self._prefix, hashed) + + def verify(self, password, encoded): + """Verify the given password against the encoded string.""" + algorithm, data = encoded.split('$', 1) + assert algorithm == self.algorithm + + # throw away the prefix + if data.startswith(self._prefix): + data = data[len(self._prefix):] + + # extract salt from encoded data + intermediate = base64.b64decode(data) + salt = intermediate[20:].strip() + + password_encoded = self.encode(password, salt) + return constant_time_compare(password_encoded, encoded) + + def safe_summary(self, encoded): + algorithm, hash = encoded.split('$', 1) + assert algorithm == self.algorithm + return OrderedDict([ + (_('algorithm'), algorithm), + (_('hash'), hashers.mask_hash(hash)), + ]) diff --git a/src/authentic2/settings.py b/src/authentic2/settings.py index 1de62951c..15a443e21 100644 --- a/src/authentic2/settings.py +++ b/src/authentic2/settings.py @@ -191,7 +191,8 @@ PASSWORD_HASHERS = list(global_settings.PASSWORD_HASHERS) + [ 'authentic2.hashers.SSHA1PasswordHasher', 'authentic2.hashers.SMD5PasswordHasher', 'authentic2.hashers.SHA1OLDAPPasswordHasher', - 'authentic2.hashers.MD5OLDAPPasswordHasher' + 'authentic2.hashers.MD5OLDAPPasswordHasher', + 'authentic2.hashers.PloneSHA1PasswordHasher', ] # Admin tools diff --git a/tests/test_hashers.py b/tests/test_hashers.py index 397ff56b8..53d2f697b 100644 --- a/tests/test_hashers.py +++ b/tests/test_hashers.py @@ -30,3 +30,10 @@ def test_joomla_hasher(): assert hashers.JoomlaPasswordHasher().verify(pwd, dj_encoded) assert hashers.JoomlaPasswordHasher.to_joomla(dj_encoded) == encoded + + +def test_plone_hasher(): + hasher = hashers.PloneSHA1PasswordHasher() + assert hasher.verify( + 'Azerty!123', + 'plonesha1${SSHA}vS4g4MtzJyAjvhyW7vsrgjpJ6lDCU+Y42a6p')