trocla-0.6.0/ 0000755 0000041 0000041 00000000000 14756064121 013037 5 ustar www-data www-data trocla-0.6.0/bin/ 0000755 0000041 0000041 00000000000 14756064121 013607 5 ustar www-data www-data trocla-0.6.0/bin/trocla 0000755 0000041 0000041 00000011104 14756064121 015016 0 ustar www-data www-data #!/usr/bin/env ruby
# CLI client for Trocla.
#
require 'rubygems'
require 'trocla'
require 'optparse'
require 'yaml'
options = { :config_file => nil, :ask_password => true, :trace => false }
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('--trace', 'Show stack trace on failure') do
options[:trace] = true
end
opts.on('--no-random', 'Do not generate a random password if there is no plain text password available') do
options['random'] = false
end
opts.on('--no-format', 'Do not format a password when setting it using `set`') do
options['no_format'] = true
end
opts.on('--length LENGTH', 'Length for a randomly created password') do |v|
options['length'] = v.to_i
end
opts.on('--password [PASSWORD]', '-p', 'Provide password at command line or STDIN') 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.safe_load(options.delete(:other_options).shift.to_s) || {})
), 0]
end
def get(options)
res = Trocla.new(options.delete(:config_file)).get_password(
options.delete(:trocla_key),
options.delete(:trocla_format),
options.merge(YAML.safe_load(options.delete(:other_options).shift.to_s) || {})
)
[res, res.nil? ? 1 : 0]
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!'
return [nil, 1]
end
else
password = options.delete(:password) || STDIN.read.chomp
end
format = options.delete(:trocla_format)
no_format = options.delete('no_format')
trocla = Trocla.new(options.delete(:config_file))
value = if no_format
password
else
trocla.formats(format).format(password, (YAML.safe_load(options.delete(:other_options).shift.to_s) || {}))
end
trocla.set_password(
options.delete(:trocla_key),
format,
value
)
['', 0]
end
def reset(options)
[Trocla.new(options.delete(:config_file)).reset_password(
options.delete(:trocla_key),
options.delete(:trocla_format),
options.merge(YAML.safe_load(options.delete(:other_options).shift.to_s) || {})
), 0]
end
def delete(options)
res = Trocla.new(options.delete(:config_file)).delete_password(
options.delete(:trocla_key),
options.delete(:trocla_format)
)
[res, res.nil? ? 1 : 0]
end
def formats(options)
key = (options.delete(:trocla_key) || '')
if key.empty?
"Available formats: #{Trocla::Formats.all.join(', ')}"
else
res = Trocla.new(options.delete(:config_file)).available_format(
key,
options.merge(YAML.safe_load(options.delete(:other_options).shift.to_s) || {})
)
[res.nil? ? res : res.join(', '), res.nil? ? 1 : 0]
end
end
def search(options)
res = Trocla.new(options.delete(:config_file)).search_key(
options.delete(:trocla_key)
)
[res.nil? ? res : res.join("\n"), res.nil? ? 1 : 0]
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', 'formats', 'search']
if (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 ['delete','formats','search'].include?(action)
begin
result, excode = send(action, options)
if result
puts result.is_a?(String) ? result : result.inspect
end
rescue Exception => e
unless e.message == 'exit'
STDERR.puts "Action failed with the following message: #{e.message}"
STDERR.puts '(See full trace by running task with --trace)'
end
raise e if options[:trace]
exit 1
end
exit excode.nil? ? 0 : excode
else
STDERR.puts "Please supply one of the following actions: #{actions.join(', ')}"
STDERR.puts "Use #{$0} --help to get a list of options for these actions"
exit 1
end
trocla-0.6.0/.document 0000644 0000041 0000041 00000000041 14756064121 014651 0 ustar www-data www-data lib/**/*.rb
bin/*
-
LICENSE.txt
trocla-0.6.0/.github/ 0000755 0000041 0000041 00000000000 14756064121 014377 5 ustar www-data www-data trocla-0.6.0/.github/workflows/ 0000755 0000041 0000041 00000000000 14756064121 016434 5 ustar www-data www-data trocla-0.6.0/.github/workflows/ruby.yml 0000644 0000041 0000041 00000001175 14756064121 020144 0 ustar www-data www-data ---
name: Ruby
on: [push, pull_request]
jobs:
spec:
runs-on: ubuntu-latest
steps:
- name: check out repository
uses: actions/checkout@v3
- name: set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: ${{ matrix.ruby }}
- name: install dependencies
run: bundle install
- name: install wireguard
run: sudo apt install -y wireguard
- name: run rspec
run: bundle exec rake spec
strategy:
fail-fast: false
matrix:
ruby: ['2.5','2.7', '3.0', '3.1','3.2','3.3','head','jruby','jruby-head']
trocla-0.6.0/lib/ 0000755 0000041 0000041 00000000000 14756064121 013605 5 ustar www-data www-data trocla-0.6.0/lib/trocla.rb 0000644 0000041 0000041 00000012007 14756064121 015416 0 ustar www-data www-data require 'trocla/version'
require 'trocla/util'
require 'trocla/formats'
require 'trocla/encryptions'
require 'trocla/stores'
require 'trocla/hooks'
# Trocla class
class Trocla
def initialize(config_file = nil)
if config_file
@config_file = File.expand_path(config_file)
elsif File.exist?(def_config_file = File.expand_path('~/.troclarc.yaml')) || File.exist?(def_config_file = File.expand_path('/etc/troclarc.yaml'))
@config_file = def_config_file
end
end
def self.open(config_file = nil)
trocla = Trocla.new(config_file)
if block_given?
yield trocla
trocla.close
else
trocla
end
end
def password(key, format, options={})
# respect a default profile, but let the
# profiles win over the default options
options['profiles'] ||= config['options']['profiles']
options = merge_profiles(options['profiles']).merge(options) if options['profiles']
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, options)).nil?
return password
end
plain_pwd = get_password(key, 'plain', options)
if options['random'] && plain_pwd.nil?
plain_pwd = Trocla::Util.random_str(options['length'].to_i, options['charset'])
set_password(key, 'plain', plain_pwd, options) 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
pwd = self.formats(format).format(plain_pwd, options)
# it's possible that meanwhile another thread/process was faster in
# formating the password. But we want todo that second lookup
# only for expensive formats
if self.formats(format).expensive?
get_password(key, format, options) || set_password(key, format, pwd, options)
else
set_password(key, format, pwd, options)
end
end
def get_password(key, format, options = {})
render(format, decrypt(store.get(key, format)), options)
end
def reset_password(key, format, options = {})
delete_password(key, format)
password(key, format, options)
end
def delete_password(key, format = nil, options = {})
v = store.delete(key, format)
hooks_runner.run('delete', key, format, options)
if v.is_a?(Hash)
Hash[*v.map do |f, encrypted_value|
[f, render(format, decrypt(encrypted_value), options)]
end.flatten]
else
render(format, decrypt(v), options)
end
end
def set_password(key, format, password, options = {})
store.set(key, format, encrypt(password), options)
hooks_runner.run('set', key, format, options)
render(format, password, options)
end
def available_format(key, options = {})
render(false, store.formats(key), options)
end
def search_key(key, options = {})
render(false, store.search(key), options)
end
def formats(format)
(@format_cache ||= {})[format] ||= Trocla::Formats[format].new(self)
end
def encryption
@encryption ||= Trocla::Encryptions[config['encryption']].new(config['encryption_options'], self)
end
def config
@config ||= read_config
end
def close
store.close
end
private
def store
@store ||= build_store
end
def build_store
s = config['store']
clazz = if s.is_a?(Symbol)
Trocla::Stores[s]
else
require config['store_require'] if config['store_require']
eval(s)
end
clazz.new(config['store_options'], self)
end
def hooks_runner
@hooks_runner ||= begin
Trocla::Hooks::Runner.new(self)
end
end
def read_config
if @config_file.nil?
default_config
else
raise "Configfile #{@config_file} does not exist!" unless File.exist?(@config_file)
c = default_config.merge(YAML.load(File.read(@config_file)))
c['profiles'] = default_config['profiles'].merge(c['profiles'])
# migrate all options to new store options
# TODO: remove workaround in 0.3.0
c['store_options']['adapter'] = c['adapter'] if c['adapter']
c['store_options']['adapter_options'] = c['adapter_options'] if c['adapter_options']
c['encryption_options'] = c['ssl_options'] if c['ssl_options']
c
end
end
def encrypt(value)
encryption.encrypt(value)
end
def decrypt(value)
return nil if value.nil?
encryption.decrypt(value)
end
def render(format, output, options = {})
if format && output && f = self.formats(format)
f.render(output, options['render'] || {})
else
output
end
end
def default_config
require 'yaml'
YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__), 'trocla', 'default_config.yaml'))))
end
def merge_profiles(profiles)
Array(profiles).inject({}) do |res, profile|
raise "No such profile #{profile} defined" unless profile_hash = config['profiles'][profile]
profile_hash.merge(res)
end
end
end
trocla-0.6.0/lib/VERSION 0000644 0000041 0000041 00000000037 14756064121 014655 0 ustar www-data www-data major:0
minor:6
patch:0
build:
trocla-0.6.0/lib/trocla/ 0000755 0000041 0000041 00000000000 14756064121 015071 5 ustar www-data www-data trocla-0.6.0/lib/trocla/store.rb 0000644 0000041 0000041 00000004313 14756064121 016553 0 ustar www-data www-data # implements the default store behavior
class Trocla::Store
attr_reader :store_config, :trocla
def initialize(config, trocla)
@store_config = config
@trocla = trocla
end
# closes the store
# when called do whatever "closes" your
# store, e.g. close database connections.
def close; end
# should return value for key & format
# returns nil if nothing or a nil value
# was found.
# If a key is expired it must return nil.
def get(key, format)
raise 'not implemented'
end
# sets value for key & format
# setting the plain format must invalidate
# all other formats as they should either
# be derived from plain or set directly.
# options is a hash containing further
# information for the store. e.g. expiration
# of a key. Keys can have an expiration /
# timeout by setting `expires` within
# the options hashs. Value of `expires`
# must be an integer indicating the
# amount of seconds a key can live with.
# This mechanism is expected to be
# be implemented by the backend.
def set(key, format, value, options = {})
if format == 'plain'
set_plain(key, value, options)
else
set_format(key, format, value, options)
end
end
# deletes the value for format
# if format is nil everything is deleted
# returns value of format or hash of
# format => value # if everything is
# deleted.
def delete(key, format = nil)
format.nil? ? (delete_all(key) || {}) : delete_format(key, format)
end
# returns all formats for a key
def formats(_)
raise 'not implemented'
end
# def searches for a key
def search(_)
raise 'not implemented'
end
private
# sets a new plain value
# *must* invalidate all
# other formats
def set_plain(key, value, options)
raise 'not implemented'
end
# sets a value of a format
def set_format(key, format, value, options)
raise 'not implemented'
end
# deletes all entries of this key
# and returns a hash with all
# formats and values
# or nil if nothing is found
def delete_all(_)
raise 'not implemented'
end
# deletes the value of the passed
# key & format and returns the
# value.
def delete_format(key, format)
raise 'not implemented'
end
end
trocla-0.6.0/lib/trocla/encryptions/ 0000755 0000041 0000041 00000000000 14756064121 017446 5 ustar www-data www-data trocla-0.6.0/lib/trocla/encryptions/ssl.rb 0000644 0000041 0000041 00000002043 14756064121 020573 0 ustar www-data www-data require 'openssl'
require 'base64'
class Trocla::Encryptions::Ssl < Trocla::Encryptions::Base
def encrypt(value)
ciphertext = ''
value.scan(/.{0,#{chunksize}}/m).each do |chunk|
ciphertext += Base64.encode64(public_key.public_encrypt(chunk)).gsub("\n", '') + "\n" if chunk
end
ciphertext
end
def decrypt(value)
plaintext = ''
value.split(/\n/).each do |line|
plaintext += private_key.private_decrypt(Base64.decode64(line)) if line
end
plaintext
end
private
def chunksize
public_key.n.num_bytes - 11
end
def private_key
@private_key ||= begin
file = require_option(:private_key)
OpenSSL::PKey::RSA.new(File.read(file), nil)
end
end
def public_key
@public_key ||= begin
file = require_option(:public_key)
OpenSSL::PKey::RSA.new(File.read(file), nil)
end
end
def option(key)
config[key]
end
def require_option(key)
val = option(key)
raise "Config error: 'ssl_options' => :#{key} is not defined" if val.nil?
val
end
end
trocla-0.6.0/lib/trocla/encryptions/none.rb 0000644 0000041 0000041 00000000213 14756064121 020726 0 ustar www-data www-data class Trocla::Encryptions::None < Trocla::Encryptions::Base
def encrypt(value)
value
end
def decrypt(value)
value
end
end
trocla-0.6.0/lib/trocla/default_config.yaml 0000644 0000041 0000041 00000001276 14756064121 020734 0 ustar www-data www-data ---
store: :moneta
store_options:
adapter: :YAML
adapter_options:
:file: '/tmp/trocla.yaml'
encryption: :none
options:
random: true
length: 16
charset: default
profiles:
rootpw:
charset: consolesafe
length: 32
mysql:
charset: shellsafe
length: 32
login:
charset: consolesafe
length: 16
x509veryverylong:
# 15 years
days: 5475
# 5475 days
expires: 466560000
x509verylong:
# 10 years
days: 3650
# 3600 days
expires: 311040000
x509long:
# 5 years
days: 1825
# 1800 days
expires: 155520000
x509auto:
days: 40
# 30 days
expires: 2592000
x509short:
days: 2
# 1 day
expires: 86400
trocla-0.6.0/lib/trocla/hooks.rb 0000644 0000041 0000041 00000001313 14756064121 016537 0 ustar www-data www-data class Trocla
module Hooks
class Runner
attr_reader :trocla
def initialize(trocla)
@trocla = trocla
end
def run(action, key, format, options)
return unless hooks[action]
hooks[action].each do |cmd|
Trocla::Hooks.send(cmd, trocla, key, format, options)
end
end
private
def hooks
@hooks ||= begin
res = {}
(trocla.config['hooks'] || {}).each do |action,action_hooks|
res[action] ||= []
action_hooks.each do |cmd,file|
require File.join(file)
res[action] << cmd
end
end
res
end
end
end
end
end
trocla-0.6.0/lib/trocla/formats.rb 0000644 0000041 0000041 00000002363 14756064121 017075 0 ustar www-data www-data # frozen_string_literal: true
# Trocla::Formats
class Trocla::Formats
# Base
class Base
attr_reader :trocla
def initialize(trocla)
@trocla = trocla
end
def render(output, render_options = {})
output
end
def expensive?
self.class.expensive?
end
class << self
def expensive(is_expensive)
@expensive = is_expensive
end
def expensive?
@expensive == true
end
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.exist?(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
trocla-0.6.0/lib/trocla/stores/ 0000755 0000041 0000041 00000000000 14756064121 016410 5 ustar www-data www-data trocla-0.6.0/lib/trocla/stores/vault.rb 0000644 0000041 0000041 00000003441 14756064121 020072 0 ustar www-data www-data # the default vault based store
class Trocla::Stores::Vault < Trocla::Store
attr_reader :vault, :mount, :destroy
def initialize(config, trocla)
super(config, trocla)
require 'vault'
@mount = (config.delete(:mount) || 'kv')
@destroy = (config.delete(:destroy) || false)
# load expire support by default
@vault = Vault::Client.new(config)
end
def close; end
def get(key, format)
read(key)[format.to_sym]
end
def formats(key)
read(key).keys
end
def search(key)
arr = key.split('/')
regexp = Regexp.new(arr.pop(1)[0].to_s)
path = arr.join('/')
list = vault.kv(mount).list(path)
list.map! do |l|
if regexp.match(l)
path.empty? ? l : [path, l].join('/')
end
end
list.compact
end
private
def read(key)
k = vault.kv(mount).read(key)
k.nil? ? {} : k.data
end
def write(key, value, options = {})
vault.kv(mount).write_metadata(key, convert_metadata(options)) unless options.empty?
vault.kv(mount).write(key, value)
end
def set_plain(key, value, options)
set_format(key, 'plain', value, options)
end
def set_format(key, format, value, options)
write(
key,
read(key).merge({ format.to_sym => value }),
options
)
end
def delete_all(key)
destroy ? vault.kv(mount).destroy(key) : vault.kv(mount).delete(key)
end
def delete_format(key, format)
old = read(key)
new = old.reject { |k, _| k == format.to_sym }
new.empty? ? delete_all(key) : write(key, new)
old[format.to_sym]
end
def convert_metadata(metadatas)
metadatas.transform_keys!(&:to_sym)
metadatas[:delete_version_after] = metadatas.delete(:expires) if metadatas[:expires]
%i[random profiles expires length].each { |k| metadatas.delete(k) }
metadatas
end
end
trocla-0.6.0/lib/trocla/stores/moneta.rb 0000644 0000041 0000041 00000004473 14756064121 020230 0 ustar www-data www-data # the default moneta based store
class Trocla::Stores::Moneta < Trocla::Store
attr_reader :moneta
def initialize(config, trocla)
super(config, trocla)
require 'moneta'
# load expire support by default
adapter_options = {
:expires => true
}.merge(store_config['adapter_options'] || {})
@moneta = Moneta.new(store_config['adapter'], adapter_options)
end
def close
moneta.close
end
def get(key, format)
moneta.fetch(key, {})[format]
end
def formats(key)
r = moneta.fetch(key)
r.nil? ? nil : r.keys
end
def search(key)
raise 'The search option is not available for any adapter other than Sequel or YAML' unless store_config['adapter'] == :Sequel || store_config['adapter'] == :YAML
r = search_keys(key)
r.empty? ? nil : r
end
private
def set_plain(key, value, options)
h = { 'plain' => value }
mo = moneta_options(key, options)
if options['expires'] && options['expires'] > 0
h['_expires'] = options['expires']
else
# be sure that we disable the existing
# expires if nothing is set.
mo[:expires] = false
end
moneta.store(key, h, mo)
end
def set_format(key, format, value, options)
moneta.store(
key,
moneta.fetch(key, {}).merge({ format => value }),
moneta_options(key, options)
)
end
def delete_all(key)
moneta.delete(key)
end
def delete_format(key, format)
old_val = (h = moneta.fetch(key, {})).delete(format)
h.empty? ? moneta.delete(key) : moneta.store(key, h, moneta_options(key, {}))
old_val
end
def moneta_options(key, options)
res = {}
if options.key?('expires')
res[:expires] = options['expires']
elsif e = moneta.fetch(key, {})['_expires']
res[:expires] = e
end
res
end
def search_keys(key)
_moneta = Moneta.new(store_config['adapter'], (store_config['adapter_options'] || {}).merge({ :expires => false }))
a = []
if store_config['adapter'] == :Sequel
keys = _moneta.adapter.backend[:trocla].select_order_map { from_base64(:k) }
elsif store_config['adapter'] == :YAML
keys = _moneta.adapter.backend.transaction(true) { _moneta.adapter.backend.roots }
end
_moneta.close
regexp = Regexp.new("#{key}")
keys.each do |k|
a << k if regexp.match(k)
end
a
end
end
trocla-0.6.0/lib/trocla/stores/memory.rb 0000644 0000041 0000041 00000002715 14756064121 020252 0 ustar www-data www-data # a simple in memory store just as an example
class Trocla::Stores::Memory < Trocla::Store
attr_reader :memory
def initialize(config, trocla)
super(config, trocla)
@memory = Hash.new({})
end
def get(key, format)
unless expired?(key)
memory[key][format]
else
delete_all(key)
nil
end
end
def set(key, format, value, options = {})
super(key, format, value, options)
set_expires(key, options['expires'])
end
def formats(key)
memory[key].empty? ? nil : memory[key].keys
end
def search(key)
r = memory.keys.grep(/#{key}/)
r.empty? ? nil : r
end
private
def set_plain(key, value, _)
memory[key] = { 'plain' => value }
end
def set_format(key, format, value, _)
memory[key].merge!({ format => value })
end
def delete_all(key)
memory.delete(key)
end
def delete_format(key, format)
old_val = (h = memory[key]).delete(format)
h.empty? ? memory.delete(key) : memory[key] = h
set_expires(key,nil)
old_val
end
private
def set_expires(key, expires)
expires = memory[key]['_expires'] if expires.nil?
if expires && expires > 0
memory[key]['_expires'] = expires
memory[key]['_expires_at'] = Time.now + expires
else
memory[key].delete('_expires')
memory[key].delete('_expires_at')
end
end
def expired?(key)
memory.key?(key) &&
(a = memory[key]['_expires_at']).is_a?(Time) && \
(a < Time.now)
end
end
trocla-0.6.0/lib/trocla/util.rb 0000644 0000041 0000041 00000003655 14756064121 016404 0 ustar www-data www-data require 'securerandom'
class Trocla
# Utils
class Util
class << self
def random_str(length = 12, charset = 'default')
char = charsets[charset] || charsets['default']
charsets_size = char.size
(1..length).collect { |_| char[rand_num(charsets_size)] }.join.to_s
end
def salt(length = 8)
random_str(length, 'alphanumeric')
end
private
def rand_num(n)
SecureRandom.random_number(n)
end
def charsets
@charsets ||= begin
h = {
'default' => chars, 'alphanumeric' => alphanumeric, 'shellsafe' => shellsafe,
'windowssafe' => windowssafe, 'numeric' => numeric, 'hexadecimal' => hexadecimal,
'consolesafe' => consolesafe, 'typesafe' => typesafe
}
h.each { |k, v| h[k] = v.uniq }
end
end
def chars
@chars ||= shellsafe + special_chars
end
def shellsafe
@shellsafe ||= alphanumeric + shellsafe_chars
end
def windowssafe
@windowssafe ||= alphanumeric + windowssafe_chars
end
def consolesafe
@consolesafe ||= alphanumeric + consolesafe_chars
end
def hexadecimal
@hexadecimal ||= numeric + ('a'..'f').to_a
end
def alphanumeric
@alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + numeric
end
def numeric
@numeric ||= ('0'..'9').to_a
end
def typesafe
@typesafe ||= ('a'..'x').to_a - ['i'] - ['l'] + ('A'..'X').to_a - ['I'] - ['L'] + ('1'..'9').to_a
end
def special_chars
@special_chars ||= '*()&![]{}-'.split(//)
end
def shellsafe_chars
@shellsafe_chars ||= '+%/@=?_.,:'.split(//)
end
def windowssafe_chars
@windowssafe_chars ||= '+%/@=?_.,'.split(//)
end
def consolesafe_chars
@consolesafe_chars ||= '+.-,_'.split(//)
end
end
end
end
trocla-0.6.0/lib/trocla/formats/ 0000755 0000041 0000041 00000000000 14756064121 016544 5 ustar www-data www-data trocla-0.6.0/lib/trocla/formats/bcrypt.rb 0000644 0000041 0000041 00000000361 14756064121 020374 0 ustar www-data www-data class Trocla::Formats::Bcrypt < Trocla::Formats::Base
expensive true
require 'bcrypt'
def format(plain_password, options = {})
BCrypt::Password.create(plain_password, :cost => options['cost'] || BCrypt::Engine.cost).to_s
end
end
trocla-0.6.0/lib/trocla/formats/sha256crypt.rb 0000644 0000041 0000041 00000000273 14756064121 021165 0 ustar www-data www-data # salted crypt
class Trocla::Formats::Sha256crypt < Trocla::Formats::Base
def format(plain_password, options = {})
plain_password.crypt('$5$' << Trocla::Util.salt << '$')
end
end
trocla-0.6.0/lib/trocla/formats/plain.rb 0000644 0000041 0000041 00000000175 14756064121 020177 0 ustar www-data www-data class Trocla::Formats::Plain < Trocla::Formats::Base
def format(plain_password, options = {})
plain_password
end
end
trocla-0.6.0/lib/trocla/formats/mysql.rb 0000644 0000041 0000041 00000000317 14756064121 020237 0 ustar www-data www-data 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
trocla-0.6.0/lib/trocla/formats/ssha.rb 0000644 0000041 0000041 00000000460 14756064121 020027 0 ustar www-data www-data # 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
trocla-0.6.0/lib/trocla/formats/pgsql.rb 0000644 0000041 0000041 00000002575 14756064121 020230 0 ustar www-data www-data class Trocla::Formats::Pgsql < Trocla::Formats::Base
require 'digest/md5'
require 'openssl'
require 'base64'
def format(plain_password, options = {})
encode = (options['encode'] || 'sha256')
case encode
when 'md5'
raise 'You need pass the username as an option to use this format' unless options['username']
'md5' + Digest::MD5.hexdigest(plain_password + options['username'])
when 'sha256'
pg_sha256(plain_password)
else
raise 'Unkmow encode %s for pgsql password' % [encode]
end
end
private
def pg_sha256(password)
salt = OpenSSL::Random.random_bytes(16)
digest = digest_key(password, salt)
'SCRAM-SHA-256$%s:%s$%s:%s' % [
'4096',
Base64.strict_encode64(salt),
Base64.strict_encode64(client_key(digest)),
Base64.strict_encode64(server_key(digest))
]
end
def digest_key(password, salt)
OpenSSL::KDF.pbkdf2_hmac(
password,
salt: salt,
iterations: 4096,
length: 32,
hash: OpenSSL::Digest::SHA256.new
)
end
def client_key(digest_key)
hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new)
hmac << 'Client Key'
hmac.digest
OpenSSL::Digest.new('SHA256').digest hmac.digest
end
def server_key(digest_key)
hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new)
hmac << 'Server Key'
hmac.digest
end
end
trocla-0.6.0/lib/trocla/formats/x509.rb 0000644 0000041 0000041 00000015030 14756064121 017575 0 ustar www-data www-data require 'openssl'
# Trocla::Formats::X509
class Trocla::Formats::X509 < Trocla::Formats::Base
expensive true
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
cn = nil
if options['subject']
subject = options['subject']
if cna = OpenSSL::X509::Name.parse(subject).to_a.find { |e| e[0] == 'CN' }
cn = cna[1]
end
elsif options['CN']
subject = ''
cn = options['CN']
['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
hash = options['hash'] || 'sha2'
sign_with = options['ca']
become_ca = options['become_ca'] || false
keysize = options['keysize'] || 4096
days = options['days'].nil? ? 365 : options['days'].to_i
name_constraints = Array(options['name_constraints'])
key_usages = options['key_usages']
key_usages = Array(key_usages) if key_usages
altnames = if become_ca || (an = options['altnames']) && Array(an).empty?
[]
else
# ensure that we have the CN with us, but only if it
# it's like a hostname.
# This might have to be improved.
if cn.include?(' ')
Array(an).collect { |v| "DNS:#{v}" }.join(', ')
else
(["DNS:#{cn}"] + Array(an).collect { |v| "DNS:#{v}" }).uniq.join(', ')
end
end
begin
key = mkkey(keysize)
rescue Exception => e
puts e.backtrace
raise "Private key for #{subject} creation failed: #{e.message}"
end
cert = nil
if sign_with # certificate signed with CA
begin
ca_str = trocla.get_password(sign_with, 'x509')
ca = OpenSSL::X509::Certificate.new(ca_str)
cakey = OpenSSL::PKey::RSA.new(ca_str)
caserial = getserial(sign_with)
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, signature(hash))
rescue Exception => e
raise "Certificate request #{subject} creation failed: #{e.message}"
end
begin
cert = mkcert(
caserial, request.subject, ca, request.public_key, days,
altnames, key_usages, name_constraints, become_ca
)
cert.sign(cakey, signature(hash))
addserial(sign_with, caserial)
rescue Exception => e
raise "Certificate #{subject} signing failed: #{e.message}"
end
else # self-signed certificate
begin
subj = OpenSSL::X509::Name.parse(subject)
cert = mkcert(
getserial(subj), subj, nil, key.public_key, days,
altnames, key_usages, name_constraints, become_ca
)
cert.sign(key, signature(hash))
rescue Exception => e
raise "Self-signed certificate #{subject} creation failed: #{e.message}"
end
end
key.to_pem + cert.to_pem
end
def render(output, render_options = {})
if render_options['keyonly']
OpenSSL::PKey::RSA.new(output).to_pem
elsif render_options['certonly']
OpenSSL::X509::Certificate.new(output).to_pem
elsif render_options['publickeyonly']
OpenSSL::PKey::RSA.new(output).public_key.to_pem
else
super(output, render_options)
end
end
private
# nice help: https://gist.github.com/mitfik/1922961
def signature(hash = 'sha2')
if hash == 'sha1'
OpenSSL::Digest::SHA1.new
elsif hash == 'sha224'
OpenSSL::Digest::SHA224.new
elsif hash == 'sha2' || hash == 'sha256'
OpenSSL::Digest::SHA256.new
elsif hash == 'sha384'
OpenSSL::Digest::SHA384.new
elsif hash == 'sha512'
OpenSSL::Digest::SHA512.new
else
raise "Unrecognized hash: #{hash}"
end
end
def mkkey(len)
OpenSSL::PKey::RSA.generate(len)
end
def mkreq(subject, public_key)
request = OpenSSL::X509::Request.new
request.subject = subject
request.public_key = public_key
request
end
def mkcert(serial, subject, issuer, public_key, days, altnames, key_usages = nil, name_constraints = [], become_ca = false)
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')]
if become_ca
cert.add_extension ef.create_extension('basicConstraints', 'CA:TRUE', true)
unless (ku = key_usages || ca_key_usages).empty?
cert.add_extension ef.create_extension('keyUsage', ku.join(', '), true)
end
if name_constraints && !name_constraints.empty?
cert.add_extension ef.create_extension('nameConstraints', "permitted;DNS:#{name_constraints.join(',permitted;DNS:')}", true)
end
else
cert.add_extension ef.create_extension('subjectAltName', altnames, true) unless altnames.empty?
cert.add_extension ef.create_extension('basicConstraints', 'CA:FALSE', true)
unless (ku = key_usages || cert_key_usages).empty?
cert.add_extension ef.create_extension('keyUsage', ku.join(', '), true)
end
end
cert.add_extension ef.create_extension('authorityKeyIdentifier', 'keyid:always,issuer:always')
cert
end
def getserial(ca)
newser = Trocla::Util.random_str(20, 'hexadecimal').to_i(16)
all_serials(ca).include?(newser) ? getserial(ca) : newser
end
def all_serials(ca)
if allser = trocla.get_password("#{ca}_all_serials", 'plain')
YAML.safe_load(allser)
else
[]
end
end
def addserial(ca,serial)
serials = all_serials(ca) << serial
trocla.set_password("#{ca}_all_serials", 'plain', YAML.dump(serials))
end
def cert_key_usages
['nonRepudiation', 'digitalSignature', 'keyEncipherment']
end
def ca_key_usages
[
'keyCertSign', 'cRLSign', 'nonRepudiation',
'digitalSignature', 'keyEncipherment'
]
end
end
trocla-0.6.0/lib/trocla/formats/wireguard.rb 0000644 0000041 0000041 00000002222 14756064121 021060 0 ustar www-data www-data require 'open3'
require 'yaml'
class Trocla::Formats::Wireguard < Trocla::Formats::Base
expensive true
def format(plain_password, options={})
return YAML.safe_load(plain_password) if plain_password.match(/---/)
wg_priv = nil
wg_pub = nil
begin
Open3.popen3('wg genkey') do |_stdin, stdout, _stderr, _waiter|
wg_priv = stdout.read.chomp
end
rescue SystemCallError => e
raise 'trocla wireguard: wg binary not found' if e.message =~ /No such file or directory/
raise "trocla wireguard: #{e.message}"
end
begin
Open3.popen3('wg pubkey') do |stdin, stdout, _stderr, _waiter|
stdin.write(wg_priv)
stdin.close
wg_pub = stdout.read.chomp
end
rescue SystemCallError => e
raise "trocla wireguard: #{e.message}"
end
YAML.dump({ 'wg_priv' => wg_priv, 'wg_pub' => wg_pub })
end
def render(output, render_options = {})
data = YAML.safe_load(output)
if render_options['privonly']
data['wg_priv']
elsif render_options['pubonly']
data['wg_pub']
else
'pub: ' + data['wg_pub'] + "\npriv: " + data['wg_priv']
end
end
end
trocla-0.6.0/lib/trocla/formats/md5crypt.rb 0000644 0000041 0000041 00000000270 14756064121 020637 0 ustar www-data www-data # salted crypt
class Trocla::Formats::Md5crypt < Trocla::Formats::Base
def format(plain_password, options = {})
plain_password.crypt('$1$' << Trocla::Util.salt << '$')
end
end
trocla-0.6.0/lib/trocla/formats/sshkey.rb 0000644 0000041 0000041 00000002112 14756064121 020373 0 ustar www-data www-data require 'sshkey'
class Trocla::Formats::Sshkey < Trocla::Formats::Base
expensive true
def format(plain_password,options={})
if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY/m)
# Import, validate ssh key
begin
sshkey = ::SSHKey.new(plain_password)
rescue Exception => e
raise "SSH key import failed: #{e.message}"
end
return sshkey.private_key + sshkey.ssh_public_key
end
type = options['type'] || 'rsa'
bits = options['bits'] || 2048
begin
sshkey = ::SSHKey.generate(
type: type, bits: bits,
comment: options['comment'],
passphrase: options['passphrase']
)
rescue Exception => e
raise "SSH key creation failed: #{e.message}"
end
sshkey.private_key + sshkey.ssh_public_key
end
def render(output, render_options = {})
if render_options['privonly']
::SSHKey.new(output).private_key
elsif render_options['pubonly']
::SSHKey.new(output).ssh_public_key
else
super(output, render_options)
end
end
end
trocla-0.6.0/lib/trocla/formats/sha512crypt.rb 0000644 0000041 0000041 00000000273 14756064121 021160 0 ustar www-data www-data # salted crypt
class Trocla::Formats::Sha512crypt < Trocla::Formats::Base
def format(plain_password, options = {})
plain_password.crypt('$6$' << Trocla::Util.salt << '$')
end
end
trocla-0.6.0/lib/trocla/formats/sha1.rb 0000644 0000041 0000041 00000000327 14756064121 017727 0 ustar www-data www-data 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
trocla-0.6.0/lib/trocla/encryptions.rb 0000644 0000041 0000041 00000002463 14756064121 020000 0 ustar www-data www-data # frozen_string_literal: true
# Trocla::Encryptions
class Trocla::Encryptions
# Base
class Base
attr_reader :trocla, :config
def initialize(config, trocla)
@trocla = trocla
@config = config
end
def encrypt(_)
raise NoMethodError.new("#{self.class.name} needs to implement 'encrypt()'")
end
def decrypt(_)
raise NoMethodError.new("#{self.class.name} needs to implement 'decrypt()'")
end
end
class << self
def [](enc)
encryptions[enc.to_s.downcase]
end
def all
Dir[path '*'].collect do |enc|
File.basename(enc, '.rb').downcase
end
end
def available?(encryption)
all.include?(encryption.to_s.downcase)
end
private
def encryptions
@@encryptions ||= Hash.new do |hash, encryption|
encryption = encryption.to_s.downcase
if File.exist?(path encryption)
require "trocla/encryptions/#{encryption}"
class_name = "Trocla::Encryptions::#{encryption.capitalize}"
hash[encryption] = (eval class_name)
else
raise "Encryption #{encryption} is not supported!"
end
end
end
def path(encryption)
File.expand_path(
File.join(File.dirname(__FILE__), 'encryptions', "#{encryption}.rb")
)
end
end
end
trocla-0.6.0/lib/trocla/version.rb 0000644 0000041 0000041 00000001014 14756064121 017077 0 ustar www-data www-data # encoding: utf-8
class Trocla
# VERSION
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
trocla-0.6.0/lib/trocla/stores.rb 0000644 0000041 0000041 00000001536 14756064121 016742 0 ustar www-data www-data require 'trocla/store'
# store management
class Trocla::Stores
class << self
def [](store)
stores[store.to_s.downcase]
end
def all
@all ||= Dir[path '*'].collect do |store|
File.basename(store, '.rb').downcase
end
end
def available?(store)
all.include?(store.to_s.downcase)
end
private
def stores
@@stores ||= Hash.new do |hash, store|
store = store.to_s.downcase
if File.exist?(path(store))
require "trocla/stores/#{store}"
class_name = "Trocla::Stores::#{store.capitalize}"
hash[store] = (eval class_name)
else
raise "Store #{store} is not supported!"
end
end
end
def path(store)
File.expand_path(
File.join(File.dirname(__FILE__), 'stores', "#{store}.rb")
)
end
end
end
trocla-0.6.0/LICENSE.txt 0000644 0000041 0000041 00000001253 14756064121 014663 0 ustar www-data www-data Trocla - a simple password generator and storage
Copyright (C) 2011-2015 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 .
trocla-0.6.0/spec/ 0000755 0000041 0000041 00000000000 14756064121 013771 5 ustar www-data www-data trocla-0.6.0/spec/data/ 0000755 0000041 0000041 00000000000 14756064121 014702 5 ustar www-data www-data trocla-0.6.0/spec/data/.keep 0000644 0000041 0000041 00000000000 14756064121 015615 0 ustar www-data www-data trocla-0.6.0/spec/spec_helper.rb 0000644 0000041 0000041 00000030015 14756064121 016606 0 ustar www-data www-data $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'jruby' if Object.const_defined?(:RUBY_ENGINE) and RUBY_ENGINE == 'jruby'
require 'rspec'
require 'rspec/pending_for'
require 'yaml'
require 'trocla'
# Requires supporting files with custom matchers and macros, etc,
# in ./support/ and its subdirectories.
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
RSpec.shared_examples "encryption_basics" do
describe 'storing' do
it "random passwords" do
expect(@trocla.password('random1', 'plain').length).to eql(16)
end
it "long random passwords" do
expect(@trocla.set_password('random1_long','plain',4096.times.collect{|s| 'x' }.join('')).length).to eql(4096)
end
end
describe 'retrieve' do
it "random passwords" do
stored = @trocla.password('random1', 'plain')
retrieved = @trocla.password('random1', 'plain')
retrieved_again = @trocla.password('random1', 'plain')
expect(retrieved).to eql(stored)
expect(retrieved_again).to eql(stored)
expect(retrieved_again).to eql(retrieved)
end
it "encrypted passwords" do
@trocla.set_password('some_pass', 'plain', 'super secret')
expect(@trocla.get_password('some_pass', 'plain')).to eql('super secret')
end
it "resets passwords" do
@trocla.set_password('some_pass', 'plain', 'super secret')
expect(@trocla.reset_password('some_pass', 'plain')).not_to eql('super secret')
end
end
describe 'deleting' do
it "plain" do
@trocla.set_password('some_pass', 'plain', 'super secret')
expect(@trocla.delete_password('some_pass', 'plain')).to eql('super secret')
end
it "delete formats" do
plain = @trocla.password('some_mysqlpass', 'plain')
mysql = @trocla.password('some_mysqlpass', 'mysql')
expect(@trocla.delete_password('some_mysqlpass', 'mysql')).to eql(mysql)
expect(@trocla.delete_password('some_mysqlpass', 'plain')).to eql(plain)
expect(@trocla.get_password('some_mysqlpass','plain')).to be_nil
expect(@trocla.get_password('some_mysqlpass','mysql')).to be_nil
end
it "all passwords" do
plain = @trocla.password('some_mysqlpass', 'plain')
mysql = @trocla.password('some_mysqlpass', 'mysql')
deleted = @trocla.delete_password('some_mysqlpass')
expect(deleted).to be_a_kind_of(Hash)
expect(deleted['plain']).to eql(plain)
expect(deleted['mysql']).to eql(mysql)
end
end
end
RSpec.shared_examples "verify_encryption" do
it "does not store plaintext passwords" do
@trocla.set_password('noplain', 'plain', 'plaintext_password')
expect(File.readlines(trocla_yaml_file).grep(/plaintext_password/)).to be_empty
end
it "makes sure identical passwords do not match when stored" do
@trocla.set_password('one_key', 'plain', 'super secret')
@trocla.set_password('another_key', 'plain', 'super secret')
yaml = YAML.load_file(trocla_yaml_file)
expect(yaml['one_key']['plain']).not_to eq(yaml['another_key']['plain'])
end
end
RSpec.shared_examples 'store_validation' do |store|
describe '.get' do
it { expect(store.get('some_key','plain')).to be_nil }
end
describe '.set' do
it 'stores nil values' do
store.set('some_nil_value','plain',nil)
expect(store.get('some_nil_value','plain')).to be_nil
end
it 'stores plain format' do
store.set('some_value','plain','value')
expect(store.get('some_value','plain')).to eql('value')
end
it 'stores other formats' do
store.set('some_value','foo','bla')
expect(store.get('some_value','foo')).to eql('bla')
end
it 'resets other formats on setting plain' do
store.set('some_value','foo','bla')
store.set('some_value','plain','value')
expect(store.get('some_value','plain')).to eql('value')
expect(store.get('some_value','foo')).to be_nil
end
end
describe '.delete' do
it { expect(store.delete('something','foo')).to be_nil }
it { expect(store.delete('something')).to be_empty }
it 'deletes the value of a format' do
store.set('some_value','foo','bla')
expect(store.delete('some_value','foo')).to eql('bla')
expect(store.get('some_value','foo')).to be_nil
end
it 'deletes only the value of a format' do
store.set('some_value','plain','value')
store.set('some_value','foo','bla')
expect(store.delete('some_value','plain')).to eql('value')
expect(store.get('some_value','plain')).to be_nil
expect(store.get('some_value','foo')).to eql('bla')
end
it 'deletes all values without a format' do
store.set('some_value','plain','value')
store.set('some_value','foo','bla')
hash = store.delete('some_value')
expect(hash).to be_a_kind_of(Hash)
expect(hash['plain']).to eql('value')
expect(hash['foo']).to eql('bla')
expect(store.get('some_value','plain')).to be_nil
expect(store.get('some_value','foo')).to be_nil
end
end
describe 'expiration' do
it 'will not return an expired key' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 3
expect(store.get('some_expiring_value','plain')).to be_nil
end
it 'increases expiration when setting anything for that key' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 1
store.set('some_expiring_value','bla','bla_to_be_expired',{ 'expires' => 3 })
sleep 2
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 2
expect(store.get('some_expiring_value','plain')).to be_nil
end
it 'keeps expiration when setting another value' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
store.set('some_expiring_value','foo','to_be_expired_foo')
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 3
expect(store.get('some_expiring_value','plain')).to be_nil
expect(store.get('some_expiring_value','foo')).to be_nil
end
it 'setting plain clears everything including expiration' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
sleep 1
store.set('some_expiring_value','plain','to_be_expired2')
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
end
it 'extends expiration when setting another value' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 4 })
sleep 2
store.set('some_expiring_value','foo','to_be_expired_foo')
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 2
expect(store.get('some_expiring_value','plain')).to be_nil
end
it 'extends expiration when deleting a format' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 4 })
store.set('some_expiring_value','foo','to_be_expired2')
sleep 2
expect(store.delete('some_expiring_value','foo')).to eql('to_be_expired2')
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 2
expect(store.get('some_expiring_value','plain')).to be_nil
end
it 'keeps expiration although we\'re fetching a value' do
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 3 })
sleep 2
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
sleep 2
expect(store.get('some_expiring_value','plain')).to be_nil
end
it 'readding a value with an expiration makes it expiring in the future' do
store.set('some_expiring_value','plain','to_be_expired')
store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
sleep 3
expect(store.get('some_expiring_value','plain')).to be_nil
end
it 'setting an expires of false removes expiration' do
store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => false })
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
end
it 'setting an expires of 0 removes expiration' do
store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 0 })
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
end
it 'setting an expires of false removes expiration even if it\'s for a different format' do
store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
store.set('some_expiring_value','foo','to_be_expired_foo',{ 'expires' => false })
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
expect(store.get('some_expiring_value','foo')).to eql('to_be_expired_foo')
end
it 'setting an expires of 0 removes expiration even if it\'s for a different format' do
store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
store.set('some_expiring_value','foo','to_be_expired_foo',{ 'expires' => 0 })
sleep 3
expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
expect(store.get('some_expiring_value','foo')).to eql('to_be_expired_foo')
end
end
end
def default_config
@default_config ||= begin
config_path = [
File.expand_path(base_dir+'/lib/trocla/default_config.yaml'),
File.expand_path(File.dirname($LOADED_FEATURES.grep(/trocla.rb/)[0])+'/trocla/default_config.yaml'),
].find { |p| File.exist?(p) }
YAML.load(File.read(config_path))
end
end
def test_config
@test_config ||= default_config.merge({
'store' => :memory,
})
end
def test_config_persistent
@test_config_persistent ||= default_config.merge({
'store_options' => {
'adapter' => :YAML,
'adapter_options' => {
:file => trocla_yaml_file
},
},
})
end
def ssl_test_config
@ssl_config ||= test_config_persistent.merge({
'encryption' => :ssl,
'encryption_options' => {
:private_key => data_dir('trocla.key'),
:public_key => data_dir('trocla.pub'),
},
})
end
def hooks_config
@hooks_config ||= test_config.merge({
'hooks' => {
'set' => {
'set_test_hook' => File.expand_path(File.join(base_dir,'spec/fixtures/set_test_hook.rb'))
},
'delete' => {
'delete_test_hook' => File.expand_path(File.join(base_dir,'spec/fixtures/delete_test_hook.rb'))
}
}
})
end
def base_dir
File.dirname(__FILE__)+'/../'
end
def data_dir(file = nil)
File.expand_path(File.join(base_dir, 'spec/data', file))
end
def trocla_yaml_file
data_dir('trocla_store.yaml')
end
def generate_ssl_keys
require 'openssl'
rsa_key = OpenSSL::PKey::RSA.new(4096)
File.open(data_dir('trocla.key'), 'w') { |f| f.write(rsa_key.to_pem) }
File.open(data_dir('trocla.pub'), 'w') { |f| f.write(rsa_key.public_key.to_pem) }
end
def remove_ssl_keys
File.unlink(data_dir('trocla.key'))
File.unlink(data_dir('trocla.pub'))
end
def remove_yaml_store
File.unlink(trocla_yaml_file)
end
class Trocla::Formats::Sleep < Trocla::Formats::Base
def format(plain_password,options={})
sleep options['sleep'] ||= 0
(options['sleep'] + 1 ).times.collect{ plain_password }.join(' ')
end
end
trocla-0.6.0/spec/fixtures/ 0000755 0000041 0000041 00000000000 14756064121 015642 5 ustar www-data www-data trocla-0.6.0/spec/fixtures/set_test_hook.rb 0000644 0000041 0000041 00000000324 14756064121 021040 0 ustar www-data www-data class Trocla
module Hooks
def self.set_test_hook(trocla, key, format, options)
self.set_messages << "#{key}_#{format}"
end
def self.set_messages
@set_messages ||= []
end
end
end
trocla-0.6.0/spec/fixtures/delete_test_hook.rb 0000644 0000041 0000041 00000000340 14756064121 021505 0 ustar www-data www-data class Trocla
module Hooks
def self.delete_test_hook(trocla, key, format, options)
self.delete_messages << "#{key}_#{format}"
end
def self.delete_messages
@delete_messages ||= []
end
end
end
trocla-0.6.0/spec/trocla_spec.rb 0000644 0000041 0000041 00000030340 14756064121 016614 0 ustar www-data www-data # -- encoding : utf-8 --
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "Trocla" do
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
end
context 'in normal usage with' do
before(:each) do
@trocla = Trocla.new
@trocla.password('init','plain')
end
describe "password" do
it "generates random passwords by default" do
expect(@trocla.password('random1','plain')).not_to eq(@trocla.password('random2','plain'))
end
it "generates passwords of length #{default_config['options']['length']}" do
expect(@trocla.password('random1','plain').length).to eq(default_config['options']['length'])
end
Trocla::Formats.all.each do |format|
if format == 'wireguard'
require 'open3'
before(:each) do
allow(Open3).to receive(:popen3).with('wg genkey').and_yield(nil, StringIO.new('key'), nil, nil)
allow(Open3).to receive(:popen3).with('wg pubkey').and_yield(StringIO.new, StringIO.new('key'), nil, nil)
end
end
describe "#{format} password format" do
it "retursn a password hashed in the #{format} format" do
expect(@trocla.password('some_test',format,format_options[format])).not_to be_empty
end
it "returns the same hashed for the #{format} format on multiple invocations" do
expect(round1=@trocla.password('some_test',format,format_options[format])).not_to be_empty
expect(@trocla.password('some_test',format,format_options[format])).to eq(round1)
end
it "also stores the plain password by default" do
pwd = @trocla.password('some_test','plain')
expect(pwd).not_to be_empty
expect(pwd.length).to eq(16)
end
end
end
Trocla::Formats.all.reject{|f| f == 'plain' }.each do |format|
it "raises an exception if not a random password is asked but plain password is not present for format #{format}" do
expect{@trocla.password('not_random',format, 'random' => false)}.to raise_error(/Password must be present as plaintext/)
end
end
describe 'with profiles' do
it 'raises an exception on unknown profile' do
expect{@trocla.password('no profile known','plain',
'profiles' => 'unknown_profile') }.to raise_error(/No such profile unknown_profile defined/)
end
it 'takes a profile and merge its options' do
pwd = @trocla.password('some_test','plain', 'profiles' => 'rootpw')
expect(pwd).not_to be_empty
expect(pwd.length).to eq(32)
expect(pwd).to_not match(/[={}\[\]\?%\*()&!]+/)
end
it 'is possible to combine profiles but first profile wins' do
pwd = @trocla.password('some_test1','plain', 'profiles' => ['rootpw','login'])
expect(pwd).not_to be_empty
expect(pwd.length).to eq(32)
expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
end
it 'is possible to combine profiles but first profile wins 2' do
pwd = @trocla.password('some_test2','plain', 'profiles' => ['login','mysql'])
expect(pwd).not_to be_empty
expect(pwd.length).to eq(16)
expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
end
it 'is possible to combine profiles but first profile wins 3' do
# mysql profile uses a 32 long random pwd with shell safe characters
# and we want to use a fixed random str here https://github.com/duritong/trocla/issues/55
allow(Trocla::Util).to receive(:random_str).with(32,'shellsafe') { "jmNi6+7dsUn@H?vfbXCq=ULEGPW,u:hu" }
pwd = @trocla.password('some_test3','plain', 'profiles' => ['mysql','login'])
expect(pwd).not_to be_empty
expect(pwd.length).to eq(32)
expect(pwd).to match(/[+%\/@=\?_.,:]+/)
end
end
end
describe "set_password" do
it "resets hashed passwords on a new plain password" do
expect(@trocla.password('set_test','mysql')).not_to be_empty
expect(@trocla.get_password('set_test','mysql')).not_to be_nil
expect(old_plain=@trocla.password('set_test','mysql')).not_to be_empty
expect(@trocla.set_password('set_test','plain','foobar')).not_to eq(old_plain)
expect(@trocla.get_password('set_test','mysql')).to be_nil
end
it "otherwise updates only the hash" do
expect(mysql = @trocla.password('set_test2','mysql')).not_to be_empty
expect(md5crypt = @trocla.password('set_test2','md5crypt')).not_to be_empty
expect(plain = @trocla.get_password('set_test2','plain')).not_to be_empty
expect(new_mysql = @trocla.set_password('set_test2','mysql','foo')).not_to eql(mysql)
expect(@trocla.get_password('set_test2','mysql')).to eq(new_mysql)
expect(@trocla.get_password('set_test2','md5crypt')).to eq(md5crypt)
expect(@trocla.get_password('set_test2','plain')).to eq(plain)
end
it 'is able to set password with umlauts and other UTF-8 charcters' do
expect(myumlaut = @trocla.set_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
expect(@trocla.get_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
end
end
describe "reset_password" do
it "resets a password" do
plain1 = @trocla.password('reset_pwd','plain')
plain2 = @trocla.reset_password('reset_pwd','plain')
expect(plain1).not_to eq(plain2)
end
it "does not reset other formats" do
expect(mysql = @trocla.password('reset_pwd2','mysql')).not_to be_empty
expect(md5crypt1 = @trocla.password('reset_pwd2','md5crypt')).not_to be_empty
expect(md5crypt2 = @trocla.reset_password('reset_pwd2','md5crypt')).not_to be_empty
expect(md5crypt2).not_to eq(md5crypt1)
expect(@trocla.get_password('reset_pwd2','mysql')).to eq(mysql)
end
end
describe "search_key" do
it "search a specific key" do
keys = ['search_key','search_key1','key_search','key_search2']
keys.each do |k|
@trocla.password(k,'plain')
end
expect(@trocla.search_key('search_key1').length).to eq(1)
end
it "ensure search regex is ok" do
keys = ['search_key2','search_key3','key_search2','key_search4']
keys.each do |k|
@trocla.password(k,'plain')
end
expect(@trocla.search_key('key').length).to eq(4)
expect(@trocla.search_key('^search').length).to eq(2)
expect(@trocla.search_key('ch.*3').length).to eq(1)
expect(@trocla.search_key('ch.*[3-4]$').length).to eq(2)
expect(@trocla.search_key('ch.*1')).to be_nil
end
end
describe "list_format" do
it "list available formats for key" do
formats = ['plain','mysql']
formats.each do |f|
@trocla.password('list_key',f)
end
expect(@trocla.available_format('list_key')).to eq(formats)
end
it "no return if key doesn't exist" do
expect(@trocla.available_format('list_key1')).to be_nil
end
end
describe "delete_password" do
it "deletes all passwords if no format is given" do
expect(@trocla.password('delete_test1','mysql')).not_to be_nil
expect(@trocla.get_password('delete_test1','plain')).not_to be_nil
@trocla.delete_password('delete_test1')
expect(@trocla.get_password('delete_test1','plain')).to be_nil
expect(@trocla.get_password('delete_test1','mysql')).to be_nil
end
it "deletes only a given format" do
expect(@trocla.password('delete_test2','mysql')).not_to be_nil
expect(@trocla.get_password('delete_test2','plain')).not_to be_nil
@trocla.delete_password('delete_test2','plain')
expect(@trocla.get_password('delete_test2','plain')).to be_nil
expect(@trocla.get_password('delete_test2','mysql')).not_to be_nil
end
it "deletes only a given non-plain format" do
expect(@trocla.password('delete_test3','mysql')).not_to be_nil
expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
@trocla.delete_password('delete_test3','mysql')
expect(@trocla.get_password('delete_test3','mysql')).to be_nil
expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
end
end
context 'concurrent access' do
context 'on expensive flagged formats' do
before(:each) do
expect(Trocla::Formats).to receive(:[]).with('sleep').at_least(:once).and_return(Trocla::Formats::Sleep)
expect(Trocla::Formats::Sleep).to receive(:expensive?).at_least(:once).and_return(true)
expect(Trocla::Formats).to receive(:available?).with('sleep').at_least(:once).and_return(true)
end
it 'should not overwrite a value if it takes longer' do
t1 = Thread.new{ @trocla.password('threadpwd','sleep','sleep' => 4) }
t2 = Thread.new{ @trocla.password('threadpwd','sleep','sleep' => 1) }
pwd1 = t1.value
pwd2 = t2.value
real_value = @trocla.password('threadpwd','sleep')
# as t2 finished first this should win
expect(pwd1).to eql(pwd2)
expect(real_value).to eql(pwd1)
expect(real_value).to eql(pwd2)
end
end
context 'on inexpensive flagged formats' do
before(:each) do
expect(Trocla::Formats).to receive(:[]).with('sleep').at_least(:once).and_return(Trocla::Formats::Sleep)
expect(Trocla::Formats::Sleep).to receive(:expensive?).at_least(:once).and_return(false)
expect(Trocla::Formats).to receive(:available?).with('sleep').at_least(:once).and_return(true)
end
it 'should not overwrite a value if it takes longer' do
t1 = Thread.new{ @trocla.password('threadpwd_inexp','sleep','sleep' => 4) }
t2 = Thread.new{ @trocla.password('threadpwd_inexp','sleep','sleep' => 1) }
pwd1 = t1.value
pwd2 = t2.value
real_value = @trocla.password('threadpwd_inexp','sleep')
# as t2 finished first but the format is inexpensive it gets overwritten
expect(pwd1).not_to eql(pwd2)
expect(real_value).to eql(pwd1)
expect(real_value).not_to eql(pwd2)
end
end
context 'real world example' do
it 'should store the quicker one' do
t1 = Thread.new{ @trocla.password('threadpwd_real','bcrypt','cost' => 17) }
t2 = Thread.new{ @trocla.password('threadpwd_real','bcrypt') }
pwd1 = t1.value
pwd2 = t2.value
real_value = @trocla.password('threadpwd_real','bcrypt')
# t2 should still win but both should be the same
expect(pwd1).to eql(pwd2)
expect(real_value).to eql(pwd1)
expect(real_value).to eql(pwd2)
end
it 'should store the quicker one test 2' do
t1 = Thread.new{ @trocla.password('my_shiny_selfsigned_ca', 'x509', {
'CN' => 'This is my self-signed certificate',
'become_ca' => false,
}) }
t2 = Thread.new{ @trocla.password('my_shiny_selfsigned_ca', 'x509', {
'CN' => 'This is my self-signed certificate',
'become_ca' => false,
}) }
cert1 = t1.value
cert2 = t2.value
real_value = @trocla.password('my_shiny_selfsigned_ca','x509')
# t2 should still win but both should be the same
expect(cert1).to eql(cert2)
expect(real_value).to eql(cert1)
expect(real_value).to eql(cert2)
end
end
end
end
context 'with .open' do
it 'closes the connection with a block' do
expect_any_instance_of(Trocla::Stores::Memory).to receive(:close)
Trocla.open{|t|
t.password('plain_open','plain')
}
end
it 'keeps the connection without a block' do
expect_any_instance_of(Trocla::Stores::Memory).not_to receive(:close)
Trocla.open.password('plain_open','plain')
end
end
def format_options
@format_options ||= Hash.new({}).merge({
'pgsql' => { 'username' => 'test' },
'x509' => { 'CN' => 'test' },
})
end
end
describe "VERSION" do
it "returns a version" do
expect(Trocla::VERSION::STRING).not_to be_empty
end
end
trocla-0.6.0/spec/trocla/ 0000755 0000041 0000041 00000000000 14756064121 015255 5 ustar www-data www-data trocla-0.6.0/spec/trocla/encryptions/ 0000755 0000041 0000041 00000000000 14756064121 017632 5 ustar www-data www-data trocla-0.6.0/spec/trocla/encryptions/none_spec.rb 0000644 0000041 0000041 00000001143 14756064121 022127 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
describe "Trocla::Encryptions::None" do
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config_persistent)
@trocla = Trocla.new
end
after(:each) do
remove_yaml_store
end
describe "none" do
include_examples 'encryption_basics'
it "stores plaintext passwords" do
@trocla.set_password('noplain', 'plain', 'plaintext_password')
expect(File.readlines(trocla_yaml_file).grep(/plaintext_password/)).to eq([" plain: plaintext_password\n"])
end
end
end
trocla-0.6.0/spec/trocla/encryptions/ssl_spec.rb 0000644 0000041 0000041 00000000772 14756064121 022000 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
describe "Trocla::Encryptions::Ssl" do
before(:all) do
generate_ssl_keys
end
after(:all) do
remove_ssl_keys
end
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(ssl_test_config)
@trocla = Trocla.new
end
after(:each) do
remove_yaml_store
end
describe "encrypt" do
include_examples 'encryption_basics'
include_examples 'verify_encryption'
end
end
trocla-0.6.0/spec/trocla/util_spec.rb 0000644 0000041 0000041 00000002715 14756064121 017576 0 ustar www-data www-data 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 "is random" do
expect(Trocla::Util.send(m)).not_to eq(Trocla::Util.send(m))
end
it "defaults to length #{length}" do
expect(Trocla::Util.send(m).length).to eq(length)
end
it "is possible to change length" do
expect(Trocla::Util.send(m,8).length).to eq(8)
expect(Trocla::Util.send(m,32).length).to eq(32)
expect(Trocla::Util.send(m,1).length).to eq(1)
end
end
end
describe :numeric_generator do
10.times.each do |i|
it "creates random numeric password #{i}" do
expect(Trocla::Util.random_str(12, 'numeric')).to match(/^[0-9]{12}$/)
end
end
end
describe :hexadecimal_generator do
10.times.each do |i|
it "creates random hexadecimal password #{i}" do
expect(Trocla::Util.random_str(12, 'hexadecimal')).to match(/^[0-9a-f]{12}$/)
end
end
end
describe :typesafe_generator do
10.times.each do |i|
it "creates random typesafe password #{i}" do
expect(Trocla::Util.random_str(12, 'typesafe')).to match(/^[1-9a-hj-km-xA-HJ-KM-X]{12}$/)
end
end
end
describe :salt do
10.times.each do |i|
it "contains only characters and numbers #{i}" do
expect(Trocla::Util.salt).to match(/^[a-z0-9]+$/i)
end
end
end
end
trocla-0.6.0/spec/trocla/store/ 0000755 0000041 0000041 00000000000 14756064121 016411 5 ustar www-data www-data trocla-0.6.0/spec/trocla/store/memory_spec.rb 0000644 0000041 0000041 00000000331 14756064121 021255 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require 'trocla/stores/memory'
describe Trocla::Stores::Memory do
include_examples 'store_validation', Trocla::Stores::Memory.new({},nil)
end
trocla-0.6.0/spec/trocla/store/moneta_spec.rb 0000644 0000041 0000041 00000000374 14756064121 021237 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require 'trocla/stores/moneta'
describe Trocla::Stores::Moneta do
include_examples 'store_validation', Trocla::Stores::Moneta.new({'adapter' => :Memory},{:expires => true})
end
trocla-0.6.0/spec/trocla/hooks_spec.rb 0000644 0000041 0000041 00000002677 14756064121 017753 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe "Trocla::Hooks::Runner" do
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(hooks_config)
@trocla = Trocla.new
end
after(:each) do
Trocla::Hooks.set_messages.clear
Trocla::Hooks.delete_messages.clear
end
describe 'running hooks' do
describe 'setting password' do
it "calls the set hook" do
@trocla.password('random1', 'plain')
expect(Trocla::Hooks.set_messages.length).to eql(1)
expect(Trocla::Hooks.delete_messages.length).to eql(0)
expect(Trocla::Hooks.set_messages.first).to eql("random1_plain")
end
end
describe 'deleting password' do
it "calls the delete hook" do
@trocla.delete_password('random1', 'plain')
expect(Trocla::Hooks.delete_messages.length).to eql(1)
expect(Trocla::Hooks.set_messages.length).to eql(0)
expect(Trocla::Hooks.delete_messages.first).to eql("random1_plain")
end
end
describe 'reset password' do
it "calls the delete and set hook" do
@trocla.reset_password('random1', 'plain')
expect(Trocla::Hooks.set_messages.length).to eql(1)
expect(Trocla::Hooks.set_messages.first).to eql("random1_plain")
expect(Trocla::Hooks.delete_messages.length).to eql(1)
expect(Trocla::Hooks.delete_messages.first).to eql("random1_plain")
end
end
end
end
trocla-0.6.0/spec/trocla/formats/ 0000755 0000041 0000041 00000000000 14756064121 016730 5 ustar www-data www-data trocla-0.6.0/spec/trocla/formats/sshkey_spec.rb 0000644 0000041 0000041 00000003327 14756064121 021602 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
describe "Trocla::Format::Sshkey" do
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
@trocla = Trocla.new
end
let(:sshkey_options) do
{
'type' => 'rsa',
'bits' => 4096,
'comment' => 'My ssh key'
}
end
describe "sshkey" do
it "is able to create an ssh keypair without options" do
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', {})
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
expect(sshkey).to match(/ssh-/)
end
it "is able to create an ssh keypair with options" do
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', sshkey_options)
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
expect(sshkey).to match(/ssh-/)
expect(sshkey).to end_with('My ssh key')
end
it 'supports fetching only the priv key' do
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'privonly' => true }})
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
expect(sshkey).not_to match(/ssh-/)
end
it 'supports fetching only the pub key' do
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'pubonly' => true }})
expect(sshkey).to start_with('ssh-rsa')
expect(sshkey).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
end
it "is able to create an ssh keypair with a passphrase" do
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'passphrase' => 'spec' })
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
expect(sshkey).to match(/ssh-/)
end
end
end
trocla-0.6.0/spec/trocla/formats/pgsql_spec.rb 0000644 0000041 0000041 00000001370 14756064121 021416 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
describe 'Trocla::Format::Pgsql' do
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
@trocla = Trocla.new
end
describe 'default pgsql' do
it 'create a pgsql password keypair without options in sha256' do
pass = @trocla.password('pgsql_password_sh256', 'pgsql', {})
expect(pass).to match(/^SCRAM-SHA-256\$(.*):(.*)\$(.*):/)
end
end
describe 'pgsql in md5 encode' do
it 'create a pgsql password in md5 encode' do
pass = @trocla.password(
'pgsql_password_md5', 'pgsql',
{ 'username' => 'toto', 'encode' => 'md5' }
)
expect(pass).to match(/^md5/)
end
end
end
trocla-0.6.0/spec/trocla/formats/x509_spec.rb 0000644 0000041 0000041 00000043211 14756064121 020775 0 ustar www-data www-data require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require 'date'
describe "Trocla::Format::X509" do
before(:each) do
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
@trocla = Trocla.new
end
let(:ca_options) do
{
'CN' => 'This is my self-signed certificate which doubles as CA',
'become_ca' => true,
}
end
let(:cert_options) do
{
'ca' => 'my_shiny_selfsigned_ca',
'subject' => '/C=ZZ/O=Trocla Inc./CN=test/emailAddress=example@example.com',
}
end
def verify(ca,cert)
store = OpenSSL::X509::Store.new
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
Array(ca).each do |c|
store.add_cert(c)
end
store.verify(cert)
end
describe "x509 selfsigned" do
it "is able to create self signed cert without being a ca by default" do
cert_str = @trocla.password('my_shiny_selfsigned_ca', 'x509', {
'CN' => 'This is my self-signed certificate',
'become_ca' => false,
})
cert = OpenSSL::X509::Certificate.new(cert_str)
# selfsigned?
expect(cert.issuer.to_s).to eq(cert.subject.to_s)
# default size
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
# it's a self signed cert and NOT a CA
# Change of behavior on openssl side: https://github.com/openssl/openssl/issues/15146
validates_self_even_if_no_ca = RUBY_ENGINE == 'jruby' ? true : Gem::Version.new(%x{openssl version}.split(' ')[1]) > Gem::Version.new('1.1.1g')
expect(verify(cert,cert)).to be validates_self_even_if_no_ca
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
expect(v).to eq('CA:FALSE')
# we want to include only CNs that look like a DNS name
expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }).to be_nil
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).not_to match(/Certificate Sign/)
expect(ku).not_to match(/CRL Sign/)
end
it "is able to create a self signed cert that is a CA" do
ca_str = @trocla.password('my_shiny_selfsigned_ca', 'x509', ca_options)
ca = OpenSSL::X509::Certificate.new(ca_str)
# selfsigned?
expect(ca.issuer.to_s).to eq(ca.subject.to_s)
expect((Date.parse(ca.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify(ca,ca)).to be true
v = ca.extensions.find{|e| e.oid == 'basicConstraints' }.value
expect(v).to eq('CA:TRUE')
ku = ca.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).to match(/Certificate Sign/)
expect(ku).to match(/CRL Sign/)
end
it "is able to create a self signed cert without any keyUsage restrictions" do
cert_str = @trocla.password('my_shiny_selfsigned_without restrictions', 'x509', {
'CN' => 'This is my self-signed certificate',
'key_usages' => [],
})
cert = OpenSSL::X509::Certificate.new(cert_str)
# selfsigned?
expect(cert.issuer.to_s).to eq(cert.subject.to_s)
# default size
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
# it's a self signed cert and NOT a CA, but has no keyUsage limitation
expect(verify(cert,cert)).to be true
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
expect(v).to_not eq('CA:TRUE')
expect(cert.extensions.find{|e| e.oid == 'keyUsage' }).to be_nil
end
it "is able to create a self signed cert with custom keyUsage restrictions" do
cert_str = @trocla.password('my_shiny_selfsigned_without restrictions', 'x509', {
'CN' => 'This is my self-signed certificate',
'key_usages' => [ 'cRLSign', ],
})
cert = OpenSSL::X509::Certificate.new(cert_str)
# selfsigned?
expect(cert.issuer.to_s).to eq(cert.subject.to_s)
# default size
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
# it's a self signed cert and NOT a CA, as it's key is restricted to only CRL Sign
expect(verify(cert,cert)).to be false
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
expect(v).to_not eq('CA:TRUE')
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).to match(/CRL Sign/)
expect(ku).not_to match(/Certificate Sign/)
end
end
describe "x509 signed by a ca" do
before(:each) do
ca_str = @trocla.password('my_shiny_selfsigned_ca', 'x509', ca_options)
@ca = OpenSSL::X509::Certificate.new(ca_str)
end
it 'is able to get a cert signed by the ca' do
cert_str = @trocla.password('mycert', 'x509', cert_options)
cert = OpenSSL::X509::Certificate.new(cert_str)
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify(@ca,cert)).to be true
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
expect(v).to eq('CA:FALSE')
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).not_to match(/Certificate Sign/)
expect(ku).not_to match(/CRL Sign/)
end
it 'supports fetching only the key' do
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'keyonly' => true }))
expect(cert_str).not_to match(/-----BEGIN CERTIFICATE-----/)
expect(cert_str).to match(/-----BEGIN RSA PRIVATE KEY-----/)
end
it 'supports fetching only the publickey' do
pkey_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'publickeyonly' => true }))
expect(pkey_str).not_to match(/-----BEGIN CERTIFICATE-----/)
expect(pkey_str).to match(/-----BEGIN PUBLIC KEY-----/)
end
it 'supports fetching only the cert' do
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
end
it 'supports fetching only the cert even a second time' do
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
end
it 'does not simply increment the serial' do
cert_str = @trocla.password('mycert', 'x509', cert_options)
cert1 = OpenSSL::X509::Certificate.new(cert_str)
cert_str = @trocla.password('mycert2', 'x509', cert_options)
cert2 = OpenSSL::X509::Certificate.new(cert_str)
expect(cert1.serial.to_i).not_to eq(1)
expect(cert2.serial.to_i).not_to eq(2)
expect((cert2.serial - cert1.serial).to_i).not_to eq(1)
end
it 'is able to get a cert signed by the ca that is again a ca' do
cert_str = @trocla.password('mycert', 'x509', cert_options.merge({
'become_ca' => true,
}))
cert = OpenSSL::X509::Certificate.new(cert_str)
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify(@ca,cert)).to be true
expect(cert.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:TRUE')
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).to match(/Certificate Sign/)
expect(ku).to match(/CRL Sign/)
end
it 'supports simple name constraints for CAs' do
ca2_str = @trocla.password('mycert_with_nc', 'x509', cert_options.merge({
'name_constraints' => ['example.com','bla.example.net'],
'become_ca' => true,
}))
ca2 = OpenSSL::X509::Certificate.new(ca2_str)
expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
expect(verify(@ca,ca2)).to be true
end
expect(ca2.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:TRUE')
ku = ca2.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).to match(/Certificate Sign/)
expect(ku).to match(/CRL Sign/)
nc = ca2.extensions.find{|e| e.oid == 'nameConstraints' }.value
pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
expect(nc).to match(/Permitted:\n DNS:example.com\n DNS:bla.example.net/)
end
valid_cert_str = @trocla.password('myvalidexamplecert','x509', {
'subject' => '/C=ZZ/O=Trocla Inc./CN=foo.example.com/emailAddress=example@example.com',
'ca' => 'mycert_with_nc'
})
valid_cert = OpenSSL::X509::Certificate.new(valid_cert_str)
expect(valid_cert.issuer.to_s).to eq(ca2.subject.to_s)
expect(verify([@ca,ca2],valid_cert)).to be true
expect((Date.parse(valid_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
false_cert_str = @trocla.password('myfalseexamplecert','x509', {
'subject' => '/C=ZZ/O=Trocla Inc./CN=foo.example.net/emailAddress=example@example.com',
'ca' => 'mycert_with_nc'
})
false_cert = OpenSSL::X509::Certificate.new(false_cert_str)
expect(false_cert.issuer.to_s).to eq(ca2.subject.to_s)
expect(verify([@ca,ca2],false_cert)).to be false
expect((Date.parse(false_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
end
it 'supports simple name constraints for CAs with leading dots' do
ca2_str = @trocla.password('mycert_with_nc', 'x509', cert_options.merge({
'name_constraints' => ['.example.com','.bla.example.net'],
'become_ca' => true,
}))
ca2 = OpenSSL::X509::Certificate.new(ca2_str)
expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
expect(verify(@ca,ca2)).to be true
end
expect(ca2.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:TRUE')
ku = ca2.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).to match(/Certificate Sign/)
expect(ku).to match(/CRL Sign/)
nc = ca2.extensions.find{|e| e.oid == 'nameConstraints' }.value
expect(nc).to match(/Permitted:\n DNS:.example.com\n DNS:.bla.example.net/)
valid_cert_str = @trocla.password('myvalidexamplecert','x509', {
'subject' => '/C=ZZ/O=Trocla Inc./CN=foo.example.com/emailAddress=example@example.com',
'ca' => 'mycert_with_nc'
})
valid_cert = OpenSSL::X509::Certificate.new(valid_cert_str)
expect(valid_cert.issuer.to_s).to eq(ca2.subject.to_s)
expect((Date.parse(valid_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
# workaround broken openssl
if Gem::Version.new(%x{openssl version}.split(' ')[1]) < Gem::Version.new('1.0.2')
skip_for(:engine => 'ruby',:reason => 'NameConstraints verification is broken on older openssl versions https://rt.openssl.org/Ticket/Display.html?id=3562') do
expect(verify([@ca,ca2],valid_cert)).to be true
end
else
expect(verify([@ca,ca2],valid_cert)).to be true
end
false_cert_str = @trocla.password('myfalseexamplecert','x509', {
'subject' => '/C=ZZ/O=Trocla Inc./CN=foo.example.net/emailAddress=example@example.com',
'ca' => 'mycert_with_nc'
})
false_cert = OpenSSL::X509::Certificate.new(false_cert_str)
expect(false_cert.issuer.to_s).to eq(ca2.subject.to_s)
expect((Date.parse(false_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify([@ca,ca2],false_cert)).to be false
end
it 'is able to get a cert signed by the ca that is again a ca that is able to sign certs' do
ca2_str = @trocla.password('mycert_and_ca', 'x509', cert_options.merge({
'become_ca' => true,
}))
ca2 = OpenSSL::X509::Certificate.new(ca2_str)
expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify(@ca,ca2)).to be true
cert2_str = @trocla.password('mycert', 'x509', {
'ca' => 'mycert_and_ca',
'subject' => '/C=ZZ/O=Trocla Inc./CN=test2/emailAddress=example@example.com',
'become_ca' => true,
})
cert2 = OpenSSL::X509::Certificate.new(cert2_str)
expect(cert2.issuer.to_s).to eq(ca2.subject.to_s)
expect((Date.parse(cert2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
skip_for(:engine => 'jruby',:reason => 'Chained CA validation seems to be broken on jruby atm.') do
expect(verify([@ca,ca2],cert2)).to be true
end
end
it 'respects all options' do
co = cert_options.merge({
'hash' => 'sha512',
'keysize' => 2048,
'days' => 3650,
'subject' => nil,
'C' => 'AA',
'ST' => 'Earth',
'L' => 'Here',
'O' => 'SSLTrocla',
'OU' => 'root',
'CN' => 'www.test',
'emailAddress' => 'test@example.com',
'altnames' => [ 'test', 'test1', 'test2', 'test3' ],
})
cert_str = @trocla.password('mycert', 'x509', co)
cert = OpenSSL::X509::Certificate.new(cert_str)
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
['C','ST','L','O','OU','CN'].each do |field|
expect(cert.subject.to_s).to match(/#{field}=#{co[field]}/)
end
expect(cert.subject.to_s).to match(/(Email|emailAddress)=#{co['emailAddress']}/)
expect(cert.signature_algorithm).to eq('sha512WithRSAEncryption')
expect(cert.not_before).to be < Time.now
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(3650)
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
expect(cert.public_key.n.num_bytes * 8).to eq(2048)
expect(verify(@ca,cert)).to be true
skip_for(:engine => 'jruby',:reason => 'subjectAltName represenation is broken in jruby-openssl -> https://github.com/jruby/jruby-openssl/pull/123') do
expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }.value).to eq('DNS:www.test, DNS:test, DNS:test1, DNS:test2, DNS:test3')
end
expect(cert.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:FALSE')
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
expect(ku).not_to match(/Certificate Sign/)
expect(ku).not_to match(/CRL Sign/)
end
it 'shold not add subject alt name on empty array' do
co = cert_options.merge({
'CN' => 'www.test',
'altnames' => []
})
cert_str = @trocla.password('mycert', 'x509', co)
cert = OpenSSL::X509::Certificate.new(cert_str)
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify(@ca,cert)).to be true
expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }).to be_nil
end
it 'prefers full subject of single subject parts' do
co = cert_options.merge({
'C' => 'AA',
'ST' => 'Earth',
'L' => 'Here',
'O' => 'SSLTrocla',
'OU' => 'root',
'CN' => 'www.test',
'emailAddress' => 'test@example.net',
})
cert_str = @trocla.password('mycert', 'x509', co)
cert = OpenSSL::X509::Certificate.new(cert_str)
['C','ST','L','O','OU','CN'].each do |field|
expect(cert.subject.to_s).not_to match(/#{field}=#{co[field]}/)
end
expect(cert.subject.to_s).not_to match(/(Email|emailAddress)=#{co['emailAddress']}/)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(verify(@ca,cert)).to be true
end
it "is able to create a signed cert with custom keyUsage restrictions" do
cert_str = @trocla.password('mycert_without_restrictions', 'x509', cert_options.merge({
'CN' => 'sign only test',
'key_usages' => [ ],
}))
cert = OpenSSL::X509::Certificate.new(cert_str)
# default size
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
expect(verify(@ca,cert)).to be true
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
expect(v).to_not eq('CA:TRUE')
expect(cert.extensions.find{|e| e.oid == 'keyUsage' }).to be_nil
end
end
end
trocla-0.6.0/.rspec 0000644 0000041 0000041 00000000037 14756064121 014154 0 ustar www-data www-data --color --format documentation
trocla-0.6.0/Rakefile 0000644 0000041 0000041 00000002773 14756064121 014515 0 ustar www-data www-data # encoding: utf-8
require 'rubygems'
require 'bundler'
begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end
require 'rake'
require 'jeweler'
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
require 'trocla'
Jeweler::Tasks.new do |gem|
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
gem.name = "trocla"
gem.homepage = "https://tech.immerda.ch/2011/12/trocla-get-hashed-passwords-out-of-puppet-manifests/"
gem.license = "GPLv3"
gem.summary = "Trocla a simple password generator and storage"
gem.description = "Trocla helps you to generate random passwords and to store them in various formats (plain, MD5, bcrypt) for later retrival."
gem.email = "mh+trocla@immerda.ch"
gem.authors = ["mh"]
gem.version = Trocla::VERSION::STRING
# dependencies defined in Gemfile
end
Jeweler::RubygemsDotOrgTasks.new
require 'rspec/core'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList['spec/**/*_spec.rb']
end
RSpec::Core::RakeTask.new(:rcov) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true
end
task :default => :spec
gem 'rdoc'
require 'rdoc/task'
RDoc::Task.new do |rdoc|
version = Trocla::VERSION::STRING
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "trocla #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
trocla-0.6.0/Gemfile 0000644 0000041 0000041 00000001022 14756064121 014325 0 ustar www-data www-data source 'http://rubygems.org'
# Add dependencies required to use your gem here.
# Example:
# gem "activesupport", ">= 2.3.5"
gem 'highline', '~> 2.0.0'
gem 'moneta', '~> 1.0'
if defined?(RUBY_ENGINE) && (RUBY_ENGINE == 'jruby')
gem 'jruby-openssl'
end
gem 'bcrypt'
gem 'sshkey'
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
gem 'rake'
gem 'addressable'
gem 'jeweler', '~> 2.0'
gem 'rdoc'
gem 'rspec'
gem 'rspec-pending_for'
end
trocla-0.6.0/ext/ 0000755 0000041 0000041 00000000000 14756064121 013637 5 ustar www-data www-data trocla-0.6.0/ext/redhat/ 0000755 0000041 0000041 00000000000 14756064121 015106 5 ustar www-data www-data trocla-0.6.0/ext/redhat/rubygem-trocla.spec 0000644 0000041 0000041 00000005764 14756064121 020732 0 ustar www-data www-data # Generated from trocla-0.1.2.gem by gem2rpm -*- rpm-spec -*-
%global gem_name trocla
Name: rubygem-%{gem_name}
Version: 0.3.0
Release: 1%{?dist}
Summary: Trocla a simple password generator and storage
Group: Development/Languages
License: GPLv3
URL: https://tech.immerda.ch/2011/12/trocla-get-hashed-passwords-out-of-puppet-manifests/
Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem
Requires: rubygem-moneta
Requires: rubygem-bcrypt
Requires: rubygem-highline
BuildRequires: rubygem-moneta = 0.7.20
BuildRequires: rubygem-bcrypt
BuildRequires: rubygem-highline
%if 0%{?rhel} >= 7
BuildRequires: ruby(release)
%endif
BuildRequires: rubygems-devel
BuildRequires: ruby
# BuildRequires: rubygem(mocha)
# BuildRequires: rubygem(rspec) => 2.4
# BuildRequires: rubygem(rspec) < 3
# BuildRequires: rubygem(jeweler) => 1.6
# BuildRequires: rubygem(jeweler) < 2
BuildArch: noarch
%description
Trocla helps you to generate random passwords and to store them in various
formats (plain, MD5, bcrypt) for later retrival.
%package doc
Summary: Documentation for %{name}
Group: Documentation
Requires: %{name} = %{version}-%{release}
BuildArch: noarch
%description doc
Documentation for %{name}.
%prep
gem unpack %{SOURCE0}
%setup -q -D -T -n %{gem_name}-%{version}
gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec
%build
# Create the gem as gem install only works on a gem file
gem build %{gem_name}.gemspec
# %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir
# by default, so that we can move it into the buildroot in %%install
%gem_install
%install
mkdir -p %{buildroot}%{gem_dir}
cp -a .%{gem_dir}/* \
%{buildroot}%{gem_dir}/
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_sysconfdir}
mkdir -p %{buildroot}/%{_sharedstatedir}/%{gem_name}
touch %{buildroot}/%{_sharedstatedir}/%{gem_name}/%{gem_name}_data.yaml
cp -pa .%{_bindir}/* \
%{buildroot}%{_bindir}/
chmod a+x %{buildroot}%{gem_instdir}/bin/%{gem_name}
cat < %{buildroot}/%{_sysconfdir}/%{gem_name}rc.yaml
---
store: :moneta
store_options:
adapter: :YAML
adapter_options:
:file: '%{_sharedstatedir}/%{gem_name}/%{gem_name}_data.yaml'
EOF
# Run the test suite
%check
pushd .%{gem_instdir}
popd
%files
%dir %{gem_instdir}
%{_bindir}/trocla
%{gem_instdir}/.rspec
%exclude %{gem_instdir}/.travis.yml
%exclude %{gem_instdir}/.rspec
%exclude %{gem_instdir}/ext/redhat/%{name}.spec
%license %{gem_instdir}/LICENSE.txt
%{gem_instdir}/bin
%{gem_libdir}
%exclude %{gem_cache}
%{gem_spec}
%config(noreplace) %{_sysconfdir}/%{gem_name}rc.yaml
%dir %attr(-, -, -) %{_sharedstatedir}/%{gem_name}
%config(noreplace) %attr(660, root, root) %{_sharedstatedir}/%{gem_name}/%{gem_name}_data.yaml
%files doc
%doc %{gem_docdir}
%doc %{gem_instdir}/.document
%{gem_instdir}/Gemfile
%doc %{gem_instdir}/README.md
%doc %{gem_instdir}/CHANGELOG.md
%{gem_instdir}/Rakefile
%{gem_instdir}/spec
%{gem_instdir}/trocla.gemspec
%changelog
* Mon Dec 21 2015 mh - 0.2.0-1
- Release of v0.2.0
* Sun Jun 21 2015 mh - 0.1.2-1
- Initial package
trocla-0.6.0/README.md 0000644 0000041 0000041 00000046711 14756064121 014327 0 ustar www-data www-data # trocla
[](https://github.com/duritong/trocla/actions/workflows/ruby.yml)
Trocla provides you a simple way to create and store (random) passwords on a
central server, which can be retrieved by other applications. An example for
such an application is puppet and trocla can help you to not store any
plaintext or hashed passwords in your manifests by keeping these passwords only
on your puppetmaster.
Furthermore it provides you a simple cli that helps you to modify the password
storage from the cli.
Trocla does not only create and/or store a plain password, it is also able to
generate (and store) any kind of hashed passwords based on the plain password.
As long as the plain password is preset, trocla is able to generate any kind
of hashed passwords through an easy extendible plugin system.
It is not necessary to store the plain password on the server, you can also
just feed trocla with the hashed password and use that in your other tools.
A common example for that is that you let puppet retrieve (and hence create)
a salted sha512 password for a user. This will then store the salted sha512 of
a random password AND the plain text password in trocla. Later you can
retrieve (by deleting) the plain password and send it to the user. Puppet
will still simply retrieve the hashed password that is stored in trocla,
while the plain password is not anymore stored on the server.
By default trocla uses moneta to store the passwords and can use any kind of
key/value based storage supported by moneta for trocla. By default it uses a
simple yaml file.
However, since version 0.2.0 trocla also supports a pluggable storage backend
which allows you to write your custom backend. See more about stores below.
Trocla can also be integrated into [Hiera](https://docs.puppetlabs.com/hiera/) by using ZeroPointEnergy's [hiera-backend](https://github.com/ZeroPointEnergy/hiera-backend-trocla).
## Usage
### create
Assuming that we have an empty trocla storage.
trocla create user1 plain
This will create (if not already stored in trocla) a random password and
store its plain text under key user1. The password will also be returned
by trocla.
trocla create user2 mysql
This will create a random password and store its plain and mysql-style hashed
sha1 password in trocla. The hashed password is returned.
trocla create user1 mysql
This will take the already stored plain text password of key user1 and generate
and store the mysql-style hashed sha1 password.
It is possible that certain hash formats require additional options. For example
the pgsql hash requires also the user to create the md5 hash for the password.
You can pass these additional requirements as yaml-based strings to the format:
trocla create user1 pgsql 'username: user1'
This will create a pgsql password hash using the username user1.
Valid global options are:
* length: int - Define any lenght that a newly created password should have. Default: 16 - or whatever you define in your global settings.
* charset: (default|alphanumeric|shellsafe) - Which set of chars should be used for a random password? Default: default - or whatever you define in your global settings.
* profiles: a profile name or an array of profiles matching a profile_name in your configuration. Learn more about profiles below.
* random: boolean - Whether we allow creation of random passwords or we expect a password to be preset. Default: true - or whatever you define in your global settings.
* expires: An integer indicating the amount of seconds a value (e.g. password) is available. After expiration a value will not be available anymore and trying to `get` this key will return no value (nil). Meaning that calling create after expiration, would create a new password automatically. There is more about expiration in the storage backends section.
* render: A hash providing flags for formats to render the output specifially. This is a global option, but support depends on a per format basis.
Example:
trocla create some_shellsafe_password plain 'charset: shellsafe'
trocla create another_alphanumeric_20_char_password plain "charset: alphanumeric
length: 20"
### get
Get simply returns a stored password. It will not create a new password.
Assuming that we are still working with the same storage
trocla get user2 plain
will return the plain text password of the key user2.
trocla get user3 plain
This will return nothing, as no password with this format have been stored so
far.
### set
trocla set user3 plain
This will ask you for a password and set it under the appropriate key/format.
We expect a plain password to be entered and will format the password with
the selected format before storing it.
trocla set --password mysupersecretpassword user4 plain
This will take the password from the cli without asking you.
trocla set user5 mysql -p mysuperdbpassword
This will store a mysql sha1 hash for the key user5, without storing any kind
of plain text password.
If you like trocla not to format a password, as you are passing in an already
formatted password (like the sha512 hash), then you must use `--no-format` to
skip formatting. Like:
trocla set user5 sha512crypt --no-format -p '$6$1234$xxxx....'
You can also pipe in a password:
echo -n foo | trocla set user6 plain -p
or a file
cat some_file | trocla set user6 plain -p
trocla set user6 plain -p < some_file
### reset
trocla reset user1 md5crypt
This will recreate the salted md5 shadow-style hash. However, it will not create
a new plain text passwords. Hence, this is mainly usefull to create new hashed
passwords based on new salts.
If the plain password of a key is resetted, every already hashed password is
deleted as well, as the hashes wouldn't match anymore the plain text password.
### delete
trocla delete user1 plain
This will delete the plain password of the key user1 and return it.
### formats
trocla formats
This will list all available and supported formats.
## Attention
If you don't feed trocla initially with a hash and/or delete the generated
plain text passwords trocla will likely create a lot of plain text passwords
and store them on your machine/server. This is by intend and is all about which
problems (mainly passwords in configuration management manifests) trocla tries
to address. It is possible to store all passwords encrypted in the specific
backend.
See backend encryption for more information, however be aware that the key must
always also reside on the trocla node. So it mainly makes sense if you store
them on a remote backend like a central database server.
## Formats
Most formats are straight forward to use. Some formats require some additional
options to work properly. These are documented here:
### pgsql
Password hashes for PostgreSQL servers. Since postgesql 10 you can use the sha256 hash, you have two options:
* Create a ssh256 hash password with option `encode: sha256` (default value)
* Create a md5 hash, the username is require for the salt key, with option `encode: md5` and `username: your_user`
### bcrypt
You are able to tune the [cost factor of bcrypt](https://github.com/codahale/bcrypt-ruby#cost-factors) by passing the option `cost`.
Note: ruby bcrypt does not support a [cost > 31](https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L45).
### x509
This format takes a set of additional options. Required are:
subject: A subject for the target certificate. E.g. /C=ZZ/O=Trocla Inc./CN=test/emailAddress=example@example.com
OR
CN: The CN of the the target certificate. E.g. 'This is my self-signed certificate which doubles as CA'
Additional options are:
ca The trocla key of CA (imported into or generated within trocla) that
will be used to sign that certificate.
become_ca Whether the certificate should become a CA or not. Default: false,
to enable set it to true.
hash Hash to be used. Default sha2
keysize Keysize for the new key. Default is: 4096
serial Serial to be used, default is selecting a random one.
days How many days should the certificate be valid. Default 365
C instead within the subject string
ST instead within the subject string
L instead within the subject string
O instead within the subject string
OU instead within the subject string
emailAddress instead within the subject string
key_usages Any specific key_usages different than the default ones. If you specify
any, you must specify all that you want. If you don't want to have any,
you must specify an empty array.
altnames An array of subjectAltNames. By default for non CA certificates we
ensure that the CN ends up here as well. If you don't want that.
You need to pass an empty array.
name_constraints An array of domains that are added as permitted x509 NameConstraint.
By default, we do not add any contraint, meaning all domains are
signable by the CA, as soon as we have one item in the list, only
DNS entries matching this list are allowed. Be aware, that older
openssl versions have a bug with [leading dots](https://rt.openssl.org/Ticket/Display.html?id=3562) for name
constraints. So using them might not work everywhere as expected.
Output render options are:
certonly If set to true the x509 format will return only the certificate
keyonly If set to true the x509 format will return only the private key
publickeyonly If set to true the x509 format will return only the public key
### sshkey
This format generate a ssh keypair
Additional options are:
type The ssh key type (rsa, dsa). Default: rsa
bits Specifies the number of bits in the key to create. Default: 2048
comment Specifies a comment.
passphrase Specifies a passphrase.
Output render options are:
pubonly If set to true the sshkey format will return only the ssh public key
privonly If set to true the sshkey format will return only the ssh private key
### wireguard
This format generate a keypair for WireGuard.
The format requires the wg binary from WireGuard userland utilities.
Output render options are:
pubonly If set to true the wireguard format will return only the public key
privonly If set to true the wireguard format will return only the private key
## Installation
* Debian has trocla within its sid-release: `apt-get install trocla`
* For RHEL/CentOS 7 there is a [copr reporisotry](https://copr.fedoraproject.org/coprs/duritong/trocla/). Follow the help there to integrate the repository and install trocla.
* Trocla is also distributed as gem: `gem install trocla`
## Configuration
Trocla can be configured in /etc/troclarc.yaml and in ~/.troclarc.yaml. A sample configuration file can be found in `lib/trocla/default_config.yaml`.
By default trocla configures moneta to store all data in /tmp/trocla.yaml
### Profiles
It is possible to define profiles within the configuration file. The idea behind profiles are to make it easy to group together certain options for
automatic password generation.
Trocla ships with a default set of profiles, which are part of the `lib/trocla/default_config.yaml` configuration file. It is possible to override
the existing profiles within your own configuration file, as well as adding more. Note that the profiles part of the configuration file is merged
together and your configuration file has precedence.
The profiles part in the config is a hash where each entry consist of a name (key) and a hash of options (value).
Profiles make it especially easy to define a preset of options for SSL certificates as you will only need to set the certificate specific options,
while global options such as C, O or OU can be preset within the profile.
Profiles are used by setting the profiles option to a name of the pre-configured profiles, when passing options to the password option. On the cli
this looks like:
trocla create foo plain 'profiles: rootpw'
It is possible to pass mutliple profiles as an array, while the order will also reflect the precedence of the options.
Also it is possible to set a default profiles option in the options part of the configuration file.
### Storage backends
Trocla has a pluggable storage backend, which allows you to choose the way that values are stored (persistently).
Such a store is a simple class that implements Trocla::Store and at the moment there are the following store implementations:
* Moneta - the default store using [moneta](https://rubygems.org/gems/moneta) to delegate storing the values
* Memory - simple inmemory backend. Mainly used for testing.
* Vault - modern secrets storage by HashiCorp, require the ruby gem [vault](https://github.com/hashicorp/vault-ruby)
The backend is chosen based on the `store` configuration option. If it is a symbol, we expect it to be a store that we ship with trocla. Otherwise, we assume it to be a fully qualified ruby class name, that inherits from Trocla::Store. If trocla should load an additional library to be able to find your custom store class, you can set `store_require` to whatever should be passed to a ruby require statement.
Store backends can be configured through the `store_options` configuration.
#### Expiration
We expect storage backends to implement support for the `expires` option, so that keys expire after the passed amount of seconds. Furthermore a storage backend needs to implement the behaviour described by the rspec shared_example 'store_validation' section 'expiration'. Mainly:
* Expiration is always for all formats per key.
* Adding, deleting or updating a format will keep the existing expiration, but reset the planned expiration.
* While setting a new plain format will not only erase all other formats, but also erase/reset any expires.
* Setting a value with an expires option of 0 or false, will remove any existent expiration.
New backends should be tested using the provided shared example.
> **WARNING**: Vault backend use metadatas. It's set if an option is define. `expires` is automaticly change to `delete_version_after`, and you can use an interger or [format string](https://www.vaultproject.io/api-docs/secret/kv/kv-v2#parameters)
#### Moneta backends
Trocla uses moneta as its default storage backend and hence can store your passwords in any of moneta's supported backends. By default it uses the yaml backend, which is configured as followed:
```YAML
store_options:
adapter: :YAML
adapter_options:
:file: '/tmp/trocla.yaml'
```
In environments with multiple Puppet masters using an existing DB cluster might make sense. The configured user needs to be granted at least SELECT, INSERT, UPDATE, DELETE and CREATE permissions on your database:
```YAML
store_options:
adapter: :Sequel
adapter_options:
:db: 'mysql://db.server.name'
:user: 'trocla'
:password: '***'
:database: 'trocladb'
:table: 'trocla'
```
These examples are by no way complete, moneta has much more to offer. Please have a look at [moneta's documentation](https://github.com/minad/moneta/blob/master/README.md) for further information.
#### Vault backend
[Vault](https://www.vaultproject.io/) is a modern secret storage supported by HashiCorp, which works with a REST API. You can create multiple storage engine.
To use vault with trocla you need to create a kv (key/value) storage engine on the vault side. Trocla can use [v1](https://www.vaultproject.io/docs/secrets/kv/kv-v1) and [v2](https://www.vaultproject.io/docs/secrets/kv/kv-v2) API endpoints, but it's recommended to use the v2 (native hash object, history, acl...).
You need to install the `vault` gem to be able to use the vault backend, which is not included in the default dependencies for trocla.
With vault storage, the terminology changes:
* `mount`, this is the name of your kv engine
* `key`, this is the biggest change. As usual with trocla, the key is a simple string. With the vault kv engine, the key map to a path, so you can have a key like `my/path/key` for structured your data
* `secret`, is the data content of your key. This is a simple hash with key (format) and value (the secret content of your format)
The trocla mapping works the same way as with a moneta or file backend.
The `store_options` are a dynamic argument for initializer [Vault::Client](https://github.com/hashicorp/vault-ruby/blob/master/lib/vault/client.rb) class (except `:mount`, used to defined the kv name). You can define only one kv mount.
```YAML
store: :vault
store_options:
:mount: kv
:token: s.Tok3n
:address: https://vault.local
```
With Vault when you delete a key, you don't delete all key content. The metadatas, like history, are still here and the endpoint are not delete. If you prefere to destroy all key content you can add `:destroy: true` in the `store_options:` hash.
### Backend encryption
By default trocla does not encrypt anything it stores. You might want to let Trocla encrypt all your passwords, at the moment the only supported way is SSL.
Given that often trocla's store is on the same system at it's being used, there might be little sense to encrypt everything while the encryption keys are on the same system. However, if you are for example using an existing DB cluster using backend encryption you won't store any plaintext passwords within the database system.
### Backend SSL encryption
To enable SSL encryption (e.g. by using your puppet masters SSL keys), you need to set the following configuration options:
```YAML
encryption: :ssl
encryption_options:
:private_key: '/var/lib/puppet/ssl/private_keys/trocla.pem'
:public_key: '/var/lib/puppet/ssl/public_keys/trocla.pem'
```
## Hooks
You can specify hooks to be called whenever trocla sets or deletes a password. The idea is that this allows you to run custom code that can trigger further actions based on deleting or setting a password.
Enabling hooks is done through the following configuration:
```YAML
hooks:
set:
my_hook: /path/to/my_hook_file.rb
delete:
other_hook: /path/to/my_other_hook_file.rb
```
A hook must have the following implementation based on the above config:
```Ruby
class Trocla
module Hooks
def self.my_hook(trocla, key, format, options)
# [... your code ...]
end
end
end
```
You can specify only either one or both kinds of hooks.
Hooks must not raise any exceptions or interrupt the flow itself. They can also not change the value that was set or revert a deletion.
However, they have Trocla itself available (through `trocla`) and you must ensure to not create infinite loops.
## Update & Changes
See [Changelog](CHANGELOG.md)
## Contributing to trocla
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
* Fork the project
* Start a feature/bugfix branch
* Commit and push until you are happy with your contribution
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
## Copyright
Copyright (c) 2011-2015 mh. See LICENSE.txt for
further details.
trocla-0.6.0/CHANGELOG.md 0000644 0000041 0000041 00000014572 14756064121 014661 0 ustar www-data www-data # Changelog
## to 0.6.0
* move away from sha1 since they are not supported anymore on all distributions
* fix tests on various platforms and newer ruby versions
* introduce hooks for set and delete actions
## to 0.5.1
* support more moneta versions (#78) - Thank you [jcharaoui](https://github.com/jcharaoui)
* Fix issue with passing down expires to vault (#79) - Thank you [Steffy Fort](https://github.com/fe80)
* Don't require openssl Gem and make sure we don't regress on JRuby
## to 0.5.0
* moved from travis ci to github actions (#73) - Thank you [Georg-g](https://github.com/geor-g)
* Support expire in vault (#71) - Thank you [Steffy Fort](https://github.com/fe80)
* Syntax improvements (#70) - Thank you [Steffy Fort](https://github.com/fe80)
* Add SCRAM-SHA-256 postgres support (#69) - Thank you [Steffy Fort](https://github.com/fe80)
* Support destroying and entry to properly clean up in vault (#68) - Thank you [Steffy Fort](https://github.com/fe80)
* Support search with vault backend (#67) - Thank you [Steffy Fort](https://github.com/fe80)
* Add wireguard format (#65) - Thank you [Jonas Genannt](https://github.com/hggh)
* Expand search path for sample config - Thank you [Anarcat](https://github.com/anarcat)
## to 0.4.0
* Add vault backend (#61) - Thank you [Steffy Fort](https://github.com/fe80)
* Add sshkey format similar to the OpenSSL - Thank you [Raphaël Rondeau](https://github.com/rrondeau)
* format/x509 allow to render 'publickeyonly' (#62) - Thank you [Thomas Weißschuh](https://github.com/t-8ch)
* Add a method to search for keys and list all formats of a key (#49) - Thank you - [Steffy Fort](https://github.com/fe80)
* Proper return code on cli (#57) - Thank you [Steffy Fort](https://github.com/fe80)
* expand search path for sample config file to fix autopkgtest (#64) - Thank you [anarcat](https://github.com/anarcat)
* drop support for ruby < 2.7 & update dependencies
* skip self-signed cert verification test on newer openssl version (#63)
* Fix reseting passwords when using SSL encryption (#52)
## to 0.3.0
* Add open method to be able to immediately close a trocla store after using it - thanks martinpfeiffer
* Add typesafe charset - thanks hggh
* Support cost option for bcrypt
* address concurrency corner cases, when 2 concurrent threads or even processes
are currently calculating the same (expensive) format.
* parse additional options on cli (#39 & #46) - thanks fe80
## to 0.2.3
1. Add extended CA validity profiles
1. Make it possible to define keyUsage
## to 0.2.2
1. Bugfix to render output correctly also on an already existing set
1. Fix tests not working around midnight, due to timezone differences
## to 0.2.1
1. New Feature: Introduce a way to render specific formats, mainly this allows you to control the output of a specific format. See the x509 format for more information.
## to 0.2.0
1. New feature profiles: Introduce profiles to make it easy to have a default set of properties. See the profiles section for more information.
1. New feature expiration: Make it possible that keys can have an expiration. See the expiration section for more information.
1. Increase default password length to 16.
1. Add a console safe password charset. It should provide a subset of chars that are easier to type on a physical keyboard.
1. Fix a bug with encryptions while deleting all formats.
1. Introduce pluggable stores, so in the future we are able to talk to different backends and not only moneta. For testing and inspiration a simple in memory storage backend was added.
1. CHANGE: moneta's configuration for `adapter` & `adapter_options` now live under store_options in the configuration file. Till 0.3.0 old configuration entries will still be accepted.
1. CHANGE: ssl_options is now known as encryption_options. Till 0.3.0 old configuration entries will still be accepted.
1. Improve randomness when creating a serial number.
1. Add a new charset: hexadecimal
1. Add support for name constraints within the x509 format
1. Clarify documentation of the set action, as well as introduce `--no-format` for the set action.
## to 0.1.3
1. CHANGE: Self signed certificates are no longer CAs by default, actually they have never been due to a bug. If you want that a certificate is also a CA, you *must* pass `become_ca: true` to the options hash. But this makes it actually possible, that you can even have certificate chains. Thanks for initial hint to [Adrien Bréfort](https://github.com/abrefort)
1. Default keysize is now 4096
1. SECURITY: Do not increment serial, rather choose a random one.
1. Fixing setting of altnames, was not possible due to bug, till now.
1. Add extended tests for the x509 format, that describe all the internal specialities and should give an idea how it can be used.
1. Add cli option to list all formats
## to 0.1.1
1. fix storing data longer that public Keysize -11. Thanks [Timo Goebel](https://github.com/timogoebel)
1. add a numeric only charset. Thanks [Jonas Genannt](https://github.com/hggh)
1. fix reading key expire time. Thanks [asquelt](https://github.com/asquelt)
## to 0.1.0
1. Supporting encryption of the backends. Many thanks to Thomas Gelf
1. Adding a windows safe password charset
## to 0.0.12
1. change from sha1 signature for the x509 format to sha2
1. Fix an issue where shellsafe characters might have already been initialized with shell-unsafe characters. Plz review any shell-safe character passwords regarding this problem. See the [fix](https://github.com/duritong/trocla/pull/19) for more information. Thanks [asquelt](https://github.com/asquelt) for the fix.
## to 0.0.8
1. be sure to update as well the moneta gem, trocla now uses the official moneta releases and supports current avaiable versions.
1. Options for moneta's backends have changed. For example, if you are using the yaml-backend you will likely need to change the adapter option `:path:` to `:file:` to match moneta's new API.
1. **IMPORTANT:** If you are using the yaml backend you need to migrate the current data *before* using the new trocla version! You can migrate the datastore by using the following two sed commands: `sed -i 's/^\s\{3\}/ /' /PATH/TO/trocla_data.yaml` && `sed -i '/^\s\{2\}value\:/d' /PATH/TO/trocla_data.yaml`.
1. **SECURITY:** Previous versions of trocla used quite a simple random generator. Especially in combination with the puppet `fqdn_rand` function, you likely have very predictable random passwords and I recommend you to regenerate all randomly generated passwords! Now!
trocla-0.6.0/trocla.gemspec 0000644 0000041 0000041 00000006457 14756064121 015704 0 ustar www-data www-data # 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.6.0 ruby lib
Gem::Specification.new do |s|
s.name = "trocla".freeze
s.version = "0.6.0".freeze
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
s.require_paths = ["lib".freeze]
s.authors = ["mh".freeze]
s.date = "2024-12-30"
s.description = "Trocla helps you to generate random passwords and to store them in various formats (plain, MD5, bcrypt) for later retrival.".freeze
s.email = "mh+trocla@immerda.ch".freeze
s.executables = ["trocla".freeze]
s.extra_rdoc_files = [
"LICENSE.txt",
"README.md"
]
s.files = [
".document",
".github/workflows/ruby.yml",
".rspec",
"CHANGELOG.md",
"Gemfile",
"LICENSE.txt",
"README.md",
"Rakefile",
"bin/trocla",
"ext/redhat/rubygem-trocla.spec",
"lib/VERSION",
"lib/trocla.rb",
"lib/trocla/default_config.yaml",
"lib/trocla/encryptions.rb",
"lib/trocla/encryptions/none.rb",
"lib/trocla/encryptions/ssl.rb",
"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/sshkey.rb",
"lib/trocla/formats/wireguard.rb",
"lib/trocla/formats/x509.rb",
"lib/trocla/hooks.rb",
"lib/trocla/store.rb",
"lib/trocla/stores.rb",
"lib/trocla/stores/memory.rb",
"lib/trocla/stores/moneta.rb",
"lib/trocla/stores/vault.rb",
"lib/trocla/util.rb",
"lib/trocla/version.rb",
"spec/data/.keep",
"spec/fixtures/delete_test_hook.rb",
"spec/fixtures/set_test_hook.rb",
"spec/spec_helper.rb",
"spec/trocla/encryptions/none_spec.rb",
"spec/trocla/encryptions/ssl_spec.rb",
"spec/trocla/formats/pgsql_spec.rb",
"spec/trocla/formats/sshkey_spec.rb",
"spec/trocla/formats/x509_spec.rb",
"spec/trocla/hooks_spec.rb",
"spec/trocla/store/memory_spec.rb",
"spec/trocla/store/moneta_spec.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/".freeze
s.licenses = ["GPLv3".freeze]
s.rubygems_version = "3.5.22".freeze
s.summary = "Trocla a simple password generator and storage".freeze
s.specification_version = 4
s.add_runtime_dependency(%q.freeze, ["~> 2.0.0".freeze])
s.add_runtime_dependency(%q.freeze, ["~> 1.0".freeze])
s.add_runtime_dependency(%q.freeze, [">= 0".freeze])
s.add_runtime_dependency(%q.freeze, [">= 0".freeze])
s.add_development_dependency(%q.freeze, [">= 0".freeze])
s.add_development_dependency(%q.freeze, [">= 0".freeze])
s.add_development_dependency(%q.freeze, ["~> 2.0".freeze])
s.add_development_dependency(%q.freeze, [">= 0".freeze])
s.add_development_dependency(%q.freeze, [">= 0".freeze])
s.add_development_dependency(%q.freeze, [">= 0".freeze])
end