Created
February 27, 2018 17:26
-
-
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)
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
#!/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