Blocking the weakest passwords
The recent Gawker passwords leak once again highlights the widespread use of passwords that offer essentially no security.
Some years ago, when working on a secure web app for a large organisation — let’s call them Secret Testing Ltd — I was keen that people shouldn’t choose hopelessly weak passwords. I was particularly concerned by my sysadmin colleague’s fondness for passwords of the form ‘p/\55w0rd’ or ‘S3cr3t-T35t|ng’.
I therefore wrote some simple Ruby code to try to catch very weak passwords:
class PasswordUtils def self.look_and_sound_alikes(original) look_or_sound_alikes = { \ '0' => ['o'] , '1' => ['i', 'l'], '2' => ['to', 'too'], '3' => ['e'], '4' => ['a', 'for'], '5' => ['s'], '6' => ['g'], '8' => ['b', 'ate'], '9' => ['g'], 'i' => ['i'], '$' => ['s'], '|' => ['i', 'l', ''], '!' => ['i', 'l', ''], '@' => ['g', 'a'], '(' => ['c'], '[' => ['c', 'e'], '{' => ['e'], '&' => ['and'], '*' => ['star'], ')' => ['d'], '^' => ['a'], '/' => ['a', 'v'], '\\' => [''], '<' => ['k'] } # the penultimate two substitutions catch /\ (A) and \/ (V), albeit without # distinguishing them, and the last (in conjunction with the empty options # for ! and |) catches !< and |< (K) versions = [''] original.downcase.each_char do |c| versions.collect! do |v| alikes = look_or_sound_alikes[c] || [c] alikes.collect { |c1| v + c1 } end versions.flatten! end versions end def self.obvious?(password, custom_banned_list = '') standard_banned_list = File.read("#{RAILS_ROOT}/config/banned_password_words.txt") banned_words = standard_banned_list.downcase.split(/\W+/) + custom_banned_list.downcase.split(/\W+/).delete_if { |w| w.length < 4 } alike_words = [password.downcase] + PasswordUtils.look_and_sound_alikes(password) banned_words.any? do |banned_word| alike_words.any? { |alike_word| alike_word.include? banned_word } end end end |
The first method of the class undoes some common substitutions:
> PasswordUtils.look_and_sound_alikes('p/\55w0rd') => ["password", "pvssword"] > PasswordUtils.look_and_sound_alikes('S3cr3t-T35t|ng') => ["secret-testing", "secret-testlng", "secret-testng"] |
And the second method uses this in conjunction with a list of banned words to check that an entered password isn’t hopelessly weak.
> PasswordUtils.obvious?('p/\55w0rd') => true > PasswordUtils.obvious?('S3cr3t-T35t|ng') => true > PasswordUtils.obvious?('gkAsd76!o') => false |
Obviously, you need a good banned words list — perhaps starting with some top N passwords lists, and including the name of your company, the web app, and words related to the area of business, local pubs, and so on.
It’s also a good idea to include custom banned words for individual users. For example:
custom_banned_list = "#{first_names} #{surname} #{department} #{organisation} #{email}" PasswordUtils.password_obvious?(password, custom_banned_list) |
Passing these tests isn’t a sufficient condition for ruling out a weak password, but it’s arguably a necessary one, and a good start.