keepass-password-generator/lib/keepass/password/generator.rb

112 lines
3.7 KiB
Ruby

require 'keepass/password/char_set'
require 'keepass/random'
module KeePass
module Password
class InvalidPatternError < RuntimeError; end
# Generate passwords using KeePass password generator patterns.
#
# @see http://keepass.info/help/base/pwgenerator.html
class Generator
# Available character sets
CHARSET_IDS = CharSet::DEFAULT_MAPPING.keys.join
# ASCII printables regular expression
LITERALS_RE = /[\x20-\x7e]/
CHAR_TOKEN_RE = Regexp.new("([#{CHARSET_IDS}])|\\\\(#{LITERALS_RE.source})")
GROUP_TOKEN_RE = Regexp.new("(#{CHAR_TOKEN_RE.source}|" +
"\\[((#{CHAR_TOKEN_RE.source})*?)\\])" +
"(\\{(\\d+)\\})?")
VALIDATOR_RE = Regexp.new("\\A(#{GROUP_TOKEN_RE.source})+\\Z")
LOOKALIKE = "O0l1I|"
LOOKALIKE_CHARSET = CharSet.new.add_from_strings LOOKALIKE
# @return [String] the pattern
attr_reader :pattern
# @return [Array<CharSet>] the character sets from the pattern
attr_reader :char_sets
# @return [Boolean] whether or not to permute the password
attr_accessor :permute
# Instantiates a new PasswordGenerator object.
#
# @param [String] pattern the pattern
# @param [Hash] options the options
# @option options [Boolean] :permute (true) whether or not to randomly permute generated passwords
# @option options [Boolean] :remove_lookalikes (false) whether or not to remove lookalike characters
# @option options [Hash] :charset_mapping (CharSet::DEFAULT_MAPPING) the KeePass character set ID mapping
# @return [PasswordGenerator] self
# @raise [InvalidPatternError] if `pattern` is invalid
def initialize(pattern, options = {})
@permute = options.has_key?(:permute) ? options[:permute] : true
@pattern = pattern
@char_sets = pattern_to_char_sets(pattern, options)
end
# Returns a new password.
#
# @return [String] a new password
def generate
result = char_sets.map { |c| Random.sample_array(c.to_a) }
result = Random.shuffle_array(result) if permute
result.join
end
private
def pattern_to_char_sets(pattern, options) #:nodoc:
remove_lookalikes = options[:remove_lookalikes] || false
mapping = options[:charset_mapping] || CharSet::DEFAULT_MAPPING
char_sets = []
i = 1
pattern.scan(GROUP_TOKEN_RE) do |x1, char, bs_char, char_group, x5, x6, x7, x8, repeat|
char_set = CharSet.new
char_set.mapping = mapping
begin
if char
char_set.add_from_char_set_id(char)
elsif bs_char
char_set.add(bs_char)
else
char_group.scan(CHAR_TOKEN_RE) do |c, e|
if c
char_set.add_from_char_set_id(c)
else
char_set.add(e)
end
end
end
rescue InvalidCharSetIDError => e
raise InvalidPatternError, e.message
end
char_set -= LOOKALIKE_CHARSET if remove_lookalikes
if char_set.empty?
raise InvalidPatternError, "empty character set for token #{i} for #{pattern.inspect}"
end
(repeat ? repeat.to_i : 1).times { char_sets << char_set }
i += 1
end
if char_sets.any?
char_sets
else
raise InvalidPatternError, "no char sets from #{pattern.inspect}"
end
end
# private
end
end
end