reset repository with new author information, license and version

This commit is contained in:
John Nishinaga 2011-03-22 09:11:41 -04:00
commit 85a0a65ccb
19 changed files with 679 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.gem
.bundle
.yardoc
Gemfile.lock
doc
pkg

2
.rspec Normal file
View File

@ -0,0 +1,2 @@
--colour
--format documentation

3
.yardopts Normal file
View File

@ -0,0 +1,3 @@
--markup markdown
lib/**/*.rb
README.md MIT-LICENSE.txt

8
CHANGELOG.md Normal file
View File

@ -0,0 +1,8 @@
## Version 0.1.0 / 2011-03-22
* updated license
* bumped version to 0.1.0
## Version 0.0.1 / 2011-02-19
* initial release

3
Gemfile Normal file
View File

@ -0,0 +1,3 @@
source 'http://rubygems.org'
gemspec

19
MIT-LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2011 Pat Deegan, PhD & Associates, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

58
README.md Normal file
View File

@ -0,0 +1,58 @@
# keepass-password-generator
Generate passwords using KeePass password generator patterns.
## RubyGems installation
gem install keepass-password-generator
## Bundler installation
In your `Gemfile`:
gem 'keepass-password-generator'
Install bundled gems:
bundle
## Usage
require 'keepass/password'
KeePass::Password.generate('A{6}s')
#=> "Un2hd#t"
See <http://keepass.info/help/base/pwgenerator.html> for information about KeePass patterns.
## Examples
A 40-bit WEP key:
KeePass::Password.generate('h{10}')
#=> "ae6929dc0e"
A random MAC address:
KeePass::Password.generate('HH\-HH\-HH\-HH\-HH\-HH', :permute => false)
#=> "0D-4D-32-64-EB-7D"
A password with 10 alphanumeric characters, where at least 2 are upper case and at least are 2 lower case characters:
KeePass::Password.generate('uullA{6}')
#=> "us2j1nTIQT"
A password with 20 alphanumeric and symbol characters, without any lookalike characters (e.g., I and |):
KeePass::Password.generate('[As]{20}', :remove_lookalikes => true)
#=> "-2~[+Rze{hZezk(\\nZ-W"
Invalid patterns raise an exception:
KeePass::Password.generate('[\I\|]{3}', :remove_lookalikes => true)
#=> KeePass::Password::InvalidPatternError: empty character set for token 1 for "[\\I\\|]{3}"
## Related gems
* <https://github.com/dmke/simple-password-gen>
* <http://rubygems.org/gems/ruby-password>

12
Rakefile Normal file
View File

@ -0,0 +1,12 @@
require 'bundler'
Bundler.setup
Bundler::GemHelper.install_tasks
require 'yard'
YARD::Rake::YardocTask.new
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec

View File

@ -0,0 +1,28 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "keepass/password/version"
Gem::Specification.new do |s|
s.name = "keepass-password-generator"
s.version = KeePass::Password::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["John Nishinaga"]
s.email = ["jingoro@casa-z.org"]
s.homepage = "https://github.com/patdeegan/keepass-password-generator"
s.summary = "keepass-password-generator-#{KeePass::Password::VERSION}"
s.description = "Generate passwords using KeePass password generator patterns"
s.rubyforge_project = "keepass-password-generator"
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
s.add_dependency 'activesupport', '>= 2.2.0'
s.add_development_dependency 'yard'
s.add_development_dependency 'bluecloth'
s.add_development_dependency 'rspec'
end

View File

@ -0,0 +1 @@
require 'keepass/password'

58
lib/keepass/password.rb Normal file
View File

@ -0,0 +1,58 @@
require 'keepass/password/char_set'
require 'keepass/password/generator'
require 'keepass/password/version'
module KeePass
module Password
# 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

View File

@ -0,0 +1,91 @@
require 'set'
module KeePass
module Password
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
end
end

View File

@ -0,0 +1,111 @@
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

View File

@ -0,0 +1,5 @@
module KeePass
module Password
VERSION = "0.1.0"
end
end

38
lib/keepass/random.rb Normal file
View File

@ -0,0 +1,38 @@
require 'active_support/secure_random'
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)
ActiveSupport::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
end

38
spec/char_set_spec.rb Normal file
View File

@ -0,0 +1,38 @@
require 'spec_helper'
describe KeePass::Password::CharSet do
describe "#add_from_strings" do
it "should add from multiple arguments" do
subject.add_from_strings('abc', 'cde', 'QQ')
subject.should == Set.new(%w(a b c d e Q))
end
end
describe "#add_from_char_set_id" do
it "should add the digits" do
subject.add_from_char_set_id('d')
subject.should == Set.new('0'..'9')
end
it "should support chaining" do
subject.add_from_char_set_id('l').add_from_char_set_id('u')
subject.should == (Set.new('a'..'z') + Set.new('A'..'Z'))
end
it "should allow x with default mapping" do
subject.add_from_char_set_id('x')
subject.should include(0x7f.chr)
end
it "should raise an error with ASCII mapping" do
subject.mapping = KeePass::Password::CharSet::ASCII_MAPPING
expect { subject.add_from_char_set_id('x') }.to raise_error(KeePass::Password::InvalidCharSetIDError)
end
end
end

