password can't contain sequence of three or more characters with re module

Viewed 741

I am creating a regular expression which should match strings that are

  • 8 or 10 characters long
  • first character should be only letter
  • 1 digit, 1 special character, No whitespace
  • sequence of characters cannot be twice, like mymymy

I could capture almost all but I have some confusion. It does not work when I put (?={8,10}) at the end. It only works when I mention character length at the beginning. Why is it? but at the same time I want to start password with letter only, so I used ^ so do character length at the start affect ^(?=[a-zA-Z]) this group? How can I get/capture a sequence of three or more characters twice/more and ignore it in my password?

r"(?={8,10})^(?=[a-zA-Z])(?=.*\d)(\S+).*$"
1 Answers

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.
Related