Skip to content

Commit

Permalink
Support decoding z3c.bcrypt passwords.
Browse files Browse the repository at this point in the history
Fixes #10
  • Loading branch information
jamadden committed Aug 18, 2017
1 parent d517a5a commit f66cbf9
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 4 deletions.
9 changes: 7 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
4.3.0 (unreleased)
==================

- Added a ``bcrypt``-based password manager (available only if the ``bcrypt``
library is importable).
- Added a ``bcrypt``-based password manager (available only if the
`bcrypt <https://pypi.python.org/pypi/bcrypt>`_ library is
importable). This manager can also check passwords that were encoded
with `z3c.bcrypt <https://pypi.python.org/pypi/z3c.bcrypt>`_. If
that package is *not* installed, then ``configure.zcml`` will
install this manager as a utility with both the ``BCRYPT``
(preferred) and ``bcrypt`` names for compatibility with it.

- Add support for Python 3.6.

Expand Down
12 changes: 12 additions & 0 deletions src/zope/password/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:zcml="http://namespaces.zope.org/zcml"
>

<include package="zope.component" file="meta.zcml" />

<utility
name="Plain Text"
provides=".interfaces.IMatchingPasswordManager"
Expand Down Expand Up @@ -45,6 +47,16 @@
provides=".interfaces.IMatchingPasswordManager"
factory=".password.BCRYPTPasswordManager"
/>
<!--
Also install it under the same name as z3c.bcrypt does
for compatibility with that library.
-->
<utility
zcml:condition="not-installed z3c.bcrypt"
name="bcrypt"
provides=".interfaces.IMatchingPasswordManager"
factory=".password.BCRYPTPasswordManager"
/>
</configure>

<configure zcml:condition="installed crypt">
Expand Down
18 changes: 16 additions & 2 deletions src/zope/password/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from hashlib import md5, sha1
from os import urandom

import re

try:
import bcrypt
except ImportError: # pragma: no cover
Expand Down Expand Up @@ -501,13 +503,21 @@ class BCRYPTPasswordManager(PlainTextPasswordManager):
"""
BCRYPT password manager.
In addition to the passwords encoded by this class,
this class can also recognize passwords encoded by :mod:`z3c.bcrypt`
and properly match and check them.
.. note:: This uses the :mod:`bcrypt` library in its
implementation, which `only uses the first 72 characters
<https://pypi.python.org/pypi/bcrypt/3.1.3#maximum-password-length>`_
of the password when computing the hash.
"""

_prefix = b'{BCRYPT}'
# This is the same regex that z3c.bcrypt uses, via way of cryptacular
# The $2a$ is a prefix.
_z3c_bcrypt_syntax = re.compile(br'\$2a\$[0-9]{2}\$[./A-Za-z0-9]{53}')


def _to_bytes(self, password, encoding):
if not isinstance(password, bytes):
Expand Down Expand Up @@ -536,7 +546,10 @@ def checkPassword(self, hashed_password, clear_password):
if not self.match(hashed_password):
return False
pw_bytes = self._clean_clear(clear_password)
pw_hash = hashed_password[len(self._prefix):]
pw_hash = hashed_password
if hashed_password.startswith(self._prefix):
pw_hash = hashed_password[len(self._prefix):]

try:
ok = bcrypt.checkpw(pw_bytes, pw_hash)
except ValueError: # pragma: no cover
Expand Down Expand Up @@ -569,7 +582,8 @@ def match(self, hashed_password):
:rtype: bool
:returns: True iif the password was hashed with this manager.
"""
return hashed_password.startswith(self._prefix)
return (hashed_password.startswith(self._prefix)
or self._z3c_bcrypt_syntax.match(hashed_password) is not None)


# Simple registry
Expand Down
43 changes: 43 additions & 0 deletions src/zope/password/tests/test_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import bcrypt

from zope.component.testing import PlacelessSetup
from zope.interface.verify import verifyObject

from zope.password.interfaces import IMatchingPasswordManager
Expand Down Expand Up @@ -118,6 +119,48 @@ def test_match(self):
self.assertTrue(pw_mgr.match(b'{BCRYPT}'))


class TestZ3cBcryptCompatible(unittest.TestCase):

password = u"right \N{CYRILLIC CAPITAL LETTER A}"
z3c_encoded = b'$2a$10$dzfwtSW1sFx5Q.9/8.3dzOyvIBz6xu4Y00kJWZpOrQ1eH4amFtHP6'


def _make_one(self):
from zope.password.password import BCRYPTPasswordManager
return BCRYPTPasswordManager()

def test_checkPassword(self):
pw_mgr = self._make_one()
self.assertTrue(pw_mgr.checkPassword(self.z3c_encoded, self.password))
# Mess with the hashed password, should not match
encoded = self.z3c_encoded[:-1]
self.assertFalse(pw_mgr.checkPassword(encoded, self.password))

def test_match(self):
pw_mgr = self._make_one()
self.assertTrue(pw_mgr.match(self.z3c_encoded))


class TestConfiguration(PlacelessSetup,
unittest.TestCase):

def setUp(self):
from zope.configuration import xmlconfig
import zope.password
xmlconfig.file('configure.zcml', zope.password)

def test_crypt_utility_names(self):
from zope.password.password import BCRYPTPasswordManager
from zope.password.interfaces import IPasswordManager
from zope import component

self.assertIsInstance(component.getUtility(IPasswordManager, 'BCRYPT'),
BCRYPTPasswordManager)
self.assertIsInstance(component.getUtility(IPasswordManager, 'bcrypt'),
BCRYPTPasswordManager)



def test_suite():
suite = unittest.TestSuite((
doctest.DocTestSuite('zope.password.password'),
Expand Down

0 comments on commit f66cbf9

Please sign in to comment.