117
spec/generator_spec.rb Normal file
View File

@ -0,0 +1,117 @@
require 'spec_helper'
describe KeePass::Password::Generator do
def char_set(*ids)
char_set = KeePass::Password::CharSet.new
ids.each { |id| char_set.add_from_char_set_id(id) }
char_set
end
subject { described_class.new(pattern, options) }
let(:options) { { } }
let(:random_class) { KeePass::Random }
describe "pattern 'h{10}' (40-bit WEP key)" do
let(:pattern) { 'h{10}' }
its(:pattern) { should == 'h{10}' }
its(:permute) { should be_true }
its(:char_sets) { should have(10).items }
it "should generate 10 hex digits " do
random_class.should_receive(:sample_array) do |array|
array.sort.should == char_set('h').to_a.sort
'0'
end.exactly(10).times
random_class.should_receive(:shuffle_array) do |array|
array.sort
end.once
subject.generate.should == '0000000000'
end
end
describe "pattern 'HH\-HH\-HH\-HH\-HH\-HH', :permute => false" do
let(:pattern) { 'HH\-HH\-HH\-HH\-HH\-HH' }
let(:options) { { :permute => false } }
its(:pattern) { should == 'HH\-HH\-HH\-HH\-HH\-HH' }
its(:permute) { should be_false }
its(:char_sets) { should have(17).items }
it "should generate a MAC address" do
random_class.should_receive(:sample_array) do |array|
if array == ['-']
'-'
else
array.sort.should == char_set('H').to_a.sort
'0'
end
end.exactly(17).times
random_class.should_not_receive(:shuffle_array)
subject.generate.should == '00-00-00-00-00-00'
end
end
describe "pattern 'uullA{6}'" do
let(:pattern) { 'uullA{6}' }
its(:pattern) { should == 'uullA{6}' }
its(:permute) { should be_true }
its(:char_sets) { should have(10).items }
it "should generate a 10-character alphanumeric password" do
random_class.should_receive(:sample_array) do |array|
array.sort.first
end.exactly(10).times
random_class.should_receive(:shuffle_array) do |array|
array.sort
end.once
subject.generate.should == '000000AAaa'
end
end
describe "pattern '[As]{20}', :remove_lookalikes => true" do
let(:pattern) { '[As]{20}' }
let(:options) { { :remove_lookalikes => true } }
its(:pattern) { should == '[As]{20}' }
its(:permute) { should be_true }
its(:char_sets) { should have(20).items }
it "should generate a 20-character password" do
test_set = (char_set('A', 's') - Set.new(%w(O 0 l 1 I |))).to_a.sort
i = 0
random_class.should_receive(:sample_array) do |array|
array.sort.should == test_set
result = array.sort[i]
i += 1
result
end.exactly(20).times
random_class.should_receive(:shuffle_array) do |array|
array.sort
end.once
subject.generate.should == '!"#$%&\'()*+,-./23456'
end
end
describe "pattern '[\\I\\|]{3}', :remove_lookalikes => true" do
let(:pattern) { '[\I\|]{3}' }
let(:options) { { :remove_lookalikes => true } }
it "should raise an error" do
expect { subject }.to raise_error(KeePass::Password::InvalidPatternError)
end
end
end

49
spec/random_spec.rb Normal file
View File

@ -0,0 +1,49 @@
require 'spec_helper'
describe KeePass::Random do
describe "#random_number" do
it "should use ActiveSupport::SecureRandom" do
ActiveSupport::SecureRandom.should_receive(:random_number).once.with(12)
described_class.random_number(12)
end
it "should accept default argument" do
ActiveSupport::SecureRandom.should_receive(:random_number).with(0)
described_class.random_number
end
end
describe "#sample_array" do
it "should call random_number with the array size" do
described_class.should_receive(:random_number).with(6).and_return(3)
described_class.sample_array(%w(a b c d e f)).should == 'd'
end
it "should return expected values for deterministic random number" do
described_class.stub(:random_number) { |arg| 0 }
described_class.sample_array(%w(a b c)).should == 'a'
described_class.sample_array(%w(b a c)).should == 'b'
described_class.sample_array(%w(c b a)).should == 'c'
end
end
describe "#shuffle_array" do
it "should call random_number with no parameters" do
described_class.should_receive(:random_number).with().at_least(5).times.and_return(0.5)
described_class.shuffle_array(%w(a b c d e))
end
it "should return the same elements" do
described_class.stub(:random_number) { 0.5 }
described_class.shuffle_array(%w(a b c d e)).sort.should == %w(a b c d e)
end
end
end

32
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,32 @@
$:.unshift File.expand_path('../../lib', __FILE__)
require 'rspec'
require 'keepass/password/generator'
# module DeterministicRandomness
#
# def deterministic_random_number(&block)
# ActiveSupport::SecureRandom.stub(:random_number) do |arg|
# if block
# block.call(arg)
# else
# 0
# end
# end
# end
#
# # def deterministic_shuffle
# # Array.any_instance.stub(:shuffle!)
# # end
#
# end
#
# RSpec.configure do |config|
# config.include DeterministicRandomness
# end
# RSpec::Matchers.define :have_char_set_length_of do |expected|
# match do |actual|
# actual.char_sets.size == expected
# end
# end