Skip to content

Commit

Permalink
Add a 'match' method to the IPasswordManager interface, which returns…
Browse files Browse the repository at this point in the history
… True if a given password hash was encdoded with the scheme implemented by the specific manager.

Note that the plain-text manager always returns False for this method, as the alternative is to always return True and thus also validate hashed password against their literal values, a security risk.
  • Loading branch information
Martijn Pieters committed Feb 20, 2011
1 parent 77fb0ec commit b29d263
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 4 deletions.
6 changes: 4 additions & 2 deletions CHANGES.txt
Expand Up @@ -2,10 +2,12 @@
CHANGES
=======

3.6.2 (unreleased)
4.0.0 (unreleased)
------------------

- Nothing changed yet.
- Add a 'match' method to the IPasswordManager interface, which returns True
if a given password hash was encdoded with the scheme implemented by the
specific manager.


3.6.1 (2010-05-27)
Expand Down
9 changes: 8 additions & 1 deletion README.txt
Expand Up @@ -39,14 +39,21 @@ Usage

It's very easy to use password managers. The
``zope.password.interfaces.IPasswordManager`` interface defines only
two methods::
three methods::

def encodePassword(password):
"""Return encoded data for the given password"""

def checkPassword(encoded_password, password):
"""Return whether the given encoded data coincide with the given password"""

def match(encoded_password):
"""
Returns True when the given data was encoded with the scheme
implemented by this password manager.

"""

The implementations mentioned above are in the
``zope.password.password`` module.

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -17,7 +17,7 @@


setup(name='zope.password',
version='3.6.2dev',
version='4.0.0dev',
author='Zope Foundation and Contributors',
author_email='zope-dev@zope.org',
description='Password encoding and checking utilities',
Expand Down
7 changes: 7 additions & 0 deletions src/zope/password/interfaces.py
Expand Up @@ -23,3 +23,10 @@ def encodePassword(password):

def checkPassword(encoded_password, password):
"""Does the given encoded data coincide with the given password"""

def match(encoded_password):
"""
Returns True when the given data was encoded with the scheme
implemented by this password manager.
"""
55 changes: 55 additions & 0 deletions src/zope/password/password.py
Expand Up @@ -50,6 +50,15 @@ class PlainTextPasswordManager(object):
True
>>> manager.checkPassword(encoded, password + u"wrong")
False
The plain text password manager *never* claims to implement the scheme,
because this would open a security hole, where a hash from a different
scheme could be used as-is as a plain-text password. Authentication code
that needs to support plain-text passwords need to explicitly check for
plain-text password matches after all other options have been tested for::
>>> manager.match(encoded)
False
"""

implements(IPasswordManager)
Expand All @@ -60,6 +69,14 @@ def encodePassword(self, password):
def checkPassword(self, encoded_password, password):
return encoded_password == self.encodePassword(password)

def match(self, encoded_password):
# We always return False for PlainText because it was a) not encrypted
# and b) matching against actual encryption methods would result in
# the ability to authenticate with the un-encrypted hash as a password.
# For example, you should not be able to authenticate with a literal
# SSHA hash.
return False


class SSHAPasswordManager(PlainTextPasswordManager):
"""SSHA password manager.
Expand All @@ -83,6 +100,8 @@ class SSHAPasswordManager(PlainTextPasswordManager):
>>> encoded
'{SSHA}BLTuxxVMXzouxtKVb7gLgNxzdAI='
>>> manager.match(encoded)
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand Down Expand Up @@ -118,6 +137,12 @@ class SSHAPasswordManager(PlainTextPasswordManager):
>>> manager.checkPassword(unicode(manager.encodePassword(passwd)), passwd)
True
The manager only claims to implement SSHA encodings, anything not starting
with the string {SSHA} returns False::
>>> manager.match('{MD5}someotherhash')
False
"""

implements(IPasswordManager)
Expand All @@ -138,6 +163,9 @@ def checkPassword(self, encoded_password, password):
salt = byte_string[20:]
return encoded_password == self.encodePassword(password, salt)

def match(self, encoded_password):
return encoded_password.startswith('{SSHA}')


class MD5PasswordManager(PlainTextPasswordManager):
"""MD5 password manager.
Expand All @@ -155,6 +183,8 @@ class MD5PasswordManager(PlainTextPasswordManager):
>>> encoded = manager.encodePassword(password, salt="")
>>> encoded
'{MD5}86dddccec45db4599f1ac00018e54139'
>>> manager.match(encoded)
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -163,6 +193,8 @@ class MD5PasswordManager(PlainTextPasswordManager):
>>> encoded = manager.encodePassword(password)
>>> encoded[-32:]
'86dddccec45db4599f1ac00018e54139'
>>> manager.match(encoded)
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -181,6 +213,13 @@ class MD5PasswordManager(PlainTextPasswordManager):
>>> manager.checkPassword(encoded, password)
True
However, because the prefix is missing, the password manager cannot claim
to implement the scheme:
>>> manager.match(encoded)
False
"""

implements(IPasswordManager)
Expand All @@ -197,6 +236,9 @@ def checkPassword(self, encoded_password, password):
salt = encoded_password[:-32]
return encoded_password == self.encodePassword(password, salt)[5:]

def match(self, encoded_password):
return encoded_password.startswith('{MD5}')


class SHA1PasswordManager(PlainTextPasswordManager):
"""SHA1 password manager.
Expand All @@ -214,6 +256,8 @@ class SHA1PasswordManager(PlainTextPasswordManager):
>>> encoded = manager.encodePassword(password, salt="")
>>> encoded
'{SHA1}04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
>>> manager.match(encoded)
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -222,6 +266,8 @@ class SHA1PasswordManager(PlainTextPasswordManager):
>>> encoded = manager.encodePassword(password)
>>> encoded[-40:]
'04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
>>> manager.match(encoded)
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -241,6 +287,12 @@ class SHA1PasswordManager(PlainTextPasswordManager):
>>> manager.checkPassword(encoded, password)
True
However, because the prefix is missing, the password manager cannot claim
to implement the scheme:
>>> manager.match(encoded)
False
"""

implements(IPasswordManager)
Expand All @@ -257,6 +309,9 @@ def checkPassword(self, encoded_password, password):
salt = encoded_password[:-40]
return encoded_password == self.encodePassword(password, salt)[6:]

def match(self, encoded_password):
return encoded_password.startswith('{SHA1}')


# Simple registry
managers = [
Expand Down

0 comments on commit b29d263

Please sign in to comment.