├── .gitignore ├── lib ├── passphrase_entropy │ └── version.rb └── passphrase_entropy.rb ├── Rakefile ├── passphrase_entropy.gemspec ├── README.md └── test └── passphrase_entropy_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /lib/passphrase_entropy/version.rb: -------------------------------------------------------------------------------- 1 | class PassphraseEntropy 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | task :default => :test 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.test_files = FileList['test/*_test.rb'] 8 | t.ruby_opts << "-w" 9 | t.verbose = true 10 | end 11 | -------------------------------------------------------------------------------- /passphrase_entropy.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib/", __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require "passphrase_entropy/version" 5 | 6 | spec = Gem::Specification.new do |s| 7 | s.name = "passphrase_entropy" 8 | s.version = PassphraseEntropy::VERSION 9 | s.author = "Paul Battley" 10 | s.email = "pbattley@gmail.com" 11 | s.summary = "Estimate the entropy of a passphrase" 12 | s.files = Dir["{lib,test}/**/*.rb"] 13 | s.require_path = "lib" 14 | s.test_files = Dir["test/*_test.rb"] 15 | s.has_rdoc = true 16 | s.homepage = "https://github.com/alphagov/passphrase_entropy" 17 | end 18 | -------------------------------------------------------------------------------- /lib/passphrase_entropy.rb: -------------------------------------------------------------------------------- 1 | require "zlib" 2 | 3 | # Estimate the entropy of a passphrase. This is calculated as the number of bytes 4 | # required to encode the passphrase on top of a Deflate stream of a preset 5 | # dictionary. 6 | # 7 | class PassphraseEntropy 8 | 9 | # Instantiate a new PasswordEntropy calculator. 10 | # dictionary should be a String containing a list of words; this is 11 | # /usr/share/dict/words by default, which should be good for English systems. 12 | # 13 | def initialize(dictionary=default_dictionary) 14 | @dictionary = dictionary 15 | end 16 | 17 | # Estimate the entropy of s (in bytes) 18 | # 19 | def entropy(s) 20 | zlen(s) - baseline 21 | end 22 | 23 | def inspect(*args) 24 | to_s 25 | end 26 | 27 | private 28 | def default_dictionary 29 | File.read("/usr/share/dict/words") 30 | end 31 | 32 | def zlen(s) 33 | z = Zlib::Deflate.new 34 | out = z.deflate(@dictionary + s, Zlib::FINISH) 35 | z.close 36 | out.bytesize 37 | end 38 | 39 | def baseline 40 | @baseline ||= zlen("") 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PassphraseEntropy 2 | ================= 3 | 4 | Estimate the entropy of a passphrase. This is calculated as the number of bytes 5 | required to encode the passphrase on top of a Deflate stream of a preset 6 | dictionary. 7 | 8 | Inspired by [xkcd 936](http://xkcd.com/936/). 9 | 10 | Usage 11 | ----- 12 | 13 | require 'passphrase_entropy' 14 | 15 | pe = PassphraseEntropy.new 16 | # or customise the dictionary: 17 | pe = PassphraseEntropy.new(File.read('/usr/share/dict/words')) 18 | 19 | pe.entropy('password') # => 6 20 | pe.entropy('correct horse battery staple') # => 24 21 | pe.entropy('Tr0ub4dor&3') # => 21 22 | 23 | You can decide on your acceptable level of complexity. 24 | 25 | Notes 26 | ----- 27 | 28 | It's a bit slow: the dictionary must be deflated every time. This could be 29 | ameliorated by saving the state, but that would require a modified zlib 30 | library. (It's easy to do with a pure Ruby zlib library, but Ruby is so much 31 | slower in this case that the overall gain in speed is almost zero.) 32 | 33 | Tested using the `web2` dictionary installed in Ubuntu Linux by: 34 | 35 | apt-get install dictionaries-common miscfiles 36 | 37 | Results will vary depending on the dictionary used. 38 | -------------------------------------------------------------------------------- /test/passphrase_entropy_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require "minitest/autorun" 4 | require "passphrase_entropy" 5 | 6 | class PassphraseEntropyTest < MiniTest::Unit::TestCase 7 | 8 | def setup 9 | @passphrase_entropy = PassphraseEntropy.new 10 | end 11 | 12 | def assert_better(better, worse) 13 | eb = @passphrase_entropy.entropy(better) 14 | ew = @passphrase_entropy.entropy(worse) 15 | assert(eb > ew, "Expected #{better} (#{eb}) to be better than #{worse} (#{ew})") 16 | end 17 | 18 | def test_adding_punctuation_should_improve_entropy 19 | assert_better "rubbish!", "rubbish" 20 | end 21 | 22 | def test_random_letters_should_be_better_than_words 23 | assert_better "sdfjhweu", "password" 24 | end 25 | 26 | def test_capital_letters_and_numbers_should_improve_entropy 27 | assert_better "Password1", "password" 28 | end 29 | 30 | def test_complex_passwords_should_be_better_than_simple_ones 31 | assert_better "Slightly^better 1", "Password1" 32 | end 33 | 34 | def test_should_agree_with_xkcd_936 35 | assert_better "correct horse battery staple", "Tr0ub4dor&3" 36 | end 37 | 38 | def test_mixed_symbols_should_be_better_than_words 39 | assert_better "~T3n Char$", "antidisestablishmentarianism" 40 | end 41 | end 42 | --------------------------------------------------------------------------------