From b29d263fc6ac97ac22c66f4d526ea1c2fec34374 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Sun, 20 Feb 2011 11:23:47 +0000 Subject: [PATCH] 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. 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. --- CHANGES.txt | 6 ++-- README.txt | 9 +++++- setup.py | 2 +- src/zope/password/interfaces.py | 7 +++++ src/zope/password/password.py | 55 +++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 403f3e8..9a4c917 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/README.txt b/README.txt index 6c19f17..925f6af 100644 --- a/README.txt +++ b/README.txt @@ -39,7 +39,7 @@ 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""" @@ -47,6 +47,13 @@ two methods:: 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. diff --git a/setup.py b/setup.py index e1e0e21..de5e684 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/src/zope/password/interfaces.py b/src/zope/password/interfaces.py index fdd18fa..3c96c1d 100644 --- a/src/zope/password/interfaces.py +++ b/src/zope/password/interfaces.py @@ -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. + + """ diff --git a/src/zope/password/password.py b/src/zope/password/password.py index 4af8991..7619377 100644 --- a/src/zope/password/password.py +++ b/src/zope/password/password.py @@ -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) @@ -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. @@ -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") @@ -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) @@ -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. @@ -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") @@ -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") @@ -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) @@ -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. @@ -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") @@ -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") @@ -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) @@ -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 = [