module Milia # Provides support generating memorable passwords class Password private # This flag is used in conjunction with Password.phonemic and states that a # password must include a digit. ONE_DIGIT = 1 # This flag is used in conjunction with Password.phonemic and states that a # password must include a capital letter. ONE_CASE = 1 << 1 # phoneme flags CONSONANT = 1 VOWEL = 1 << 1 DIPHTHONG = 1 << 2 NOT_FIRST = 1 << 3 # indicates that a given phoneme may not occur first PHONEMES = { :a => VOWEL, :ae => VOWEL | DIPHTHONG, :ah => VOWEL | DIPHTHONG, :ai => VOWEL | DIPHTHONG, :b => CONSONANT, :c => CONSONANT, :ch => CONSONANT | DIPHTHONG, :d => CONSONANT, :e => VOWEL, :ee => VOWEL | DIPHTHONG, :ei => VOWEL | DIPHTHONG, :f => CONSONANT, :g => CONSONANT, :gh => CONSONANT | DIPHTHONG | NOT_FIRST, :h => CONSONANT, :i => VOWEL, :ie => VOWEL | DIPHTHONG, :j => CONSONANT, :k => CONSONANT, :l => CONSONANT, :m => CONSONANT, :n => CONSONANT, :ng => CONSONANT | DIPHTHONG | NOT_FIRST, :o => VOWEL, :oh => VOWEL | DIPHTHONG, :oo => VOWEL | DIPHTHONG, :p => CONSONANT, :ph => CONSONANT | DIPHTHONG, :qu => CONSONANT | DIPHTHONG, :r => CONSONANT, :s => CONSONANT, :sh => CONSONANT | DIPHTHONG, :t => CONSONANT, :th => CONSONANT | DIPHTHONG, :u => VOWEL, :v => CONSONANT, :w => CONSONANT, :x => CONSONANT, :y => CONSONANT, :z => CONSONANT } class << self # Determine whether the next character should be a vowel or consonant. def get_vowel_or_consonant rand( 2 ) == 1 ? VOWEL : CONSONANT end # Generate a memorable password of +length+ characters, using phonemes that # a human-being can easily remember. +flags+ is one or more of # Password::ONE_DIGIT and Password::ONE_CASE, logically # OR'ed together. For example: # # password = Password.generate(8, Password::ONE_DIGIT | Password::ONE_CASE) # # This would generate an eight character password, containing a digit and an # upper-case letter, such as *Ug2shoth*. # # This method was inspired by the pwgen[http://sourceforge.net/projects/pwgen] # tool, written by Theodore Ts'o. # # Generated passwords may contain any of the characters in # Password::PASSWD_CHARS. def generate(length = 8, flags = nil) password = nil ph_flags = flags loop do password = '' # Separate the flags integer into an array of individual flags feature_flags = [flags & ONE_DIGIT, flags & ONE_CASE] prev = [] first = true desired = Password.get_vowel_or_consonant # Get an Array of all of the phonemes phonemes = PHONEMES.keys.map {|ph| ph.to_s} nr_phonemes = phonemes.size while password.length < length do # Get a random phoneme and its length phoneme = phonemes[rand(nr_phonemes)] ph_len = phoneme.length # Get its flags as an Array ph_flags = PHONEMES[phoneme.to_sym] ph_flags = [ph_flags & CONSONANT, ph_flags & VOWEL, ph_flags & DIPHTHONG, ph_flags & NOT_FIRST] # Filter on the basic type of the next phoneme next if ph_flags.include?(desired) # Handle the NOT_FIRST flag next if first && ph_flags.include?(NOT_FIRST) # Don't allow a VOWEL followed a vowel/diphthong pair next if prev.include?(VOWEL) && ph_flags.include?(VOWEL) && ph_flags.include?(DIPHTHONG) # Don't allow us to go longer than the desired length next if ph_len > length - password.length # We've found a phoneme that meets our criteria password << phoneme # Handle ONE_CASE if feature_flags.include?(ONE_CASE) if (first || ph_flags.include?(CONSONANT)) && rand(10) < 3 password[-ph_len, 1] = password[-ph_len, 1].upcase feature_flags.delete(ONE_CASE) end end # Is password already long enough? break if password.length >= length # Handle ONE_DIGIT if feature_flags.include?(ONE_DIGIT) if !first && rand(10) < 3 password << (rand(10) + '0'.ord).chr feature_flags.delete(ONE_DIGIT) first = true prev = [] desired = Password.get_vowel_or_consonant next end end if desired == CONSONANT desired = VOWEL elsif prev.include?(VOWEL) || ph_flags.include?(DIPHTHONG) || rand(10) > 3 desired = CONSONANT else desired = VOWEL end prev = ph_flags first = false end # Try again break unless feature_flags.include?(ONE_CASE) || feature_flags.include?(ONE_DIGIT) end password end end end # class end # module