Skip to content

Commit

Permalink
100% coverage
Browse files Browse the repository at this point in the history
Also fix zpasswd under Python 3.
  • Loading branch information
jamadden committed Aug 7, 2017
1 parent e04f63a commit 46ba5fd
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 96 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

- Drop support for Python 3.3.

- Fix the ``zpasswd`` console script on Python 3.

4.2.0 (2016-07-07)
==================

Expand Down
8 changes: 2 additions & 6 deletions src/zope/password/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,5 @@

PY3 = sys.version_info[0] == 3

if PY3:
text_type = str
bytes_type = bytes
else:
text_type = unicode
bytes_type = str
text_type = str if bytes is not str else unicode
bytes_type = bytes
29 changes: 18 additions & 11 deletions src/zope/password/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,18 @@
try:
from crypt import crypt
from random import choice
except ImportError:
except ImportError: # pragma: no cover
# The crypt module is not universally available, apparently
crypt = None

from zope.interface import implementer
from zope.password.interfaces import IMatchingPasswordManager
from zope.password.compat import text_type

_encoder = getencoder("utf-8")

PY2 = sys.version_info[0] == 2

try:
unicode
except NameError:
# Py3: Define unicode.
unicode = str


if crypt is not None:
@implementer(IMatchingPasswordManager)
Expand Down Expand Up @@ -156,6 +151,10 @@ class MySQLPasswordManager(object):
{MYSQL}0ecd752c5097d395
>>> manager.match(encoded)
True
>>> manager.match(encoded.decode())
True
>>> manager.checkPassword(encoded.decode(), password)
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -165,7 +164,7 @@ class MySQLPasswordManager(object):
hash ``379693e271cd3bd6``, according to
http://phpsec.org/articles/2005/password-hashing.html
Our password manager generates the same value when seeded with the, so we
Our password manager generates the same value when seeded with the same seed, so we
can be sure, our output is compatible with MySQL versions before 4.1:
>>> password = 'PHP & Information Security'
Expand All @@ -186,6 +185,14 @@ class MySQLPasswordManager(object):
>>> manager.match('{MD5}someotherhash')
False
Spaces and tabs are ignored:
>>> encoded = manager.encodePassword('\tign or ed')
>>> print(encoded.decode())
{MYSQL}75818366052c6a78
>>> encoded = manager.encodePassword('ignored')
>>> print(encoded.decode())
{MYSQL}75818366052c6a78
"""


Expand All @@ -198,7 +205,7 @@ def encodePassword(self, password):
# In Python 2 bytes iterate over single-char strings.
i = ord(i)
if i == ord(b' ') or i == ord(b'\t'):
continue
continue # pragma: no cover (this is actually hit, but coverage isn't reporting it)
nr ^= (((nr & 63) + add) * i) + (nr << 8)
nr2 += (nr2 << 8) ^ nr
add += i
Expand All @@ -207,11 +214,11 @@ def encodePassword(self, password):
return ("{MYSQL}%08lx%08lx" % (r0, r1)).encode()

def checkPassword(self, encoded_password, password):
if isinstance(encoded_password, unicode):
if isinstance(encoded_password, text_type):
encoded_password = encoded_password.encode('ascii')
return encoded_password == self.encodePassword(password)

def match(self, encoded_password):
if isinstance(encoded_password, unicode):
if isinstance(encoded_password, text_type):
encoded_password = encoded_password.encode('ascii')
return encoded_password.startswith(b'{MYSQL}')
39 changes: 36 additions & 3 deletions src/zope/password/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

try:
import bcrypt
except ImportError:
except ImportError: # pragma: no cover
bcrypt = None

from zope.interface import implementer
Expand Down Expand Up @@ -112,6 +112,8 @@ class SSHAPasswordManager(PlainTextPasswordManager):
>>> manager.match(encoded)
True
>>> manager.match(encoded.decode())
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -138,6 +140,16 @@ class SSHAPasswordManager(PlainTextPasswordManager):
>>> manager.checkPassword(encoded, password + u"wrong")
False
We can also pass a salt that is a text string:
>>> salt = u'salt'
>>> password = 'secret'
>>> encoded = manager.encodePassword(password, salt)
>>> isinstance(encoded, bytes)
True
>>> print(encoded.decode())
{SSHA}gVK8WC9YyFT1gMsQHTGCgT3sSv5zYWx0
Because a random salt is generated, the output of encodePassword is
different every time you call it.
Expand Down Expand Up @@ -224,6 +236,8 @@ class SMD5PasswordManager(PlainTextPasswordManager):
>>> manager.match(encoded)
True
>>> manager.match(encoded.decode())
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand All @@ -250,6 +264,16 @@ class SMD5PasswordManager(PlainTextPasswordManager):
>>> manager.checkPassword(encoded, password + u"wrong")
False
We can also pass a salt that is a text string:
>>> salt = u'salt'
>>> password = 'secret'
>>> encoded = manager.encodePassword(password, salt)
>>> isinstance(encoded, bytes)
True
>>> print(encoded.decode())
{SMD5}mc0uWpXVVe5747A4pKhGJXNhbHQ=
Because a random salt is generated, the output of encodePassword is
different every time you call it.
Expand Down Expand Up @@ -313,6 +337,8 @@ class MD5PasswordManager(PlainTextPasswordManager):
{MD5}ht3czsRdtFmfGsAAGOVBOQ==
>>> manager.match(encoded)
True
>>> manager.match(encoded.decode())
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand Down Expand Up @@ -391,6 +417,8 @@ class SHA1PasswordManager(PlainTextPasswordManager):
{SHA}BLTuxxVMXzouxtKVb7gLgNxzdAI=
>>> manager.match(encoded)
True
>>> manager.match(encoded.decode())
True
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
Expand Down Expand Up @@ -489,6 +517,11 @@ def _clean_hashed(self, hashed_password):
def checkPassword(self, hashed_password, clear_password):
"""Check a `hashed_password` against a `clear password`.
>>> from zope.password.password import BCRYPTPasswordManager
>>> manager = BCRYPTPasswordManager()
>>> manager.checkPassword(b'not from here', None)
False
:param hashed_password: The encoded password.
:type hashed_password: str
:param clear_password: The password to check.
Expand All @@ -502,7 +535,7 @@ def checkPassword(self, hashed_password, clear_password):
pw_hash = hashed_password[len(self._prefix):]
try:
ok = bcrypt.checkpw(pw_bytes, pw_hash)
except ValueError:
except ValueError: # pragma: no cover
# invalid salt
ok = False
return ok
Expand All @@ -526,7 +559,7 @@ def encodePassword(self, password, salt=None):
return self._prefix + bcrypt.hashpw(pw, salt=salt)

def match(self, hashed_password):
"""Was the password hashed with this password manager.
"""Was the password hashed with this password manager?
:param hashed_password: The encoded password.
:type hashed_password: str
Expand Down
2 changes: 1 addition & 1 deletion src/zope/password/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

try:
from zope.password.legacy import CryptPasswordManager
except ImportError:
except ImportError: # pragma: no cover
CryptPasswordManager = None


Expand Down
6 changes: 1 addition & 5 deletions src/zope/password/tests/test_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,9 @@
"""
import contextlib
import doctest
import re
import unittest

try:
import bcrypt
except ImportError:
bcrypt = None
import bcrypt

from zope.interface.verify import verifyObject
from zope.testing import renormalizing
Expand Down
Loading

0 comments on commit 46ba5fd

Please sign in to comment.