Skip to content

Commit

Permalink
Adding test cases for password strength algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
bdmac committed Mar 11, 2013
1 parent 67cff3e commit ac57862
Show file tree
Hide file tree
Showing 17 changed files with 784 additions and 15 deletions.
12 changes: 11 additions & 1 deletion Gemfile
@@ -1,4 +1,14 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in strong_password.gemspec
gemspec

version = ENV["RAILS_VERSION"] || "3.2"

rails = case version
when "master"
{github: "rails/rails"}
else
"~> #{version}.0"
end

gem "rails", rails
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -25,7 +25,7 @@ Or install it yourself as:

## Usage

Usage instructions


## Contributing

Expand Down
15 changes: 12 additions & 3 deletions lib/strong_password.rb
@@ -1,5 +1,14 @@
require "strong_password/version"
require 'active_model/validations'

require 'strong_password/version'
require 'strong_password/nist_bonus_bits'
require 'strong_password/entropy_calculator'
require 'strong_password/strength_checker'
require 'strong_password/password_variants'
require 'strong_password/dictionary_adjuster'
require 'strong_password/qwerty_adjuster'
require 'strong_password/validators/strength_validator' if defined?(ActiveModel)
require 'strong_password/railtie' if defined?(Rails)

module StrongPassword
# Your code goes here...
end
end
114 changes: 114 additions & 0 deletions lib/strong_password/dictionary_adjuster.rb
@@ -0,0 +1,114 @@
module StrongPassword
class DictionaryAdjuster
COMMON_PASSWORDS = ["123456","password","12345678","1234","pussy","12345","dragon","qwerty",
"696969","mustang","letmein","baseball","master","michael","football","shadow","monkey","abc123",
"pass","6969","jordan","harley","ranger","iwantu","jennifer","hunter","2000","test","batman",
"trustno1","thomas","tigger","robert","access","love","buster","1234567","soccer","hockey","killer",
"george","sexy","andrew","charlie","superman","asshole","dallas","jessica","panties","pepper",
"1111","austin","william","daniel","golfer","summer","heather","hammer","yankees","joshua","maggie",
"biteme","enter","ashley","thunder","cowboy","silver","richard","orange","merlin","michelle",
"corvette","bigdog","cheese","matthew","121212","patrick","martin","freedom","ginger","blowjob",
"nicole","sparky","yellow","camaro","secret","dick","falcon","taylor","111111","131313","123123",
"bitch","hello","scooter","please","","porsche","guitar","chelsea","black","diamond","nascar",
"jackson","cameron","654321","computer","amanda","wizard","xxxxxxxx","money","phoenix","mickey",
"bailey","knight","iceman","tigers","purple","andrea","horny","dakota","aaaaaa","player","sunshine",
"morgan","starwars","boomer","cowboys","edward","charles","girls","booboo","coffee","xxxxxx",
"bulldog","ncc1701","rabbit","peanut","john","johnny","gandalf","spanky","winter","brandy","compaq",
"carlos","tennis","james","mike","brandon","fender","anthony","blowme","ferrari","cookie","chicken",
"maverick","chicago","joseph","diablo","sexsex","hardcore","666666","willie","welcome","chris",
"panther","yamaha","justin","banana","driver","marine","angels","fishing","david","maddog","hooters",
"wilson","butthead","dennis","captain","bigdick","chester","smokey","xavier","steven","viking",
"snoopy","blue","eagles","winner","samantha","house","miller","flower","jack","firebird","butter",
"united","turtle","steelers","tiffany","zxcvbn","tomcat","golf","bond007","bear","tiger","doctor",
"gateway","gators","angel","junior","thx1138","porno","badboy","debbie","spider","melissa","booger",
"1212","flyers","fish","porn","matrix","teens","scooby","jason","walter","cumshot","boston","braves",
"yankee","lover","barney","victor","tucker","princess","mercedes","5150","doggie","zzzzzz","gunner",
"horney","bubba","2112","fred","johnson","xxxxx","tits","member","boobs","donald","bigdaddy","bronco",
"penis","voyager","rangers","birdie","trouble","white","topgun","bigtits","bitches","green","super",
"qazwsx","magic","lakers","rachel","slayer","scott","2222","asdf","video","london","7777","marlboro",
"srinivas","internet","action","carter","jasper","monster","teresa","jeremy","11111111","bill","crystal",
"peter","pussies","cock","beer","rocket","theman","oliver","prince","beach","amateur","7777777","muffin",
"redsox","star","testing","shannon","murphy","frank","hannah","dave","eagle1","11111","mother","nathan",
"raiders","steve","forever","angela","viper","ou812","jake","lovers","suckit","gregory","buddy",
"whatever","young","nicholas","lucky","helpme","jackie","monica","midnight","college","baby","brian",
"mark","startrek","sierra","leather","232323","4444","beavis","bigcock","happy","sophie","ladies",
"naughty","giants","booty","blonde","golden","0","fire","sandra","pookie","packers","einstein",
"dolphins","0","chevy","winston","warrior","sammy","slut","8675309","zxcvbnm","nipples","power",
"victoria","asdfgh","vagina","toyota","travis","hotdog","paris","rock","xxxx","extreme","redskins",
"erotic","dirty","ford","freddy","arsenal","access14","wolf","nipple","iloveyou","alex","florida",
"eric","legend","movie","success","rosebud","jaguar","great","cool","cooper","1313","scorpio",
"mountain","madison","987654","brazil","lauren","japan","naked","squirt","stars","apple","alexis",
"aaaa","bonnie","peaches","jasmine","kevin","matt","qwertyui","danielle","beaver","4321","4128",
"runner","swimming","dolphin","gordon","casper","stupid","shit","saturn","gemini","apples","august",
"3333","canada","blazer","cumming","hunting","kitty","rainbow","112233","arthur","cream","calvin",
"shaved","surfer","samson","kelly","paul","mine","king","racing","5555","eagle","hentai","newyork",
"little","redwings","smith","sticky","cocacola","animal","broncos","private","skippy","marvin",
"blondes","enjoy","girl","apollo","parker","qwert","time","sydney","women","voodoo","magnum",
"juice","abgrtyu","777777","dreams","maxwell","music","rush2112","russia","scorpion","rebecca",
"tester","mistress","phantom","billy","6666","albert"]

