Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding test cases for password strength algorithm
- Loading branch information
Showing
17 changed files
with
784 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,7 @@ Or install it yourself as: | |
|
||
## Usage | ||
|
||
Usage instructions | ||
|
||
|
||
## Contributing | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.