Skip to content

Instantly share code, notes, and snippets.

@itspriddle
Created February 27, 2018 17:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itspriddle/471773852440c4896505a7e05616c857 to your computer and use it in GitHub Desktop.
Save itspriddle/471773852440c4896505a7e05616c857 to your computer and use it in GitHub Desktop.
LaunchBar Action to generate a password (based on https://github.com/johnbintz/keepass-password-generator)
#!/usr/bin/env ruby
# Generates a random password using keepass-password-generator
# Imported from https://github.com/johnbintz/keepass-password-generator
require 'securerandom'
require 'set'
module KeePass
module Random
# If `n` is a positive integer, then returns a random
# integer `r` such that 0 <= `r` < `n`.
#
# If `n` is 0 or unspecified, then returns a random
# float `r` such that 0 <= `r` < 1.
#
# @param [Integer] n the upper bound
# @return [Integer|Float] the random number
# @see ActiveSupport::SecureRandom#random_number
def self.random_number(n = 0)
SecureRandom.random_number(n)
end
# Returns a randomly sampled item from the array.
#
# @param [Array] array the array to sample from
# @return [Object] random item or nil if no items exist
def self.sample_array(array)
array[random_number(array.size)]
end
# Returns the array shuffled randomly.
#
# @param [Array] array the array to shuffle
# @return [Array] the shuffled array
def self.shuffle_array(array)
array.sort_by { random_number }
end
end
module Password
VERSION = "0.1.0"
class InvalidCharSetIDError < RuntimeError; end
# Character sets for the KeePass password generator.
#
# @see http://keepass.info/help/base/pwgenerator.html#pattern
class CharSet < Set
UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
LOWERCASE = "abcdefghijklmnopqrstuvwxyz"
DIGITS = "0123456789"
UPPER_CONSONANTS = "BCDFGHJKLMNPQRSTVWXYZ"
LOWER_CONSONANTS = "bcdfghjklmnpqrstvwxyz"
UPPER_VOWELS = "AEIOU"
LOWER_VOWELS = "aeiou"
PUNCTUATION = ",.;:"
BRACKETS = "[]{}()<>"
PRINTABLE_ASCII_SPECIAL = "!\"#\$%&'()*+,-./:;<=>?[\\]^_{|}~"
UPPER_HEX = "0123456789ABCDEF"
LOWER_HEX = "0123456789abcdef"
HIGH_ANSI = (0x7f..0xfe).map { |i| i.chr }.join
DEFAULT_MAPPING = {
'a' => [LOWERCASE, DIGITS],
'A' => [LOWERCASE, UPPERCASE, DIGITS],
'U' => [UPPERCASE, DIGITS],
'c' => [LOWER_CONSONANTS],
'C' => [LOWER_CONSONANTS, UPPER_CONSONANTS],
'z' => [UPPER_CONSONANTS],
'd' => [DIGITS],
'h' => [LOWER_HEX],
'H' => [UPPER_HEX],
'l' => [LOWERCASE],
'L' => [LOWERCASE, UPPERCASE],
'u' => [UPPERCASE],
'p' => [PUNCTUATION],
'b' => [BRACKETS],
's' => [PRINTABLE_ASCII_SPECIAL],
'S' => [UPPERCASE, LOWERCASE, DIGITS, PRINTABLE_ASCII_SPECIAL],
'v' => [LOWER_VOWELS],
'V' => [LOWER_VOWELS, UPPER_VOWELS],
'Z' => [UPPER_VOWELS],
'x' => [HIGH_ANSI],
}
ASCII_MAPPING = DEFAULT_MAPPING.reject { |k, v| k == 'x' }
# @return [Hash] the KeePass character set ID mapping
attr_accessor :mapping
# Instantiates a new CharSet object.
#
# @see Set#new
def initialize(*args)
@mapping = DEFAULT_MAPPING
super
end
# Adds several characters according to the KeePass character class.
#
# @see http://keepass.info/help/base/pwgenerator.html#pattern
# @param [String] char_set_id the KeePass character set ID
# @raise [InvalidCharSetIDError] if mapping does not contain `char_set_id`
# @return [CharSet] self
def add_from_char_set_id(char_set_id)
if strings = mapping[char_set_id]
add_from_strings *strings
else
raise InvalidCharSetIDError, "no such char set ID #{char_set_id.inspect}"
end
end
# Adds each character from one or more strings.
#
# @param [Array] *strings one or more strings to add
# @return [CharSet] self
def add_from_strings(*strings)
strings.each { |s| merge Set.new(s.split('')) }
self
end
end
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
# Returns a generated password.
#
# @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 [String] the new password
# @raise [InvalidPatternError] if `pattern` is invalid
def self.generate(pattern, options = {})
Generator.new(pattern, options).generate
end
# Returns whether or not the pattern is valid.
#
# @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 [Boolean] whether or not the pattern is valid
def self.validate_pattern(pattern, options = {})
begin
generate(pattern, options)
true
rescue InvalidPatternError
false
end
end
# Returns an entropy estimate of a password.
#
# @param [String] test the password to test
# @see http://en.wikipedia.org/wiki/Password_strength
def self.estimate_entropy(test)
chars = 0
chars += 26 if test =~ LOWERCASE_TEST_RE
chars += 26 if test =~ UPPERCASE_TEST_RE
chars += 10 if test =~ DIGITS_TEST_RE
chars += CharSet::PRINTABLE_ASCII_SPECIAL.size if test =~ SPECIAL_TEST_RE
if chars == 0
0
else
(test.size * Math.log(chars) / Math.log(2)).to_i
end
end
end
end
password = KeePass::Password.generate("L{9}d{9}s{2}")
IO.popen("pbcopy", "w") { |f| f << password.chomp }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment