diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/VERSION | 4 | ||||
-rw-r--r-- | lib/trocla.rb | 94 | ||||
-rw-r--r-- | lib/trocla/default_config.yaml | 8 | ||||
-rw-r--r-- | lib/trocla/formats.rb | 40 | ||||
-rw-r--r-- | lib/trocla/formats/bcrypt.rb | 6 | ||||
-rw-r--r-- | lib/trocla/formats/md5crypt.rb | 6 | ||||
-rw-r--r-- | lib/trocla/formats/mysql.rb | 6 | ||||
-rw-r--r-- | lib/trocla/formats/pgsql.rb | 7 | ||||
-rw-r--r-- | lib/trocla/formats/plain.rb | 7 | ||||
-rw-r--r-- | lib/trocla/formats/sha1.rb | 7 | ||||
-rw-r--r-- | lib/trocla/formats/sha256crypt.rb | 6 | ||||
-rw-r--r-- | lib/trocla/formats/sha512crypt.rb | 6 | ||||
-rw-r--r-- | lib/trocla/formats/ssha.rb | 9 | ||||
-rw-r--r-- | lib/trocla/formats/x509.rb | 128 | ||||
-rw-r--r-- | lib/trocla/util.rb | 43 | ||||
-rw-r--r-- | lib/trocla/version.rb | 22 |
16 files changed, 399 insertions, 0 deletions
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 |