I would strongly advise not doing it with a single regex, but rather (say) with an array of checking functions that you can add, enable or disable at leisure.
Consider that password checking is a user-driven operation, so it will always last many more milliseconds that whatever check you might employ; premature optimization in this stage is largely pointless (unless you're designing a password cracker, but then the architecture is completely different).
Something like:
import re
rules = {
'be between 8 and 10 chars' : (lambda pw: 8 <= len(pw) <= 10 ),
'start with a letter' : (lambda pw: re.match(r"^[A-Z]", pw)),
'contain a number' : (lambda pw: re.match(r"\\d", pw)),
'contain EXACTLY ONE number': (lambda pw: re.match(r"^\\D*\\d\\D*$", pw)),
...
}
fails = [ rule for rule, check in rules.items() if not check(pw) ]
Now if fails is not empty, you know the password fails and you can give a feedback to the user: you can directly use fails and build the string like "It must" + {fails but the last, comma separated} + ( (", " if Oxford) + "and" if fails > 1 ) + {last item of fails}.
The password is not acceptable.
It must be between 8 and 10 chars and contain a number
For the "not contain repeated sequences of size X or more", you add one extra function:
'cannot contain the same 3-letter sequence twice'
: (lambda pw: len( [pw[i:i+3] for i in range(len(pw)-2)] )
== len(set([pw[i:i+3] for i in range(len(pw)-2)]))),
'cannot contain the same 2-letter sequence twice'
: (lambda pw: len( [pw[i:i+2] for i in range(len(pw)-1)] )
== len(set([pw[i:i+2] for i in range(len(pw)-1)]))),
Note that, strictly speaking, "banana" contains the sequence "an" (and "na") twice, and also the "ana" sequence twice: bANAna and banANA.
Granted that it is less efficient, yet this approach has, I think, several advantages:
- is is easily modularized
- can be configured without re-calculating a complex regex
- test cases can be provided with the same syntax as the rules (each rule could have a dict with
{ "mustpass": [ "pw1", "pw2", ... ], "mustfail": [ "pw5", "pw6"... ] }, which can be automatized with ease
- modifying one test does not alter the pass/fail state of other tests
- allows detailing the user which rule he broke
- the setup can be changed to fail early (only the first error is reported) for efficiency's sake. In this case, faster or often failed tests would go first.