summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.document4
-rw-r--r--.rspec1
-rw-r--r--Gemfile.lock35
-rw-r--r--LICENSE.txt15
-rwxr-xr-xbin/trocla121
-rw-r--r--lib/VERSION4
-rw-r--r--lib/trocla.rb94
-rw-r--r--lib/trocla/default_config.yaml8
-rw-r--r--lib/trocla/formats.rb40
-rw-r--r--lib/trocla/formats/bcrypt.rb6
-rw-r--r--lib/trocla/formats/md5crypt.rb6
-rw-r--r--lib/trocla/formats/mysql.rb6
-rw-r--r--lib/trocla/formats/pgsql.rb7
-rw-r--r--lib/trocla/formats/plain.rb7
-rw-r--r--lib/trocla/formats/sha1.rb7
-rw-r--r--lib/trocla/formats/sha256crypt.rb6
-rw-r--r--lib/trocla/formats/sha512crypt.rb6
-rw-r--r--lib/trocla/formats/ssha.rb9
-rw-r--r--lib/trocla/formats/x509.rb128
-rw-r--r--lib/trocla/util.rb43
-rw-r--r--lib/trocla/version.rb22
-rw-r--r--spec/data/.keep0
-rw-r--r--spec/trocla/util_spec.rb28
-rw-r--r--spec/trocla_spec.rb128
-rw-r--r--trocla.gemspec89
25 files changed, 820 insertions, 0 deletions
diff --git a/.document b/.document
new file mode 100644
index 0000000..c98d021
--- /dev/null
+++ b/.document
@@ -0,0 +1,4 @@
+lib/**/*.rb
+bin/*
+-
+LICENSE.txt
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..4e1e0d2
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--color
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..43bc6bb
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,35 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ bcrypt (3.1.7)
+ diff-lcs (1.1.3)
+ git (1.2.5)
+ highline (1.6.2)
+ jeweler (1.6.4)
+ bundler (~> 1.0)
+ git (>= 1.2.5)
+ rake
+ mocha (0.9.12)
+ moneta (0.7.20)
+ rake (0.9.2)
+ rdoc (3.8)
+ rspec (2.4.0)
+ rspec-core (~> 2.4.0)
+ rspec-expectations (~> 2.4.0)
+ rspec-mocks (~> 2.4.0)
+ rspec-core (2.4.0)
+ rspec-expectations (2.4.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.4.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bcrypt
+ highline
+ jeweler
+ mocha
+ moneta (~> 0.7)
+ rdoc (~> 3.8)
+ rspec (~> 2.4)
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..5b0754c
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,15 @@
+Trocla - a simple password generator and storage
+Copyright (C) 2011 Marcel Haerry
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/bin/trocla b/bin/trocla
new file mode 100755
index 0000000..6949318
--- /dev/null
+++ b/bin/trocla
@@ -0,0 +1,121 @@
+#!/usr/bin/env ruby
+# CLI client for Trocla.
+#
+require 'rubygems'
+require 'trocla'
+require 'optparse'
+require 'yaml'
+
+options = { :config_file => nil, :ask_password => true }
+
+OptionParser.new do |opts|
+ opts.on("--version", "-V", "Version information") do
+ puts Trocla::VERSION::STRING
+ exit
+ end
+
+ opts.on("--config CONFIG", "-c", "Configuration file") do |v|
+ if File.exist?(v)
+ options[:config_file] = v
+ else
+ STDERR.puts "Cannot find config file: #{v}"
+ exit 1
+ end
+ end
+
+ opts.on("--no-random") do
+ options['random'] = false
+ end
+
+ opts.on("--length LENGTH") do |v|
+ options['length'] = v.to_i
+ end
+
+ opts.on("--password [PASSWORD]", "-p", "Provide password at command line") do |pass|
+ options[:ask_password] = false
+ options[:password] = pass
+ end
+
+end.parse!
+
+def create(options)
+ Trocla.new(options.delete(:config_file)).password(
+ options.delete(:trocla_key),
+ options.delete(:trocla_format),
+ options.merge(YAML.load(options.delete(:other_options).shift.to_s)||{})
+ )
+end
+
+def get(options)
+ Trocla.new(options.delete(:config_file)).get_password(
+ options.delete(:trocla_key),
+ options.delete(:trocla_format)
+ )
+end
+def set(options)
+ if options.delete(:ask_password)
+ require 'highline/import'
+ password = ask("Enter your password: ") { |q| q.echo = "x" }.to_s
+ pwd2 = ask("Repeat password: ") { |q| q.echo = "x" }.to_s
+ unless password == pwd2
+ STDERR.puts "Passwords did not match, exiting!"
+ exit 1
+ end
+ else
+ password = options.delete(:password) || STDIN.read.chomp
+ end
+ format = options.delete(:trocla_format)
+ trocla = Trocla.new(options.delete(:config_file))
+ trocla.set_password(
+ options.delete(:trocla_key),
+ format,
+ trocla.formats(format).format(password, options.delete(:other_options).shift.to_s)
+ )
+ ""
+end
+
+def reset(options)
+ Trocla.new(options.delete(:config_file)).reset_password(
+ options.delete(:trocla_key),
+ options.delete(:trocla_format),
+ options.merge(YAML.load(options.delete(:other_options).shift.to_s)||{})
+ )
+end
+
+def delete(options)
+ Trocla.new(options.delete(:config_file)).delete_password(
+ options.delete(:trocla_key),
+ options.delete(:trocla_format)
+ )
+end
+
+def check_format(format_name)
+ if format_name.nil?
+ STDERR.puts "Missing format, exiting..."
+ exit 1
+ elsif !Trocla::Formats.available?(format_name)
+ STDERR.puts "Error: The format #{format_name} is not available"
+ exit 1
+ end
+end
+
+actions=['create','get','set','reset','delete']
+
+if !(ARGV.length < 2) && (action=ARGV.shift) && actions.include?(action)
+ options[:trocla_key] = ARGV.shift
+ options[:trocla_format] = ARGV.shift
+ options[:other_options] = ARGV
+ check_format(options[:trocla_format]) unless action == 'delete'
+ begin
+ if result = send(action,options)
+ puts result.is_a?(String) ? result : result.inspect
+ end
+ rescue Exception => e
+ STDERR.puts "Action failed with the following message: #{e.message}" unless e.message == 'exit'
+ exit 1
+ end
+else
+ STDERR.puts "Please supply one of the following actions: #{actions.join(', ')}"
+ exit 1
+end
+
diff --git a/lib/VERSION b/lib/VERSION
new file mode 100644
index 0000000..158fcec
--- /dev/null
+++ b/lib/VERSION
@@ -0,0 +1,4 @@
+major:0
+minor:0
+patch:11
+build:
diff --git a/lib/trocla.rb b/lib/trocla.rb
new file mode 100644
index 0000000..8d916b2
--- /dev/null
+++ b/lib/trocla.rb
@@ -0,0 +1,94 @@
+require 'trocla/version'
+require 'trocla/util'
+require 'trocla/formats'
+
+class Trocla
+
+ def initialize(config_file=nil)
+ if config_file
+ @config_file = File.expand_path(config_file)
+ elsif File.exists?(def_config_file=File.expand_path('~/.troclarc.yaml')) || File.exists?(def_config_file=File.expand_path('/etc/troclarc.yaml'))
+ @config_file = def_config_file
+ end
+ end
+
+ def password(key,format,options={})
+ options = config['options'].merge(options)
+ raise "Format #{format} is not supported! Supported formats: #{Trocla::Formats.all.join(', ')}" unless Trocla::Formats::available?(format)
+
+ unless (password=get_password(key,format)).nil?
+ return password
+ end
+
+ plain_pwd = get_password(key,'plain')
+ if options['random'] && plain_pwd.nil?
+ plain_pwd = Trocla::Util.random_str(options['length'].to_i,options['charset'])
+ set_password(key,'plain',plain_pwd) unless format == 'plain'
+ elsif !options['random'] && plain_pwd.nil?
+ raise "Password must be present as plaintext if you don't want a random password"
+ end
+ set_password(key,format,self.formats(format).format(plain_pwd,options))
+ end
+
+ def get_password(key,format)
+ cache.fetch(key,{})[format]
+ end
+
+ def reset_password(key,format,options={})
+ set_password(key,format,nil)
+ password(key,format,options)
+ end
+
+ def delete_password(key,format=nil)
+ if format.nil?
+ cache.delete(key)
+ else
+ old_val = (h = cache.fetch(key,{})).delete(format)
+ h.empty? ? cache.delete(key) : cache[key] = h
+ old_val
+ end
+ end
+
+ def set_password(key,format,password)
+ if (format == 'plain')
+ h = (cache[key] = { 'plain' => password })
+ else
+ h = (cache[key] = cache.fetch(key,{}).merge({ format => password }))
+ end
+ h[format]
+ end
+
+ def formats(format)
+ (@format_cache||={})[format] ||= Trocla::Formats[format].new(self)
+ end
+
+ private
+ def cache
+ @cache ||= build_cache
+ end
+
+ def build_cache
+ require 'moneta'
+ lconfig = config
+ Moneta.new(lconfig['adapter'], lconfig['adapter_options']||{})
+ end
+
+ def config
+ @config ||= read_config
+ end
+
+ def read_config
+ if @config_file.nil?
+ default_config
+ else
+ raise "Configfile #{@config_file} does not exist!" unless File.exists?(@config_file)
+ default_config.merge(YAML.load(File.read(@config_file)))
+ end
+ end
+
+ def default_config
+ require 'yaml'
+ YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__),'trocla','default_config.yaml'))))
+ end
+
+end
diff --git a/lib/trocla/default_config.yaml b/lib/trocla/default_config.yaml
new file mode 100644
index 0000000..d4037fd
--- /dev/null
+++ b/lib/trocla/default_config.yaml
@@ -0,0 +1,8 @@
+---
+options:
+ random: true
+ length: 12
+ charset: default
+adapter: :YAML
+adapter_options:
+ :file: '/tmp/trocla.yaml'
diff --git a/lib/trocla/formats.rb b/lib/trocla/formats.rb
new file mode 100644
index 0000000..0103c4e
--- /dev/null
+++ b/lib/trocla/formats.rb
@@ -0,0 +1,40 @@
+class Trocla::Formats
+
+ class Base
+ attr_reader :trocla
+ def initialize(trocla)
+ @trocla = trocla
+ end
+ end
+
+ class << self
+ def [](format)
+ formats[format.downcase]
+ end
+
+ def all
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'formats','*.rb'))].collect{|f| File.basename(f,'.rb').downcase }
+ end
+
+ def available?(format)
+ all.include?(format.downcase)
+ end
+
+ private
+ def formats
+ @@formats ||= Hash.new do |hash, format|
+ format = format.downcase
+ if File.exists?(path(format))
+ require "trocla/formats/#{format}"
+ hash[format] = (eval "Trocla::Formats::#{format.capitalize}")
+ else
+ raise "Format #{format} is not supported!"
+ end
+ end
+ end
+
+ def path(format)
+ File.expand_path(File.join(File.dirname(__FILE__),'formats',"#{format}.rb"))
+ end
+ end
+end
diff --git a/lib/trocla/formats/bcrypt.rb b/lib/trocla/formats/bcrypt.rb
new file mode 100644
index 0000000..4b6fb33
--- /dev/null
+++ b/lib/trocla/formats/bcrypt.rb
@@ -0,0 +1,6 @@
+class Trocla::Formats::Bcrypt < Trocla::Formats::Base
+ require 'bcrypt'
+ def format(plain_password,options={})
+ BCrypt::Password.create(plain_password).to_s
+ end
+end
diff --git a/lib/trocla/formats/md5crypt.rb b/lib/trocla/formats/md5crypt.rb
new file mode 100644
index 0000000..80d2f09
--- /dev/null
+++ b/lib/trocla/formats/md5crypt.rb
@@ -0,0 +1,6 @@
+# salted crypt
+class Trocla::Formats::Md5crypt < Trocla::Formats::Base
+ def format(plain_password,options={})
+ plain_password.crypt('$1$' << Trocla::Util.salt << '$')
+ end
+end
diff --git a/lib/trocla/formats/mysql.rb b/lib/trocla/formats/mysql.rb
new file mode 100644
index 0000000..a097f95
--- /dev/null
+++ b/lib/trocla/formats/mysql.rb
@@ -0,0 +1,6 @@
+class Trocla::Formats::Mysql < Trocla::Formats::Base
+ require 'digest/sha1'
+ def format(plain_password,options={})
+ "*" + Digest::SHA1.hexdigest(Digest::SHA1.digest(plain_password)).upcase
+ end
+end
diff --git a/lib/trocla/formats/pgsql.rb b/lib/trocla/formats/pgsql.rb
new file mode 100644
index 0000000..ef4fed3
--- /dev/null
+++ b/lib/trocla/formats/pgsql.rb
@@ -0,0 +1,7 @@
+class Trocla::Formats::Pgsql < Trocla::Formats::Base
+ require 'digest/md5'
+ def format(plain_password,options={})
+ raise "You need pass the username as an option to use this format" unless options['username']
+ "md5" + Digest::MD5.hexdigest(plain_password + options['username'])
+ end
+end
diff --git a/lib/trocla/formats/plain.rb b/lib/trocla/formats/plain.rb
new file mode 100644
index 0000000..79502e0
--- /dev/null
+++ b/lib/trocla/formats/plain.rb
@@ -0,0 +1,7 @@
+class Trocla::Formats::Plain < Trocla::Formats::Base
+
+ def format(plain_password,options={})
+ plain_password
+ end
+
+end
diff --git a/lib/trocla/formats/sha1.rb b/lib/trocla/formats/sha1.rb
new file mode 100644
index 0000000..1321b35
--- /dev/null
+++ b/lib/trocla/formats/sha1.rb
@@ -0,0 +1,7 @@
+class Trocla::Formats::Sha1 < Trocla::Formats::Base
+ require 'digest/sha1'
+ require 'base64'
+ def format(plain_password,options={})
+ '{SHA}' + Base64.encode64(Digest::SHA1.digest(plain_password))
+ end
+end
diff --git a/lib/trocla/formats/sha256crypt.rb b/lib/trocla/formats/sha256crypt.rb
new file mode 100644
index 0000000..e34c149
--- /dev/null
+++ b/lib/trocla/formats/sha256crypt.rb
@@ -0,0 +1,6 @@
+# salted crypt
+class Trocla::Formats::Sha256crypt < Trocla::Formats::Base
+ def format(plain_password,options={})
+ plain_password.crypt('$5$' << Trocla::Util.salt << '$')
+ end
+end
diff --git a/lib/trocla/formats/sha512crypt.rb b/lib/trocla/formats/sha512crypt.rb
new file mode 100644
index 0000000..47eb11e
--- /dev/null
+++ b/lib/trocla/formats/sha512crypt.rb
@@ -0,0 +1,6 @@
+# salted crypt
+class Trocla::Formats::Sha512crypt < Trocla::Formats::Base
+ def format(plain_password,options={})
+ plain_password.crypt('$6$' << Trocla::Util.salt << '$')
+ end
+end
diff --git a/lib/trocla/formats/ssha.rb b/lib/trocla/formats/ssha.rb
new file mode 100644
index 0000000..a2e0d02
--- /dev/null
+++ b/lib/trocla/formats/ssha.rb
@@ -0,0 +1,9 @@
+# salted crypt
+require 'base64'
+require 'digest'
+class Trocla::Formats::Ssha < Trocla::Formats::Base
+ def format(plain_password,options={})
+ salt = options['salt'] || Trocla::Util.salt(16)
+ "{SSHA}"+Base64.encode64("#{Digest::SHA1.digest("#{plain_password}#{salt}")}#{salt}").chomp
+ end
+end
diff --git a/lib/trocla/formats/x509.rb b/lib/trocla/formats/x509.rb
new file mode 100644
index 0000000..219cd38
--- /dev/null
+++ b/lib/trocla/formats/x509.rb
@@ -0,0 +1,128 @@
+class Trocla::Formats::X509 < Trocla::Formats::Base
+ require 'openssl'
+ def format(plain_password,options={})
+
+ if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
+ # just an import, don't generate any new keys
+ return plain_password
+ end
+
+ if options['subject']
+ subject = options['subject']
+ elsif options['CN']
+ subject = ''
+ ['C','ST','L','O','OU','CN','emailAddress'].each do |field|
+ subject << "/#{field}=#{options[field]}" if options[field]
+ end
+ else
+ raise "You need to pass \"subject\" or \"CN\" as an option to use this format"
+ end
+ sign_with = options['ca'] || nil
+ keysize = options['keysize'] || 2048
+ serial = options['serial'] || 1
+ days = options['days'] || 365
+ altnames = options['altnames'] || nil
+ altnames.collect { |v| "DNS:#{v}" }.join(', ') if altnames
+
+ begin
+ key = mkkey(keysize)
+ rescue Exception => e
+ raise "Private key for #{subject} creation failed: #{e.message}"
+ end
+
+ if sign_with # certificate signed with CA
+ begin
+ ca = OpenSSL::X509::Certificate.new(getca(sign_with))
+ cakey = OpenSSL::PKey::RSA.new(getca(sign_with))
+ caserial = getserial(sign_with, serial)
+ rescue Exception => e
+ raise "Value of #{sign_with} can't be loaded as CA: #{e.message}"
+ end
+
+ begin
+ subj = OpenSSL::X509::Name.parse(subject)
+ request = mkreq(subj, key.public_key)
+ request.sign(key, OpenSSL::Digest::SHA1.new)
+ rescue Exception => e
+ raise "Certificate request #{subject} creation failed: #{e.message}"
+ end
+
+ begin
+ csr_cert = mkcert(caserial, request.subject, ca, request.public_key, days, altnames)
+ csr_cert.sign(cakey, OpenSSL::Digest::SHA1.new)
+ setserial(sign_with, caserial)
+ rescue Exception => e
+ raise "Certificate #{subject} signing failed: #{e.message}"
+ end
+
+ key.send("to_pem") + csr_cert.send("to_pem")
+ else # self-signed certificate
+ begin
+ subj = OpenSSL::X509::Name.parse(subject)
+ cert = mkcert(serial, subj, nil, key.public_key, days, altnames)
+ cert.sign(key, OpenSSL::Digest::SHA1.new)
+ rescue Exception => e
+ raise "Self-signed certificate #{subject} creation failed: #{e.message}"
+ end
+
+ key.send("to_pem") + cert.send("to_pem")
+ end
+ end
+ private
+
+ # nice help: https://gist.github.com/mitfik/1922961
+
+ def mkkey(len)
+ OpenSSL::PKey::RSA.generate(len)
+ end
+
+ def mkreq(subject,public_key)
+ request = OpenSSL::X509::Request.new
+ request.version = 0
+ request.subject = subject
+ request.public_key = public_key
+
+ request
+ end
+
+ def mkcert(serial,subject,issuer,public_key,days,altnames)
+ cert = OpenSSL::X509::Certificate.new
+ issuer = cert if issuer == nil
+ cert.subject = subject
+ cert.issuer = issuer.subject
+ cert.not_before = Time.now
+ cert.not_after = Time.now + days * 24 * 60 * 60
+ cert.public_key = public_key
+ cert.serial = serial
+ cert.version = 2
+
+ ef = OpenSSL::X509::ExtensionFactory.new
+ ef.subject_certificate = cert
+ ef.issuer_certificate = issuer
+ cert.extensions = [ ef.create_extension("subjectKeyIdentifier", "hash") ]
+ cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true) if subject == issuer
+ cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true) if subject != issuer
+ cert.add_extension ef.create_extension("keyUsage", "nonRepudiation, digitalSignature, keyEncipherment", true)
+ cert.add_extension ef.create_extension("subjectAltName", altnames, true) if altnames
+ cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
+
+ cert
+ end
+
+ def getca(ca)
+ trocla.get_password(ca,'x509')
+ end
+
+ def getserial(ca,serial)
+ newser = trocla.get_password("#{ca}_serial",'plain')
+ if newser
+ newser + 1
+ else
+ serial
+ end
+ end
+
+ def setserial(ca,serial)
+ trocla.set_password("#{ca}_serial",'plain',serial)
+ end
+end
diff --git a/lib/trocla/util.rb b/lib/trocla/util.rb
new file mode 100644
index 0000000..8e94d0d
--- /dev/null
+++ b/lib/trocla/util.rb
@@ -0,0 +1,43 @@
+require 'securerandom'
+class Trocla
+ class Util
+ class << self
+ def random_str(length=12, charset='default')
+ _charsets = charsets[charset] || charsets['default']
+ _charsets_size = _charsets_size
+ (1..length).collect{|a| _charsets[SecureRandom.random_number(_charsets.size)] }.join.to_s
+ end
+
+ def salt(length=8)
+ alphanumeric_size = alphanumeric.size
+ (1..length).collect{|a| alphanumeric[SecureRandom.random_number(alphanumeric_size)] }.join.to_s
+ end
+
+ private
+
+ def charsets
+ @charsets ||= {
+ 'default' => chars,
+ 'alphanumeric' => alphanumeric,
+ 'shellsafe' => shellsafe,
+ }
+ end
+
+ def chars
+ @chars ||= shellsafe + special_chars
+ end
+ def shellsafe
+ @chars ||= alphanumeric + shellsafe_chars
+ end
+ def alphanumeric
+ @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
+ end
+ def special_chars
+ @special_chars ||= "*()&![]{}-".split(//)
+ end
+ def shellsafe_chars
+ @shellsafe_chars ||= "+%/@=?_.,:".split(//)
+ end
+ end
+ end
+end
diff --git a/lib/trocla/version.rb b/lib/trocla/version.rb
new file mode 100644
index 0000000..6d9476b
--- /dev/null
+++ b/lib/trocla/version.rb
@@ -0,0 +1,22 @@
+# encoding: utf-8
+class Trocla
+ class VERSION
+ version = {}
+ File.read(File.join(File.dirname(__FILE__), '../', 'VERSION')).each_line do |line|
+ type, value = line.chomp.split(":")
+ next if type =~ /^\s+$/ || value =~ /^\s+$/
+ version[type] = value
+ end
+
+ MAJOR = version['major']
+ MINOR = version['minor']
+ PATCH = version['patch']
+ BUILD = version['build']
+
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
+
+ def self.version
+ STRING
+ end
+ end
+end
diff --git a/spec/data/.keep b/spec/data/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/spec/data/.keep
diff --git a/spec/trocla/util_spec.rb b/spec/trocla/util_spec.rb
new file mode 100644
index 0000000..879b244
--- /dev/null
+++ b/spec/trocla/util_spec.rb
@@ -0,0 +1,28 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe "Trocla::Util" do
+
+ { :random_str => 12, :salt => 8 }.each do |m,length|
+ describe m do
+ it "should be random" do
+ Trocla::Util.send(m).should_not eql(Trocla::Util.send(m))
+ end
+
+ it "should default to length #{length}" do
+ Trocla::Util.send(m).length.should == length
+ end
+
+ it "should be possible to change length" do
+ Trocla::Util.send(m,8).length.should == 8
+ Trocla::Util.send(m,32).length.should == 32
+ Trocla::Util.send(m,1).length.should == 1
+ end
+ end
+ end
+
+ describe :salt do
+ it "should only contain characters and numbers" do
+ Trocla::Util.salt =~ /^[a-z0-9]+$/i
+ end
+ end
+end
diff --git a/spec/trocla_spec.rb b/spec/trocla_spec.rb
new file mode 100644
index 0000000..61d9921
--- /dev/null
+++ b/spec/trocla_spec.rb
@@ -0,0 +1,128 @@
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
+
+describe "Trocla" do
+
+ before(:each) do
+ Trocla.any_instance.expects(:read_config).returns(test_config)
+ @trocla = Trocla.new
+ end
+
+ describe "password" do
+ it "should generate random passwords by default" do
+ @trocla.password('random1','plain').should_not eql(@trocla.password('random2','plain'))
+ end
+
+ it "should generate passwords of length #{default_config['options']['length']}" do
+ @trocla.password('random1','plain').length.should eql(default_config['options']['length'])
+ end
+
+ Trocla::Formats.all.each do |format|
+ describe "#{format} password format" do
+ it "should return a password hashed in the #{format} format" do
+ @trocla.password('some_test',format,format_options[format]).should_not be_empty
+ end
+
+ it "should return the same hashed for the #{format} format on multiple invocations" do
+ (round1=@trocla.password('some_test',format,format_options[format])).should_not be_empty
+ @trocla.password('some_test',format,format_options[format]).should eql(round1)
+ end
+
+ it "should also store the plain password by default" do
+ pwd = @trocla.password('some_test','plain')
+ pwd.should_not be_empty
+ pwd.length.should eql(12)
+ end
+ end
+ end
+
+ Trocla::Formats.all.reject{|f| f == 'plain' }.each do |format|
+ it "should raise an exception if not a random password is asked but plain password is not present for format #{format}" do
+ lambda{ @trocla.password('not_random',format, 'random' => false) }.should raise_error
+ end
+ end
+ end
+
+ describe "set_password" do
+ it "should reset hashed passwords on a new plain password" do
+ @trocla.password('set_test','mysql').should_not be_empty
+ @trocla.get_password('set_test','mysql').should_not be_nil
+ (old_plain=@trocla.password('set_test','mysql')).should_not be_empty
+
+ @trocla.set_password('set_test','plain','foobar').should_not eql(old_plain)
+ @trocla.get_password('set_test','mysql').should be_nil
+ end
+
+ it "should otherwise only update the hash" do
+ (mysql = @trocla.password('set_test2','mysql')).should_not be_empty
+ (md5crypt = @trocla.password('set_test2','md5crypt')).should_not be_empty
+ (plain = @trocla.get_password('set_test2','plain')).should_not be_empty
+
+ (new_mysql = @trocla.set_password('set_test2','mysql','foo')).should_not eql(mysql)
+ @trocla.get_password('set_test2','mysql').should eql(new_mysql)
+ @trocla.get_password('set_test2','md5crypt').should eql(md5crypt)
+ @trocla.get_password('set_test2','plain').should eql(plain)
+ end
+ end
+
+ describe "reset_password" do
+ it "should reset a password" do
+ plain1 = @trocla.password('reset_pwd','plain')
+ plain2 = @trocla.reset_password('reset_pwd','plain')
+
+ plain1.should_not eql(plain2)
+ end
+
+ it "should not reset other formats" do
+ (mysql = @trocla.password('reset_pwd2','mysql')).should_not be_empty
+ (md5crypt1 = @trocla.password('reset_pwd2','md5crypt')).should_not be_empty
+
+ (md5crypt2 = @trocla.reset_password('reset_pwd2','md5crypt')).should_not be_empty
+ md5crypt2.should_not eql(md5crypt1)
+
+ @trocla.get_password('reset_pwd2','mysql').should eql(mysql)
+ end
+ end
+
+ describe "delete_password" do
+ it "should delete all passwords if no format is given" do
+ @trocla.password('delete_test1','mysql').should_not be_nil
+ @trocla.get_password('delete_test1','plain').should_not be_nil
+
+ @trocla.delete_password('delete_test1')
+ @trocla.get_password('delete_test1','plain').should be_nil
+ @trocla.get_password('delete_test1','mysql').should be_nil
+ end
+
+ it "should delete only a given format" do
+ @trocla.password('delete_test2','mysql').should_not be_nil
+ @trocla.get_password('delete_test2','plain').should_not be_nil
+
+ @trocla.delete_password('delete_test2','plain')
+ @trocla.get_password('delete_test2','plain').should be_nil
+ @trocla.get_password('delete_test2','mysql').should_not be_nil
+ end
+
+ it "should delete only a given non-plain format" do
+ @trocla.password('delete_test3','mysql').should_not be_nil
+ @trocla.get_password('delete_test3','plain').should_not be_nil
+
+ @trocla.delete_password('delete_test3','mysql')
+ @trocla.get_password('delete_test3','mysql').should be_nil
+ @trocla.get_password('delete_test3','plain').should_not be_nil
+ end
+ end
+
+ describe "VERSION" do
+ it "should return a version" do
+ Trocla::VERSION::STRING.should_not be_empty
+ end
+ end
+
+ def format_options
+ @format_options ||= Hash.new({}).merge({
+ 'pgsql' => { 'username' => 'test' },
+ 'x509' => { 'CN' => 'test' },
+ })
+ end
+
+end
diff --git a/trocla.gemspec b/trocla.gemspec
new file mode 100644
index 0000000..65c63f3
--- /dev/null
+++ b/trocla.gemspec
@@ -0,0 +1,89 @@
+# Generated by jeweler
+# DO NOT EDIT THIS FILE DIRECTLY
+# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
+# -*- encoding: utf-8 -*-
+# stub: trocla 0.0.11 ruby lib
+
+Gem::Specification.new do |s|
+ s.name = "trocla"
+ s.version = "0.0.11"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.require_paths = ["lib"]
+ s.authors = ["mh"]
+ s.date = "2014-08-21"
+ s.description = "Trocla helps you to generate random passwords and to store them in various formats (plain, MD5, bcrypt) for later retrival."
+ s.email = "mh+trocla@immerda.ch"
+ s.executables = ["trocla"]
+ s.extra_rdoc_files = [
+ "LICENSE.txt",
+ "README.md"
+ ]
+ s.files = [
+ ".document",
+ ".rspec",
+ ".travis.yml",
+ "Gemfile",
+ "Gemfile.lock",
+ "LICENSE.txt",
+ "README.md",
+ "Rakefile",
+ "bin/trocla",
+ "lib/VERSION",
+ "lib/trocla.rb",
+ "lib/trocla/default_config.yaml",
+ "lib/trocla/formats.rb",
+ "lib/trocla/formats/bcrypt.rb",
+ "lib/trocla/formats/md5crypt.rb",
+ "lib/trocla/formats/mysql.rb",
+ "lib/trocla/formats/pgsql.rb",
+ "lib/trocla/formats/plain.rb",
+ "lib/trocla/formats/sha1.rb",
+ "lib/trocla/formats/sha256crypt.rb",
+ "lib/trocla/formats/sha512crypt.rb",
+ "lib/trocla/formats/ssha.rb",
+ "lib/trocla/formats/x509.rb",
+ "lib/trocla/util.rb",
+ "lib/trocla/version.rb",
+ "spec/data/.keep",
+ "spec/spec_helper.rb",
+ "spec/trocla/util_spec.rb",
+ "spec/trocla_spec.rb",
+ "trocla.gemspec"
+ ]
+ s.homepage = "https://tech.immerda.ch/2011/12/trocla-get-hashed-passwords-out-of-puppet-manifests/"
+ s.licenses = ["GPLv3"]
+ s.rubygems_version = "2.3.0"
+ s.summary = "Trocla a simple password generator and storage"
+
+ if s.respond_to? :specification_version then
+ s.specification_version = 4
+
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
+ s.add_runtime_dependency(%q<moneta>, ["~> 0.7"])
+ s.add_runtime_dependency(%q<highline>, [">= 0"])
+ s.add_runtime_dependency(%q<bcrypt>, [">= 0"])
+ s.add_development_dependency(%q<rspec>, ["~> 2.4"])
+ s.add_development_dependency(%q<rdoc>, ["~> 3.8"])
+ s.add_development_dependency(%q<mocha>, [">= 0"])
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
+ else
+ s.add_dependency(%q<moneta>, ["~> 0.7"])
+ s.add_dependency(%q<highline>, [">= 0"])
+ s.add_dependency(%q<bcrypt>, [">= 0"])
+ s.add_dependency(%q<rspec>, ["~> 2.4"])
+ s.add_dependency(%q<rdoc>, ["~> 3.8"])
+ s.add_dependency(%q<mocha>, [">= 0"])
+ s.add_dependency(%q<jeweler>, [">= 0"])
+ end
+ else
+ s.add_dependency(%q<moneta>, ["~> 0.7"])
+ s.add_dependency(%q<highline>, [">= 0"])
+ s.add_dependency(%q<bcrypt>, [">= 0"])
+ s.add_dependency(%q<rspec>, ["~> 2.4"])
+ s.add_dependency(%q<rdoc>, ["~> 3.8"])
+ s.add_dependency(%q<mocha>, [">= 0"])
+ s.add_dependency(%q<jeweler>, [">= 0"])
+ end
+end
+