[Checkins] SVN: z3c.password/branches/adamg-evenhigher/ even harder password settings
Adam Groszer
agroszer at gmail.com
Sun Jun 21 10:16:13 EDT 2009
Log message for revision 101195:
even harder password settings
Changed:
U z3c.password/branches/adamg-evenhigher/CHANGES.txt
U z3c.password/branches/adamg-evenhigher/src/z3c/password/README.txt
U z3c.password/branches/adamg-evenhigher/src/z3c/password/field.py
U z3c.password/branches/adamg-evenhigher/src/z3c/password/interfaces.py
U z3c.password/branches/adamg-evenhigher/src/z3c/password/password.py
U z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.py
U z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.txt
-=-
Modified: z3c.password/branches/adamg-evenhigher/CHANGES.txt
===================================================================
--- z3c.password/branches/adamg-evenhigher/CHANGES.txt 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/CHANGES.txt 2009-06-21 14:16:12 UTC (rev 101195)
@@ -5,8 +5,18 @@
0.7.0 (unreleased)
------------------
-- ...
+- Feature: Even harder password settings:
+ * ``minLowerLetter``
+ * ``minUpperLetter``
+ * ``minDigits``
+ * ``minSpecials``
+ * ``minOthers``
+ * ``minUniqueCharacters``
+ * ``minUniqueLetters``: count and do not allow less then specified number
+- Feature:
+ * ``disallowPasswordReuse``: do not allow to set a previously used password
+
0.6.0 (2009-06-17)
------------------
Modified: z3c.password/branches/adamg-evenhigher/src/z3c/password/README.txt
===================================================================
--- z3c.password/branches/adamg-evenhigher/src/z3c/password/README.txt 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/src/z3c/password/README.txt 2009-06-21 14:16:12 UTC (rev 101195)
@@ -153,7 +153,181 @@
... _ =pwd.generate()
+Even higher security settings
+-----------------------------
+We can specify how many of a selected character group we want to have in the
+password.
+
+We want to have at least 5 lowercase letters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minLowerLetter = 5
+
+ >>> pwd.verify('FOOBAR123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('foobAR123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('foobaR123')
+
+ >>> pwd.generate()
+ 'Us;iwbzM[J'
+
+ >>> pwd.generate()
+ 'soXVg[V$uw'
+
+
+We want to have at least 5 uppercase letters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minUpperLetter = 5
+
+ >>> pwd.verify('foobar123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('FOOBar123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fOOBAR123')
+
+ >>> pwd.generate()
+ 'OvMPN3Bi'
+
+ >>> pwd.generate()
+ 'l:zB.VA at MH'
+
+
+We want to have at least 5 digits in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minDigits = 5
+
+ >>> pwd.verify('foobar123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('FOOBa1234')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fOBA12345')
+
+ >>> pwd.generate()
+ '(526vK(>Z42v'
+
+ >>> pwd.generate()
+ '3Z&Mtq35Y840'
+
+
+We want to have at least 5 specials in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minSpecials = 5
+
+ >>> pwd.verify('foo(bar)')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('FO.#(Ba1)')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fO.,;()5')
+
+ >>> pwd.generate()
+ '?d{*~2q|P'
+
+ >>> pwd.generate()
+ '(8a5\\(^}vB'
+
+We want to have at least 5 others in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minOthers = 5
+
+ >>> pwd.verify('foobar'+unichr(0x0c3)+unichr(0x0c4))
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('foobar'+unichr(0x0c3)+unichr(0x0c4)+unichr(0x0e1))
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fOO'+unichr(0x0e1)*5)
+
+
+Generating passwords with others not yet supported
+
+ #>>> pwd.generate()
+ #'?d{*~2q|P'
+ #
+ #>>> pwd.generate()
+ #'(8a5\\(^}vB'
+
+We want to have at least 5 different characters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minUniqueCharacters = 5
+
+ >>> pwd.verify('foofoo1212')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueCharacters
+
+ >>> pwd.verify('FOOfoo2323')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueCharacters
+
+ >>> pwd.verify('fOOBAR123')
+
+ >>> pwd.generate()
+ '{l%ix~t8R'
+
+ >>> pwd.generate()
+ 'Us;iwbzM[J'
+
+
+We want to have at least 5 different letters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minUniqueLetters = 5
+
+ >>> pwd.verify('foofoo1212')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueLetters
+
+ >>> pwd.verify('FOOBfoob2323')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueLetters
+
+ >>> pwd.verify('fOOBAR123')
+
+ >>> pwd.generate()
+ '{l%ix~t8R'
+
+ >>> pwd.generate()
+ 'Us;iwbzM[J'
+
+
The Password Field
------------------
@@ -169,6 +343,8 @@
>>> from zope.app.authentication.password import PlainTextPasswordManager
>>> from z3c.password import field
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+
>>> pwdField = field.Password(
... __name__='password',
... title=u'Password',
@@ -181,4 +357,3 @@
Traceback (most recent call last):
...
TooShortPassword
-
Modified: z3c.password/branches/adamg-evenhigher/src/z3c/password/field.py
===================================================================
--- z3c.password/branches/adamg-evenhigher/src/z3c/password/field.py 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/src/z3c/password/field.py 2009-06-21 14:16:12 UTC (rev 101195)
@@ -44,3 +44,12 @@
except AttributeError:
pass
self.checker.verify(value, old)
+
+ #try to check for disallowPasswordReuse here too, to raise
+ #problems ASAP
+ if self.context is not None:
+ try:
+ self.context._checkDisallowedPreviousPassword(value)
+ except AttributeError:
+ #if _checkDisallowedPreviousPassword is missing
+ pass
Modified: z3c.password/branches/adamg-evenhigher/src/z3c/password/interfaces.py
===================================================================
--- z3c.password/branches/adamg-evenhigher/src/z3c/password/interfaces.py 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/src/z3c/password/interfaces.py 2009-06-21 14:16:12 UTC (rev 101195)
@@ -43,6 +43,12 @@
class TooFewGroupCharacters(InvalidPassword):
__doc__ = _('''Password does not contain enough characters of one group.''')
+class TooFewUniqueCharacters(InvalidPassword):
+ __doc__ = _('''Password does not contain enough unique characters.''')
+
+class TooFewUniqueLetters(InvalidPassword):
+ __doc__ = _('''Password does not contain enough unique letters.''')
+
class PasswordExpired(Exception):
__doc__ = _('''The password has expired.''')
@@ -50,6 +56,13 @@
self.principal = principal
Exception.__init__(self, self.__doc__)
+class PreviousPasswordNotAllowed(InvalidPassword):
+ __doc__ = _('''The password set was already used before.''')
+
+ def __init__(self, principal):
+ self.principal = principal
+ Exception.__init__(self, self.__doc__)
+
class TooManyLoginFailures(Exception):
__doc__ = _('''The password was entered incorrectly too often.''')
@@ -120,9 +133,9 @@
@zope.interface.invariant
def minMaxLength(task):
if task.minLength is not None and task.maxLength is not None:
- if task.minLength > task.minLength:
+ if task.minLength > task.maxLength:
raise zope.interface.Invalid(
- u"Minimum length must be greater than the maximum length.")
+ u"Minimum length must not be greater than the maximum length.")
groupMax = zope.schema.Int(
title=_(u'Maximum Characters of Group'),
@@ -166,8 +179,7 @@
required=False,
default=None)
- #WARNING! generating a password with Others is... not always sane
- #think twice before you use it
+ #WARNING! generating a password with Others is not yet supported
minOthers = zope.schema.Int(
title=_(u'Minimum Number of Other characters'),
description=_(u'The minimum amount of other characters that a '
@@ -214,19 +226,48 @@
minl += task.minOthers
- #if task.minLength is not None:
- # if minl > task.minLength:
- # raise zope.interface.Invalid(
- # u"Sum of group minimum lengths must NOT be greater than "
- # u"the minimum length.")
-
if task.maxLength is not None:
if minl > task.maxLength:
raise zope.interface.Invalid(
u"Sum of group minimum lengths must NOT be greater than "
u"the maximum password length.")
+ minUniqueLetters = zope.schema.Int(
+ title=_(u'Minimum Number of Unique letters'),
+ description=_(u'The minimum amount of unique letters that a '
+ u'password must have. This is against passwords '
+ u'like `aAaA0000`. All characters taken lowercase.'),
+ required=False,
+ default=None)
+ @zope.interface.invariant
+ def minUniqueLettersLength(task):
+ if (task.minUniqueLetters is not None
+ and task.minUniqueLetters is not None):
+ if task.minUniqueLetters > task.maxLength:
+ raise zope.interface.Invalid(
+ u"Minimum unique letters number must not be greater than "
+ u"the maximum length.")
+
+ minUniqueCharacters = zope.schema.Int(
+ title=_(u'Minimum Number of Unique characters'),
+ description=_(u'The minimum amount of unique characters that a '
+ u'password must have. This is against passwords '
+ u'like `aAaA0000`. All characters taken lowercase.'),
+ required=False,
+ default=None)
+
+ @zope.interface.invariant
+ def minUniqueCharactersLength(task):
+ if (task.minUniqueCharacters is not None
+ and task.minUniqueCharacters is not None):
+ if task.minUniqueCharacters > task.maxLength:
+ raise zope.interface.Invalid(
+ u"Minimum unique characters length must not be greater than "
+ u"the maximum length.")
+
+
+
class IPasswordOptionsUtility(zope.interface.Interface):
"""Different general security options.
@@ -260,3 +301,9 @@
'password can be provided.'),
required=False,
default=None)
+
+ disallowPasswordReuse = zope.schema.Bool(
+ title=_(u'Disallow Password Reuse'),
+ description=_(u'Do not allow to set a previously set password again.'),
+ required=False,
+ default=False)
Modified: z3c.password/branches/adamg-evenhigher/src/z3c/password/password.py
===================================================================
--- z3c.password/branches/adamg-evenhigher/src/z3c/password/password.py 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/src/z3c/password/password.py 2009-06-21 14:16:12 UTC (rev 101195)
@@ -64,6 +64,10 @@
interfaces.IHighSecurityPasswordUtility['minSpecials'])
minOthers = FieldProperty(
interfaces.IHighSecurityPasswordUtility['minOthers'])
+ minUniqueCharacters = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minUniqueCharacters'])
+ minUniqueLetters = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minUniqueLetters'])
LOWERLETTERS = string.letters[:26]
UPPERLETTERS = string.letters[26:]
@@ -75,12 +79,22 @@
u'for more details.')
def __init__(self, minLength=8, maxLength=12, groupMax=6,
- maxSimilarity=0.6, seed=None):
+ maxSimilarity=0.6, seed=None,
+ minLowerLetter=None, minUpperLetter=None, minDigits=None,
+ minSpecials=None, minOthers=None,
+ minUniqueCharacters=None, minUniqueLetters=None):
self.minLength = minLength
self.maxLength = maxLength
self.groupMax = groupMax
self.maxSimilarity = maxSimilarity
self.random = random.Random(seed or time.time())
+ self.minLowerLetter = minLowerLetter
+ self.minUpperLetter = minUpperLetter
+ self.minDigits = minDigits
+ self.minSpecials = minSpecials
+ self.minOthers = minOthers
+ self.minUniqueCharacters = minUniqueCharacters
+ self.minUniqueLetters = minUniqueLetters
def _checkSimilarity(self, new, ref):
sm = difflib.SequenceMatcher(None, new, ref)
@@ -107,11 +121,16 @@
num_digits = 0
num_specials = 0
num_others = 0
+ uniqueChars = set()
+ uniqueLetters = set()
for char in new:
+ uniqueChars.add(char.lower())
if char in self.LOWERLETTERS:
num_lower_letters += 1
+ uniqueLetters.add(char.lower())
elif char in self.UPPERLETTERS:
num_upper_letters += 1
+ uniqueLetters.add(char.lower())
elif char in self.DIGITS:
num_digits += 1
elif char in self.SPECIALS:
@@ -145,12 +164,16 @@
and num_others < self.minOthers):
raise interfaces.TooFewGroupCharacters()
+ if (self.minUniqueCharacters is not None
+ and len(uniqueChars) < self.minUniqueCharacters):
+ raise interfaces.TooFewUniqueCharacters()
+
+ if (self.minUniqueLetters is not None
+ and len(uniqueLetters) < self.minUniqueLetters):
+ raise interfaces.TooFewUniqueLetters()
+
return
- def _randomOther(self):
- #override if you want an other range
- return unichr(self.random.randint(0x0a1, 0x0ff))
-
def generate(self, ref=None):
'''See interfaces.IHighSecurityPasswordUtility'''
verified = False
@@ -163,27 +186,9 @@
chars = self.LOWERLETTERS + self.UPPERLETTERS + \
self.DIGITS + self.SPECIALS
- if (self.minOthers is not None
- and self.minOthers > 0):
- # unichr(0x0ffff) is a placeholder for Others
- # this is deliberately this way, because a unicode
- # range of 0x0a1...0x010ffff is rather a big string
- chars += unichr(0x0ffff)
-
for count in xrange(length):
new += self.random.choice(chars)
- if (self.minOthers is not None
- and self.minOthers > 0):
- # replace now placeholders with random other characters
- newest = ''
- for c in new:
- if c == unichr(0x0ffff):
- newest += self._randomOther()
- else:
- newest += c
- new = newest
-
# Verify the new password
try:
self.verify(new, ref)
@@ -192,3 +197,28 @@
else:
verified = True
return new
+
+class PasswordOptionsUtility(object):
+ """An implementation of the security options."""
+ zope.interface.implements(interfaces.IPasswordOptionsUtility)
+
+ changePasswordOnNextLogin = FieldProperty(
+ interfaces.IPasswordOptionsUtility['changePasswordOnNextLogin'])
+ passwordExpiresAfter = FieldProperty(
+ interfaces.IPasswordOptionsUtility['passwordExpiresAfter'])
+ lockOutPeriod = FieldProperty(
+ interfaces.IPasswordOptionsUtility['lockOutPeriod'])
+ maxFailedAttempts = FieldProperty(
+ interfaces.IPasswordOptionsUtility['maxFailedAttempts'])
+ disallowPasswordReuse = FieldProperty(
+ interfaces.IPasswordOptionsUtility['disallowPasswordReuse'])
+
+ def __init__(self, changePasswordOnNextLogin=None,
+ passwordExpiresAfter=None,
+ lockOutPeriod=None, maxFailedAttempts=None,
+ disallowPasswordReuse=None):
+ self.changePasswordOnNextLogin = changePasswordOnNextLogin
+ self.passwordExpiresAfter = passwordExpiresAfter
+ self.lockOutPeriod = lockOutPeriod
+ self.maxFailedAttempts = maxFailedAttempts
+ self.disallowPasswordReuse = disallowPasswordReuse
\ No newline at end of file
Modified: z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.py
===================================================================
--- z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.py 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.py 2009-06-21 14:16:12 UTC (rev 101195)
@@ -17,6 +17,7 @@
"""
__docformat__ = "reStructuredText"
import datetime
+import persistent.list
import zope.component
from z3c.password import interfaces
@@ -34,16 +35,40 @@
lastFailedAttempt = None
lockOutPeriod = None
+ disallowPasswordReuse = None
+ previousPasswords = None
+
+ def _checkDisallowedPreviousPassword(self, password):
+ if self._disallowPasswordReuse():
+ if self.previousPasswords is not None:
+ #hack, but this should work with zope.app.authentication and
+ #z3c.authenticator
+ passwordManager = self._getPasswordManager()
+
+ for pwd in self.previousPasswords:
+ if passwordManager.checkPassword(pwd, password):
+ raise interfaces.PreviousPasswordNotAllowed(self)
+
def getPassword(self):
return super(PrincipalMixIn, self).getPassword()
def setPassword(self, password, passwordManagerName=None):
+ self._checkDisallowedPreviousPassword(password)
+
super(PrincipalMixIn, self).setPassword(password, passwordManagerName)
+
+ if self._disallowPasswordReuse():
+ if self.previousPasswords is None:
+ self.previousPasswords = persistent.list.PersistentList()
+
+ self.previousPasswords.append(self.password)
+
self.passwordSetOn = self.now()
self.failedAttempts = 0
self.lastFailedAttempt = None
self.passwordExpired = False
+
password = property(getPassword, setPassword)
def now(self):
@@ -175,4 +200,17 @@
if options.maxFailedAttempts is not None:
return options.maxFailedAttempts
else:
- return self.maxFailedAttempts
\ No newline at end of file
+ return self.maxFailedAttempts
+
+ def _disallowPasswordReuse(self):
+ if self.disallowPasswordReuse is not None:
+ return self.disallowPasswordReuse
+
+ options = self._optionsUtility()
+ if options is None:
+ return self.disallowPasswordReuse
+ else:
+ if options.disallowPasswordReuse is not None:
+ return options.disallowPasswordReuse
+ else:
+ return self.disallowPasswordReuse
\ No newline at end of file
Modified: z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.txt
===================================================================
--- z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.txt 2009-06-21 14:13:15 UTC (rev 101194)
+++ z3c.password/branches/adamg-evenhigher/src/z3c/password/principal.txt 2009-06-21 14:16:12 UTC (rev 101195)
@@ -47,8 +47,17 @@
A time delta object after the user can try again after too many login
failures.
- If ``None`` login will enabled by a correct password.
+- ``disallowPasswordReuse``
+
+ Do not allow setting a password again that was used anytime before.
+ Set to True to enable.
+
+- ``previousPasswords``
+
+ Previous (encoded) password stored when required for
+ ``disallowPasswordReuse``
+
There is the IPasswordOptionsUtility utility, with which you can provide
options for some features.
Strategy is that if the same option/property exists on the principal
@@ -71,7 +80,6 @@
Number of minutes (integer!) after the user can try again after too many login
failures.
- If ``None`` login will enabled by a correct password.
- ``maxFailedAttempts``
@@ -79,7 +87,12 @@
password before the password is locked and no new password can be provided.
(or lockOutPeriod kicks in)
+- ``disallowPasswordReuse``
+ Do not allow setting a password again that was used anytime before.
+ Set to True to enable.
+
+
Let's now create a principal:
>>> from zope.app.authentication import principalfolder
@@ -200,14 +213,7 @@
>>> import zope.interface
>>> import zope.component
>>> from z3c.password import interfaces
- >>> class PasswordOptionsUtility(object):
- ... zope.interface.implements(interfaces.IPasswordOptionsUtility)
- ...
- ... changePasswordOnNextLogin = False
- ... passwordExpiresAfter = None
- ... lockOutPeriod = None
- ... maxFailedAttempts = None
-
+ >>> from z3c.password.password import PasswordOptionsUtility
>>> poptions = PasswordOptionsUtility()
>>> zope.component.provideUtility(poptions)
@@ -590,7 +596,67 @@
1
-Edge case.
+``disallowPasswordReuse``
+-------------------------
+
+Set this option to True to disallow setting a password that was used anytime
+before.
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+
+Set ``disallowPasswordReuse``:
+(passwords will be stored from this point on, so the first '123123' NOT)
+
+ >>> poptions.disallowPasswordReuse = True
+
+ >>> user.setPassword('234234')
+ >>> user.setPassword('345345')
+ >>> user.setPassword('456456')
+ >>> user.setPassword('123123')
+
+Setting a used password again holds an exception:
+
+ >>> user.setPassword('234234')
+ Traceback (most recent call last):
+ ...
+ PreviousPasswordNotAllowed: The password set was already used before.
+
+Something else works:
+
+ >>> user.setPassword('789789')
+
+ >>> poptions.disallowPasswordReuse = False
+
+Same works when option is set on the user:
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+
+Set ``disallowPasswordReuse``:
+(passwords will be stored from this point on, so the first '123123' NOT)
+
+ >>> user.disallowPasswordReuse = True
+
+ >>> user.setPassword('234234')
+ >>> user.setPassword('345345')
+ >>> user.setPassword('456456')
+ >>> user.setPassword('123123')
+
+Setting a used password again holds an exception:
+
+ >>> user.setPassword('234234')
+ Traceback (most recent call last):
+ ...
+ PreviousPasswordNotAllowed: The password set was already used before.
+
+Something else works:
+
+ >>> user.setPassword('789789')
+
+
+
+Edge cases
+----------
+
When there is no ``maxFailedAttempts`` set, we can bang on with a bad password
forever.
@@ -606,6 +672,32 @@
... print 'bug'
+
+``failedAttempts`` needs to be reset on a successful check:
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+ >>> user.checkPassword('456456')
+ False
+
+``failedAttempts`` gets set on a bad check:
+
+ >>> user.failedAttempts
+ 1
+ >>> user.lastFailedAttempt
+ datetime.datetime(2009, 6, 14, 17, 15)
+
+ >>> user.checkPassword('123123')
+ True
+
+Gets reset on a successful check:
+
+ >>> user.failedAttempts
+ 0
+ >>> user.lastFailedAttempt is None
+ True
+
+
+
Coverage happiness
------------------
More information about the Checkins
mailing list