attr_reader :base_password

def initialize(password)
@base_password = password.dup.downcase
end

def is_strong?(entropy_threshhold: 18, minwordlen: 4, extra_words: [])
adjusted_entropy(entropy_threshhold: entropy_threshhold,
minwordlen: minwordlen,
extra_words: extra_words) >= entropy_threshhold
end

def is_weak?(entropy_threshhold: 18)
!is_strong?(entropy_threshhold: entropy_threshhold)
end

# Returns the minimum entropy for the passwords dictionary adjustments.
# If a threshhold is specified we will bail early to avoid unnecessary
# processing.
def adjusted_entropy(minwordlen: 4, extra_words: [], entropy_threshhold: 0)
dictionary_words = COMMON_PASSWORDS + extra_words
min_entropy = Float::INFINITY
# Process the passwords, while looking for possible matching words in the dictionary.
PasswordVariants.all_variants(base_password).each_with_index do |variant, num|
y = variant.length
x = -1
while x < y
x = x + 1
if ((variant[x] =~ /\w/) != nil)
next_non_word = variant.index(/\s/, x)
x2 = next_non_word ? next_non_word : variant.length + 1
found = false
while !found && (x2 - x >= minwordlen)
word = variant[x, minwordlen]
word += variant[(x + minwordlen)..x2].reverse.chars.inject('') {|memo, c| "(#{Regexp.quote(c)}#{memo})?"} if (x + minwordlen) <= y
results = dictionary_words.grep(/\b#{word}\b/)
if results.empty?
variant[x] = '*'
x = x + 1
numbits = EntropyCalculator.calculate(variant[0, x])
found = true if numbits >= entropy_threshhold
else
results.each do |match|
break unless match.present?
# Substitute *s for matched portion of word and calculate entropy
stripped_variant = variant.tr(match.strip.sub('-', '\\-'), '*')
numbits = EntropyCalculator.calculate(stripped_variant)
min_entropy = [min_entropy, numbits].min
return min_entropy if min_entropy < entropy_threshhold
end

found = true
end
end

break if found

x = x2 - 1
end
end
end
return min_entropy
end
end
end
76 changes: 76 additions & 0 deletions lib/strong_password/entropy_calculator.rb
@@ -0,0 +1,76 @@
module StrongPassword
module EntropyCalculator
# Calculates NIST entropy for a password.
def self.calculate(password, repeats_weakened = true)
if repeats_weakened
bits_with_repeats_weakened(password)
else
bits(password)
end
end

# The basic NIST entropy calculation is based solely
# on the length of the password in question.
def self.bits(password)
length = password.length
bits = if length > 20
4 + (7 * 2) + (12 * 1.5) + length - 20
elsif length > 8
4 + (7 * 2) + ((length - 8) * 1.5)
elsif length > 1
4 + ((length - 1) * 2)
else
(length == 1 ? 4 : 0)
end
bits + NistBonusBits.bonus_bits(password)
end

# A modified version of the basic entropy calculation
# which lowers the amount of entropy gained for each
# repeated character in the password
def self.bits_with_repeats_weakened(password)
resolver = EntropyResolver.new
bits = password.chars.each.with_index.inject(0) do |result, (char, index)|
char_value = resolver.entropy_for(char)
result += bit_value_at_position(index, char_value)
end
bits + NistBonusBits.bonus_bits(password)
end

private

def self.bit_value_at_position(position, base = 1)
if position > 19
return base
elsif position > 7
return base * 1.5
elsif position > 0
return base * 2
else
return 4
end
end

class EntropyResolver
BASE_VALUE = 1
REPEAT_WEAKENING_FACTOR = 0.75

attr_reader :char_multiplier

def initialize
@char_multiplier = {}
end

# Returns the current entropy value for a character and weakens the entropy
# for future calls for the same character.
def entropy_for(char)
ordinal_value = char.ord
char_multiplier[ordinal_value] ||= BASE_VALUE
char_value = char_multiplier[ordinal_value]
# Weaken the value of this character for future occurrances
char_multiplier[ordinal_value] = char_multiplier[ordinal_value] * REPEAT_WEAKENING_FACTOR
return char_value
end
end
end
end
45 changes: 45 additions & 0 deletions lib/strong_password/nist_bonus_bits.rb
@@ -0,0 +1,45 @@
module StrongPassword
module NistBonusBits
@@bonus_bits_for_password = {}

# NIST password strength rules allow up to 6 bonus bits for mixed case and non-alphabetic
def self.bonus_bits(password)
@@bonus_bits_for_password[password] ||= begin
calculate_bonus_bits_for(password)
end
end

# This smells bad as it's only used for testing...
def self.reset_bonus_cache!
@@bonus_bits_for_password = {}
end

def self.calculate_bonus_bits_for(password)
upper = !!(password =~ /[[:upper:]]/)
lower = !!(password =~ /[[:lower:]]/)
numeric = !!(password =~ /[[:digit:]]/)
other = !!(password =~ /[^a-zA-Z0-9 ]/)
space = !!(password =~ / /)

# I had this condensed to nested ternaries but that shit was ugly
bonus_bits = if upper && lower && other && numeric
6
elsif upper && lower && other && !numeric
5
elsif numeric && other && !upper && !lower
-2
elsif numeric && !other && !upper && !lower
-6
else
0
end

if !space
bonus_bits = bonus_bits - 2
elsif password.split(/\s+/).length > 3
bonus_bits = bonus_bits + 1
end
bonus_bits
end
end
end

0 comments on commit ac57862

Please sign in to comment.