George MacKerron: code blog

GIS, software development, and other snippets

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.

Share

Written by George

December 14th, 2010 at 1:24 pm