Skip to content

Commit

Permalink
Add a crypt password manager.
Browse files Browse the repository at this point in the history
  • Loading branch information
Martijn Pieters committed Feb 20, 2011
1 parent 23df21e commit 86d125e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGES.txt
Expand Up @@ -12,6 +12,9 @@ CHANGES
- Use {SHA} as the prefix for SHA1-encoded passwords to be compatible with
RFC 2307, but support matching against {SHA1} for backwards compatibility.

- Add a crypt password manager to fully support all methods named in RFC 2307.
It is contained in the 'legacy' module however, to flag crypt's status.

3.6.1 (2010-05-27)
------------------

Expand Down
12 changes: 12 additions & 0 deletions src/zope/password/configure.zcml
Expand Up @@ -27,6 +27,14 @@
factory=".password.SSHAPasswordManager"
/>

<configure zcml:condition="installed crypt">
<utility
name="Crypt"
provides=".interfaces.IPasswordManager"
factory=".legacy.CryptPasswordManager"
/>
</configure>

<utility
component=".vocabulary.PasswordManagerNamesVocabulary"
provides="zope.schema.interfaces.IVocabularyFactory"
Expand All @@ -51,6 +59,10 @@
<allow interface=".interfaces.IPasswordManager" />
</class>

<class class=".password.SSHAPasswordManager">
<allow interface=".interfaces.IPasswordManager" />
</class>

</configure>

</configure>
114 changes: 114 additions & 0 deletions src/zope/password/legacy.py
@@ -0,0 +1,114 @@
##############################################################################
#
# Copyright (c) 2009 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Legacy password managers, using now-outdated, insecure methods for hashing
"""
__docformat__ = 'restructuredtext'

from codecs import getencoder

try:
from crypt import crypt
from random import choice
except ImportError:
# The crypt module is not universally available, apparently
crypt = None

from zope.interface import implements
from zope.password.interfaces import IPasswordManager

_encoder = getencoder("utf-8")


if crypt is not None:
class CryptPasswordManager(object):
"""Crypt password manager.
Implements a UNIX crypt(3) hashing scheme. Note that crypt is
considered far inferior to more modern schemes such as SSHA hashing,
and only uses the first 8 characters of a password.
>>> from zope.interface.verify import verifyObject
>>> manager = CryptPasswordManager()
>>> verifyObject(IPasswordManager, manager)
True
>>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
>>> encoded = manager.encodePassword(password, salt="..")
>>> encoded
'{CRYPT}..I1I8wps4Na2'
>>> manager.match(encoded)
True
>>> manager.checkPassword(encoded, password)
True
Unfortunately, crypt only looks at the first 8 characters, so matching
against an 8 character password plus suffix always matches. Our test
password (including utf-8 encoding) is exactly 8 characters long, and
thus affixing 'wrong' to it tests as a correct password::
>>> manager.checkPassword(encoded, password + u"wrong")
True
Using a completely different password is rejected as expected::
>>> manager.checkPassword(encoded, 'completely wrong')
False
Using the `openssl passwd` command-line utility to encode ``secret``,
we get ``erz50QD3gv4Dw`` as seeded hash.
Our password manager generates the same value when seeded with the
same salt, so we can be sure, our output is compatible with
standard LDAP tools that also use crypt::
>>> salt = 'er'
>>> password = 'secret'
>>> encoded = manager.encodePassword(password, salt)
>>> encoded
'{CRYPT}erz50QD3gv4Dw'
>>> manager.checkPassword(encoded, password)
True
>>> manager.checkPassword(encoded, password + u"wrong")
False
>>> manager.encodePassword(password) != manager.encodePassword(password)
True
The manager only claims to implement CRYPT encodings, anything not
starting with the string {CRYPT} returns False::
>>> manager.match('{MD5}someotherhash')
False
"""

implements(IPasswordManager)

def encodePassword(self, password, salt=None):
if salt is None:
choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789./")
salt = choice(choices) + choice(choices)
return '{CRYPT}%s' % crypt(_encoder(password)[0], salt)

def checkPassword(self, encoded_password, password):
return encoded_password == self.encodePassword(password,
encoded_password[7:9])

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

21 changes: 21 additions & 0 deletions src/zope/password/testing.py
Expand Up @@ -25,6 +25,11 @@
from zope.password.password import SSHAPasswordManager
from zope.password.vocabulary import PasswordManagerNamesVocabulary

try:
from zope.password.legacy import CryptPasswordManager
except ImportError:
CryptPasswordManager = None


def setUpPasswordManagers():
"""Helper function for setting up password manager utilities for tests
Expand All @@ -41,6 +46,16 @@ def setUpPasswordManagers():
>>> getUtility(IPasswordManager, 'SHA1')
<zope.password.password.SHA1PasswordManager object at 0x...>
>>> try:
... import crypt
... except ImportError:
... CryptPasswordManager = None
... True
... else:
... from zope.password.legacy import CryptPasswordManager
... getUtility(IPasswordManager, 'Crypt') is CryptPasswordManager
True
>>> voc = getUtility(IVocabularyFactory, 'Password Manager Names')
>>> voc = voc(None)
>>> voc
Expand All @@ -53,12 +68,18 @@ def setUpPasswordManagers():
True
>>> 'MD5' in voc
True
>>> CryptPasswordManager is None or 'Crypt' in voc
True
"""
provideUtility(PlainTextPasswordManager(), IPasswordManager, 'Plain Text')
provideUtility(SSHAPasswordManager(), IPasswordManager, 'SSHA')
provideUtility(MD5PasswordManager(), IPasswordManager, 'MD5')
provideUtility(SHA1PasswordManager(), IPasswordManager, 'SHA1')

if CryptPasswordManager is not None:
provideUtility(CryptPasswordManager, IPasswordManager, 'Crypt')

provideUtility(PasswordManagerNamesVocabulary,
IVocabularyFactory, 'Password Manager Names')
1 change: 1 addition & 0 deletions src/zope/password/tests/test_password.py
Expand Up @@ -19,6 +19,7 @@
def test_suite():
return unittest.TestSuite((
doctest.DocTestSuite('zope.password.password'),
doctest.DocTestSuite('zope.password.legacy'),
doctest.DocTestSuite(
'zope.password.testing',
optionflags=doctest.ELLIPSIS),
Expand Down

0 comments on commit 86d125e

Please sign in to comment.