[DRE-commits] [trocla] 01/05: Imported Upstream version 0.2.3

zeha at debian.org zeha at debian.org
Wed Mar 2 01:47:09 UTC 2016


This is an automated email from the git hooks/post-receive script.

zeha pushed a commit to annotated tag debian/0.2.3-1
in repository trocla.

commit 53899ae71ea1e29bc64611c31bcdb0ca9b97897a
Author: Christian Hofstaedtler <zeha at debian.org>
Date:   Tue Mar 1 21:40:30 2016 +0100

    Imported Upstream version 0.2.3
---
 .travis.yml                          |   4 +
 CHANGELOG.md                         |  62 ++++++
 Gemfile                              |   6 +-
 README.md                            | 194 ++++++++++++++----
 bin/trocla                           |  47 +++--
 ext/redhat/rubygem-trocla.spec       | 118 +++++++++++
 lib/VERSION                          |   4 +-
 lib/trocla.rb                        |  95 ++++++---
 lib/trocla/default_config.yaml       |  47 ++++-
 lib/trocla/encryptions.rb            |   5 +-
 lib/trocla/encryptions/ssl.rb        |  18 +-
 lib/trocla/formats.rb                |   3 +
 lib/trocla/formats/x509.rb           | 117 +++++++----
 lib/trocla/store.rb                  |  74 +++++++
 lib/trocla/stores.rb                 |  39 ++++
 lib/trocla/stores/memory.rb          |  56 ++++++
 lib/trocla/stores/moneta.rb          |  54 +++++
 lib/trocla/util.rb                   |  30 ++-
 metadata.yml                         | 206 +++++++++++--------
 spec/spec_helper.rb                  | 255 ++++++++++++++++++++++--
 spec/trocla/encryptions/none_spec.rb |  22 +++
 spec/trocla/encryptions/ssl_spec.rb  |  34 +---
 spec/trocla/formats/x509_spec.rb     | 373 +++++++++++++++++++++++++++++++++++
 spec/trocla/store/memory_spec.rb     |   6 +
 spec/trocla/store/moneta_spec.rb     |   6 +
 spec/trocla/util_spec.rb             |  36 ++--
 spec/trocla_spec.rb                  | 185 ++++++++++-------
 trocla.gemspec                       |  55 +++---
 28 files changed, 1756 insertions(+), 395 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 8fc17b4..2c031af 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,9 @@
 language: ruby
+sudo: false
 rvm:
+  - jruby-18mode
+  - jruby-19mode
+  - 2.2.0
   - 2.1.0
   - 2.0.0
   - 1.9.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..be81d34
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,62 @@
+# Changelog
+
+## 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!
diff --git a/Gemfile b/Gemfile
index 26559b6..87132a9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,12 +11,14 @@ else
   gem "highline", "~> 1.6.2"
 end
 
+if defined?(RUBY_ENGINE) && (RUBY_ENGINE == 'jruby')
+  gem 'jruby-openssl'
+end
 gem "bcrypt"
 
 # Add dependencies to develop your gem here.
 # Include everything needed to run rake, tests, features, etc.
 group :development do
-  gem "mocha"
   if RUBY_VERSION.to_f > 1.8
     gem "rspec"
     gem "rdoc"
@@ -25,5 +27,7 @@ group :development do
     gem "rspec", "~> 2.4"
     gem "rdoc", "~> 3.8"
     gem "jeweler", "~> 1.6"
+    gem "addressable", "~> 2.3.8"
   end
+  gem 'rspec-pending_for'
 end
diff --git a/README.md b/README.md
index aa6966b..a9e40c2 100644
--- a/README.md
+++ b/README.md
@@ -11,21 +11,26 @@ 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 hashed passwords based on the plain password.
+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 md5 password for a user. This will then store the salted md5 of
+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.
 
-You can use any kind of key/value based storage supported by moneta for
-trocla. By default it uses a simple yaml file.
+Be 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
 
@@ -59,8 +64,12 @@ 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: 12 - or whatever you define in your global settings.
+* 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:
 
@@ -88,15 +97,22 @@ far.
     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 *ABC....
+    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:
 
@@ -124,37 +140,150 @@ deleted as well, as the hashes wouldn't match anymore the plain text password.
 
 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.
+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. Requires the option `username` to be set
+to the username to which the password will be assigned.
+
+### 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 at 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
 
 ## Installation
 
-Simply build and install the gem. 
+* 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 can store your passwords in all backends supported by moneta. A simple YAML file configuration may look as follows:
+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.
+
+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.
+
+#### 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
-adapter: :YAML
-adapter_options:
+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
-adapter: :Sequel
-adapter_options:
+store_options:
+  adapter: :Sequel
+  adapter_options:
     :db: 'mysql://db.server.name'
     :user: 'trocla'
     :password: '***'
@@ -162,47 +291,30 @@ adapter_options:
     :table: 'trocla'
 ```
 
-These examples are by no way complete, moneta has much more to offer.
+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.
+
+### 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.
 
-### SSL encryption
+### Backend SSL encryption
 
-You might want to let Trocla encrypt all your passwords
+To enable SSL encryption (e.g. by using your puppet masters SSL keys), you need to set the following configuration options:
 
 ```YAML
 encryption: :ssl
-ssl_options:
+encryption_options:
     :private_key: '/var/lib/puppet/ssl/private_keys/trocla.pem'
     :public_key: '/var/lib/puppet/ssl/public_keys/trocla.pem'
 ```
 
 ## Update & Changes
 
-### 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!
-1. We now support reading passwords from files, which means that you can now also easily add multi-line passwords. Have a look at the documentation above.
+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
diff --git a/bin/trocla b/bin/trocla
index 85bed57..cf3e412 100755
--- a/bin/trocla
+++ b/bin/trocla
@@ -9,12 +9,12 @@ require 'yaml'
 options = { :config_file => nil, :ask_password => true, :trace => false }
 
 OptionParser.new do |opts|
-  opts.on("--version", "-V", "Version information") do
+  opts.on('--version', '-V', 'Version information') do
     puts Trocla::VERSION::STRING
     exit
   end
 
-  opts.on("--config CONFIG", "-c", "Configuration file") do |v|
+  opts.on('--config CONFIG', '-c', 'Configuration file') do |v|
     if File.exist?(v)
       options[:config_file] = v
     else
@@ -23,19 +23,23 @@ OptionParser.new do |opts|
     end
   end
 
-  opts.on("--trace", "Show stack trace on failure") do
+  opts.on('--trace', 'Show stack trace on failure') do
     options[:trace] = true
   end
 
-  opts.on("--no-random") do
+  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("--length LENGTH") do |v|
+  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") do |pass|
+  opts.on('--password [PASSWORD]', '-p', 'Provide password at command line or STDIN') do |pass|
     options[:ask_password] = false
     options[:password] = pass
   end
@@ -59,23 +63,29 @@ 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
+    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!"
+      STDERR.puts 'Passwords did not match, exiting!'
       exit 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, options.delete(:other_options).shift.to_s)
+  end
   trocla.set_password(
     options.delete(:trocla_key),
     format,
-    trocla.formats(format).format(password, options.delete(:other_options).shift.to_s)
+    value
   )
-  ""
+  ''
 end
 
 def reset(options)
@@ -93,9 +103,13 @@ def delete(options)
   )
 end
 
+def formats(options)
+  "Available formats: #{Trocla::Formats.all.join(', ')}"
+end
+
 def check_format(format_name)
   if format_name.nil?
-    STDERR.puts "Missing format, exiting..."
+    STDERR.puts 'Missing format, exiting...'
     exit 1
   elsif !Trocla::Formats.available?(format_name)
     STDERR.puts "Error: The format #{format_name} is not available"
@@ -103,13 +117,13 @@ def check_format(format_name)
   end
 end
 
-actions=['create','get','set','reset','delete']
+actions=['create','get','set','reset','delete', 'formats' ]
 
-if !(ARGV.length < 2) && (action=ARGV.shift) && actions.include?(action)
+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 action == 'delete'
+    check_format(options[:trocla_format]) unless ['delete','formats'].include?(action)
     begin
       if result = send(action,options)
         puts result.is_a?(String) ? result : result.inspect
@@ -117,13 +131,14 @@ if !(ARGV.length < 2) && (action=ARGV.shift) && actions.include?(action)
     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)"
+        STDERR.puts '(See full trace by running task with --trace)'
       end
       raise e if options[:trace]
       exit 1
     end
 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
 
diff --git a/ext/redhat/rubygem-trocla.spec b/ext/redhat/rubygem-trocla.spec
new file mode 100644
index 0000000..82e0541
--- /dev/null
+++ b/ext/redhat/rubygem-trocla.spec
@@ -0,0 +1,118 @@
+# Generated from trocla-0.1.2.gem by gem2rpm -*- rpm-spec -*-
+%global gem_name trocla
+
+Name: rubygem-%{gem_name}
+Version: 0.2.2
+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 <<EOF > %{buildroot}/%{_sysconfdir}/%{gem_name}rc.yaml
+---
+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
diff --git a/lib/VERSION b/lib/VERSION
index 6e64a62..e959bdd 100644
--- a/lib/VERSION
+++ b/lib/VERSION
@@ -1,4 +1,4 @@
 major:0
-minor:1
-patch:2
+minor:2
+patch:3
 build:
diff --git a/lib/trocla.rb b/lib/trocla.rb
index a94d52f..b83a14a 100644
--- a/lib/trocla.rb
+++ b/lib/trocla.rb
@@ -2,9 +2,9 @@ require 'trocla/version'
 require 'trocla/util'
 require 'trocla/formats'
 require 'trocla/encryptions'
+require 'trocla/stores'
 
 class Trocla
-
   def initialize(config_file=nil)
     if config_file
       @config_file = File.expand_path(config_file)
@@ -14,49 +14,56 @@ class Trocla
   end
 
   def password(key,format,options={})
+    # respect a default profile, but let the
+    # profiles win over the default options
+    options['profiles'] ||= config['options']['profiles']
+    if options['profiles']
+      options = merge_profiles(options['profiles']).merge(options)
+    end
     options = config['options'].merge(options)
+
     raise "Format #{format} is not supported! Supported formats: #{Trocla::Formats.all.join(', ')}" unless Trocla::Formats::available?(format)
 
-    unless (password=get_password(key,format)).nil?
+    unless (password=get_password(key,format,options)).nil?
       return password
     end
 
-    plain_pwd = get_password(key,'plain')
+    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) unless format == 'plain'
+      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
-    set_password(key,format,self.formats(format).format(plain_pwd,options))
+    set_password(key,
+      format,
+      self.formats(format).format(plain_pwd,options),
+      options)
   end
 
-  def get_password(key, format)
-    decrypt(cache.fetch(key, {})[format])
+  def get_password(key, format, options={})
+    render(format,decrypt(store.get(key,format)),options)
   end
 
   def reset_password(key,format,options={})
-    set_password(key,format,nil)
+    set_password(key,format,nil,options)
     password(key,format,options)
   end
 
-  def delete_password(key,format=nil)
-    if format.nil?
-      decrypt(cache.delete(key))
+  def delete_password(key,format=nil,options={})
+    v = store.delete(key,format)
+    if v.is_a?(Hash)
+      Hash[*v.map do |f,encrypted_value|
+        [f,render(format,decrypt(encrypted_value),options)]
+      end.flatten]
     else
-      old_val = (h = cache.fetch(key,{})).delete(format)
-      h.empty? ? cache.delete(key) : cache[key] = h
-      decrypt(old_val)
+      render(format,decrypt(v),options)
     end
   end
 
-  def set_password(key,format,password)
-    if (format == 'plain')
-      h = (cache[key] = { 'plain' => encrypt(password) })
-    else
-      h = (cache[key] = cache.fetch(key,{}).merge({ format => encrypt(password) }))
-    end
-    decrypt h[format]
+  def set_password(key,format,password,options={})
+    store.set(key,format,encrypt(password),options)
+    render(format,password,options)
   end
 
   def formats(format)
@@ -64,10 +71,7 @@ class Trocla
   end
 
   def encryption
-    enc = config['encryption']
-    enc ||= :none
-    @encryption ||= Trocla::Encryptions[enc].new(self)
-    @encryption
+    @encryption ||= Trocla::Encryptions[config['encryption']].new(config['encryption_options'],self)
   end
 
   def config
@@ -75,14 +79,19 @@ class Trocla
   end
 
   private
-  def cache
-    @cache ||= build_cache
+  def store
+    @store ||= build_store
   end
 
-  def build_cache
-    require 'moneta'
-    lconfig = config
-    Moneta.new(lconfig['adapter'], lconfig['adapter_options']||{})
+  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 read_config
@@ -90,7 +99,14 @@ class Trocla
       default_config
     else
       raise "Configfile #{@config_file} does not exist!" unless File.exists?(@config_file)
-      default_config.merge(YAML.load(File.read(@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
 
@@ -103,9 +119,24 @@ class Trocla
     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
diff --git a/lib/trocla/default_config.yaml b/lib/trocla/default_config.yaml
index d4037fd..78ea01f 100644
--- a/lib/trocla/default_config.yaml
+++ b/lib/trocla/default_config.yaml
@@ -1,8 +1,47 @@
 ---
+store: :moneta
+store_options:
+  adapter: :YAML
+  adapter_options:
+    :file: '/tmp/trocla.yaml'
+
+encryption: :none
 options:
     random: true
-    length: 12
+    length: 16
     charset: default
-adapter: :YAML
-adapter_options:
-    :file: '/tmp/trocla.yaml'
+
+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
+
diff --git a/lib/trocla/encryptions.rb b/lib/trocla/encryptions.rb
index 6823665..6d6faed 100644
--- a/lib/trocla/encryptions.rb
+++ b/lib/trocla/encryptions.rb
@@ -1,9 +1,10 @@
 class Trocla::Encryptions
 
   class Base
-    attr_reader :trocla
-    def initialize(trocla)
+    attr_reader :trocla, :config
+    def initialize(config, trocla)
       @trocla = trocla
+      @config = config
     end
 
     def encrypt(value)
diff --git a/lib/trocla/encryptions/ssl.rb b/lib/trocla/encryptions/ssl.rb
index f3cc1fb..424c847 100644
--- a/lib/trocla/encryptions/ssl.rb
+++ b/lib/trocla/encryptions/ssl.rb
@@ -25,19 +25,17 @@ class Trocla::Encryptions::Ssl < Trocla::Encryptions::Base
   end
 
   def private_key
-      pass = nil
-      file = require_option(:private_key)
-      @private_key ||= OpenSSL::PKey::RSA.new(File.read(file), nil)
+      @private_key ||= begin
+        file = require_option(:private_key)
+        OpenSSL::PKey::RSA.new(File.read(file), nil)
+      end
   end
 
   def public_key
-      file = require_option(:public_key)
-      @public_key ||= OpenSSL::PKey::RSA.new(File.read(file), nil)
-  end
-
-  def config
-    @config = @trocla.config['ssl_options']
-    @config ||= Hash.new
+      @public_key ||= begin
+        file = require_option(:public_key)
+        OpenSSL::PKey::RSA.new(File.read(file), nil)
+      end
   end
 
   def option(key)
diff --git a/lib/trocla/formats.rb b/lib/trocla/formats.rb
index 0103c4e..6d27f3a 100644
--- a/lib/trocla/formats.rb
+++ b/lib/trocla/formats.rb
@@ -5,6 +5,9 @@ class Trocla::Formats
     def initialize(trocla)
       @trocla = trocla
     end
+    def render(output,render_options={})
+      output
+    end
   end
 
   class << self
diff --git a/lib/trocla/formats/x509.rb b/lib/trocla/formats/x509.rb
index 8d1cd9a..0499b87 100644
--- a/lib/trocla/formats/x509.rb
+++ b/lib/trocla/formats/x509.rb
@@ -1,5 +1,5 @@
+require 'openssl'
 class Trocla::Formats::X509 < Trocla::Formats::Base
-  require 'openssl'
   def format(plain_password,options={})
 
     if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
@@ -7,10 +7,15 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
       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
@@ -18,24 +23,41 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
       raise "You need to pass \"subject\" or \"CN\" as an option to use this format"
     end
     hash = options['hash'] || 'sha2'
-    sign_with = options['ca'] || nil
-    keysize = options['keysize'] || 2048
-    serial = options['serial'] || 1
-    days = options['days'].to_i || 365
-    altnames = options['altnames'] || nil
-    altnames.collect { |v| "DNS:#{v}" }.join(', ') if altnames
+    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 = OpenSSL::X509::Certificate.new(getca(sign_with))
-        cakey = OpenSSL::PKey::RSA.new(getca(sign_with))
-        caserial = getserial(sign_with, serial)
+        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
@@ -49,28 +71,37 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
       end
 
       begin
-        csr_cert = mkcert(caserial, request.subject, ca, request.public_key, days, altnames)
-        csr_cert.sign(cakey, signature(hash))
-        setserial(sign_with, caserial)
+        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
-
-      key.send("to_pem") + csr_cert.send("to_pem")
     else # self-signed certificate
       begin
         subj = OpenSSL::X509::Name.parse(subject)
-        cert = mkcert(serial, subj, nil, key.public_key, days, altnames)
+        cert = 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
 
-      key.send("to_pem") + cert.send("to_pem")
+  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
+    else
+      super(output,render_options)
     end
   end
-  private
 
+  private
   # nice help: https://gist.github.com/mitfik/1922961
 
   def signature(hash = 'sha2')
@@ -95,14 +126,13 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
 
   def mkreq(subject,public_key)
     request = OpenSSL::X509::Request.new
-    request.version = 0
     request.subject = subject
     request.public_key = public_key
 
     request
   end
 
-  def mkcert(serial,subject,issuer,public_key,days,altnames)
+  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
@@ -117,29 +147,50 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
     ef.subject_certificate = cert
     ef.issuer_certificate = issuer
     cert.extensions = [ ef.create_extension("subjectKeyIdentifier", "hash") ]
-    cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true) if subject == issuer
-    cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true) if subject != issuer
-    cert.add_extension ef.create_extension("keyUsage", "nonRepudiation, digitalSignature, keyEncipherment", true)
-    cert.add_extension ef.create_extension("subjectAltName", altnames, true) if altnames
+
+    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 getca(ca)
-    trocla.get_password(ca,'x509')
+  def getserial(ca)
+    newser = Trocla::Util.random_str(20,'hexadecimal').to_i(16)
+    all_serials(ca).include?(newser) ? getserial(ca) : newser
   end
 
-  def getserial(ca,serial)
-    newser = trocla.get_password("#{ca}_serial",'plain')
-    if newser
-      newser + 1
+  def all_serials(ca)
+    if allser = trocla.get_password("#{ca}_all_serials",'plain')
+      YAML.load(allser)
     else
-      serial
+      []
     end
   end
 
-  def setserial(ca,serial)
-    trocla.set_password("#{ca}_serial",'plain',serial)
+  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
diff --git a/lib/trocla/store.rb b/lib/trocla/store.rb
new file mode 100644
index 0000000..586edd2
--- /dev/null
+++ b/lib/trocla/store.rb
@@ -0,0 +1,74 @@
+# implements the default store behavior
+class Trocla::Store
+  attr_reader :store_config, :trocla
+  def initialize(config,trocla)
+    @store_config = config
+    @trocla = trocla
+  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
+
+  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(key)
+    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
diff --git a/lib/trocla/stores.rb b/lib/trocla/stores.rb
new file mode 100644
index 0000000..7f8f804
--- /dev/null
+++ b/lib/trocla/stores.rb
@@ -0,0 +1,39 @@
+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.exists?(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
diff --git a/lib/trocla/stores/memory.rb b/lib/trocla/stores/memory.rb
new file mode 100644
index 0000000..480c591
--- /dev/null
+++ b/lib/trocla/stores/memory.rb
@@ -0,0 +1,56 @@
+# 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
+
+  private
+  def set_plain(key,value,options)
+    memory[key] = { 'plain' => value }
+  end
+
+  def set_format(key,format,value,options)
+    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
diff --git a/lib/trocla/stores/moneta.rb b/lib/trocla/stores/moneta.rb
new file mode 100644
index 0000000..fd9c436
--- /dev/null
+++ b/lib/trocla/stores/moneta.rb
@@ -0,0 +1,54 @@
+# 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 get(key,format)
+    moneta.fetch(key, {})[format]
+  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
+end
diff --git a/lib/trocla/util.rb b/lib/trocla/util.rb
index 0e14eb3..691e362 100644
--- a/lib/trocla/util.rb
+++ b/lib/trocla/util.rb
@@ -15,13 +15,18 @@ class Trocla
       private
 
       def charsets
-        @charsets ||= {
-          'default'       => chars,
-          'alphanumeric'  => alphanumeric,
-          'shellsafe'     => shellsafe,
-          'windowssafe'   => windowssafe,
-          'numeric'       => numeric,
-        }
+        @charsets ||= begin
+          h = {
+            'default'      => chars,
+            'alphanumeric' => alphanumeric,
+            'shellsafe'    => shellsafe,
+            'windowssafe'  => windowssafe,
+            'numeric'      => numeric,
+            'hexadecimal'  => hexadecimal,
+            'consolesafe'  => consolesafe,
+          }
+          h.each { |k, v| h[k] = v.uniq }
+        end
       end
 
       def chars
@@ -33,8 +38,14 @@ class Trocla
       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 + ('0'..'9').to_a
+        @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + numeric
       end
       def numeric
         @numeric ||= ('0'..'9').to_a
@@ -48,6 +59,9 @@ class Trocla
       def windowssafe_chars
         @windowssafe_chars ||= "+%/@=?_.,".split(//)
       end
+      def consolesafe_chars
+        @consolesafe_chars ||= '+.-,_'.split(//)
+      end
     end
   end
 end
diff --git a/metadata.yml b/metadata.yml
index e6939c7..2c6fe9d 100644
--- a/metadata.yml
+++ b/metadata.yml
@@ -1,103 +1,133 @@
---- !ruby/object:Gem::Specification 
+--- !ruby/object:Gem::Specification
 name: trocla
-version: !ruby/object:Gem::Version 
-  version: 0.1.2
+version: !ruby/object:Gem::Version
+  version: 0.2.3
 platform: ruby
-authors: 
+authors:
 - mh
 autorequire: 
 bindir: bin
 cert_chain: []
-
-date: 2015-06-05 00:00:00 Z
-dependencies: 
-- !ruby/object:Gem::Dependency 
-  requirement: &id001 !ruby/object:Gem::Requirement 
-    requirements: 
-    - - ~>
-      - !ruby/object:Gem::Version 
-        version: 0.7.20
-  version_requirements: *id001
-  prerelease: false
-  type: :runtime
+date: 2016-02-15 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
   name: moneta
-- !ruby/object:Gem::Dependency 
-  requirement: &id002 !ruby/object:Gem::Requirement 
-    requirements: 
-    - - ~>
-      - !ruby/object:Gem::Version 
-        version: 1.6.2
-  version_requirements: *id002
-  prerelease: false
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
   type: :runtime
-  name: highline
-- !ruby/object:Gem::Dependency 
-  requirement: &id003 !ruby/object:Gem::Requirement 
-    requirements: 
-    - &id004 
-      - ">="
-      - !ruby/object:Gem::Version 
-        version: "0"
-  version_requirements: *id003
   prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: highline
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
   type: :runtime
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
   name: bcrypt
-- !ruby/object:Gem::Dependency 
-  requirement: &id005 !ruby/object:Gem::Requirement 
-    requirements: 
-    - *id004
-  version_requirements: *id005
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+  type: :runtime
   prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: rspec
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
   type: :development
-  name: mocha
-- !ruby/object:Gem::Dependency 
-  requirement: &id006 !ruby/object:Gem::Requirement 
-    requirements: 
-    - - ~>
-      - !ruby/object:Gem::Version 
-        version: "2.4"
-  version_requirements: *id006
   prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: rdoc
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
   type: :development
-  name: rspec
-- !ruby/object:Gem::Dependency 
-  requirement: &id007 !ruby/object:Gem::Requirement 
-    requirements: 
-    - - ~>
-      - !ruby/object:Gem::Version 
-        version: "3.8"
-  version_requirements: *id007
   prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: jeweler
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
   type: :development
-  name: rdoc
-- !ruby/object:Gem::Dependency 
-  requirement: &id008 !ruby/object:Gem::Requirement 
-    requirements: 
-    - - ~>
-      - !ruby/object:Gem::Version 
-        version: "1.6"
-  version_requirements: *id008
   prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: rspec-pending_for
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
   type: :development
-  name: jeweler
-description: Trocla helps you to generate random passwords and to store them in various formats (plain, MD5, bcrypt) for later retrival.
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+description: Trocla helps you to generate random passwords and to store them in various
+  formats (plain, MD5, bcrypt) for later retrival.
 email: mh+trocla at immerda.ch
-executables: 
+executables:
 - trocla
 extensions: []
-
-extra_rdoc_files: 
+extra_rdoc_files:
 - LICENSE.txt
 - README.md
-files: 
-- .document
-- .rspec
-- .travis.yml
+files:
+- ".document"
+- ".rspec"
+- ".travis.yml"
+- 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
@@ -115,36 +145,44 @@ files:
 - lib/trocla/formats/sha512crypt.rb
 - lib/trocla/formats/ssha.rb
 - lib/trocla/formats/x509.rb
+- lib/trocla/store.rb
+- lib/trocla/stores.rb
+- lib/trocla/stores/memory.rb
+- lib/trocla/stores/moneta.rb
 - lib/trocla/util.rb
 - lib/trocla/version.rb
 - spec/data/.keep
 - spec/spec_helper.rb
+- spec/trocla/encryptions/none_spec.rb
 - spec/trocla/encryptions/ssl_spec.rb
+- spec/trocla/formats/x509_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
 homepage: https://tech.immerda.ch/2011/12/trocla-get-hashed-passwords-out-of-puppet-manifests/
-licenses: 
+licenses:
 - GPLv3
 metadata: {}
-
 post_install_message: 
 rdoc_options: []
-
-require_paths: 
+require_paths:
 - lib
-required_ruby_version: !ruby/object:Gem::Requirement 
-  requirements: 
-  - *id004
-required_rubygems_version: !ruby/object:Gem::Requirement 
-  requirements: 
-  - *id004
+required_ruby_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ">="
+    - !ruby/object:Gem::Version
+      version: '0'
+required_rubygems_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ">="
+    - !ruby/object:Gem::Version
+      version: '0'
 requirements: []
-
 rubyforge_project: 
-rubygems_version: 2.0.15
+rubygems_version: 2.2.2
 signing_key: 
 specification_version: 4
 summary: Trocla a simple password generator and storage
 test_files: []
-
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 10491d8..1acc32b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,7 +1,7 @@
 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
 $LOAD_PATH.unshift(File.dirname(__FILE__))
 require 'rspec'
-require 'mocha'
+require 'rspec/pending_for'
 require 'yaml'
 require 'trocla'
 
@@ -9,8 +9,219 @@ require 'trocla'
 # in ./support/ and its subdirectories.
 Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
 
-RSpec.configure do |config|
-  
+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
+
+  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
@@ -18,26 +229,30 @@ def default_config
 end
 
 def test_config
-  return @config unless @config.nil?
-  @config = default_config
-  @config.delete('adapter_options')
-  @config['adapter'] = :Memory
-  @config
+  @config ||= default_config.merge({
+    'store' => :memory,
+  })
+end
+
+def test_config_persistent
+  @config ||= default_config.merge({
+    'store_options' => {
+      'adapter'         => :YAML,
+      'adapter_options' => {
+        :file => trocla_yaml_file
+      },
+    },
+  })
 end
 
 def ssl_test_config
-  return @ssl_config unless @ssl_config.nil?
-  @ssl_config = test_config
-  @ssl_config['encryption'] = :ssl
-  @ssl_config['ssl_options'] = {
-    :private_key => data_dir('trocla.key'),
-    :public_key  => data_dir('trocla.pub')
-  }
-  @ssl_config['adapter'] = :YAML
-  @ssl_config['adapter_options'] = {
-    :file => trocla_yaml_file
-  }
-  @ssl_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 base_dir
diff --git a/spec/trocla/encryptions/none_spec.rb b/spec/trocla/encryptions/none_spec.rb
new file mode 100644
index 0000000..7710eeb
--- /dev/null
+++ b/spec/trocla/encryptions/none_spec.rb
@@ -0,0 +1,22 @@
+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
diff --git a/spec/trocla/encryptions/ssl_spec.rb b/spec/trocla/encryptions/ssl_spec.rb
index bf687a2..de88f7c 100644
--- a/spec/trocla/encryptions/ssl_spec.rb
+++ b/spec/trocla/encryptions/ssl_spec.rb
@@ -20,37 +20,7 @@ describe "Trocla::Encryptions::Ssl" do
   end
 
   describe "encrypt" do
-    it "should be able to store random passwords" do
-      @trocla.password('random1', 'plain').length.should eql(12)
-    end
-
-    it "should be able to store long random passwords" do
-      @trocla.set_password('random1_long','plain',4096.times.collect{|s| 'x' }.join('')).length.should eql(4096)
-    end
-
-    it "should be able to retrieve stored random passwords" do
-      stored = @trocla.password('random1', 'plain')
-      retrieved = @trocla.password('random1', 'plain')
-      retrieved_again = @trocla.password('random1', 'plain')
-      retrieved.should eql(stored)
-      retrieved_again.should eql(stored)
-    end
-
-    it "should be able to read encrypted passwords" do
-      @trocla.set_password('some_pass', 'plain', 'super secret')
-      @trocla.get_password('some_pass', 'plain').should eql('super secret')
-    end
-
-    it "should not store plaintext passwords" do
-      @trocla.set_password('noplain', 'plain', 'plaintext_password')
-      File.readlines(trocla_yaml_file).grep(/plaintext_password/).should be_empty
-    end
-
-    it "should make 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)
-      yaml['one_key']['plain'].should_not eql(yaml['another_key']['plain'])
-    end
+    include_examples 'encryption_basics'
+    include_examples 'verify_encryption'
   end
 end
diff --git a/spec/trocla/formats/x509_spec.rb b/spec/trocla/formats/x509_spec.rb
new file mode 100644
index 0000000..92a6027
--- /dev/null
+++ b/spec/trocla/formats/x509_spec.rb
@@ -0,0 +1,373 @@
+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 at 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
+      expect(verify(cert,cert)).to be false
+
+      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 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 at 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 at 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 at 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 %x{openssl version} =~ /1\.0\.[2-9]/
+        expect(verify([@ca,ca2],valid_cert)).to be true
+      else
+        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
+      end
+
+      false_cert_str = @trocla.password('myfalseexamplecert','x509', {
+        'subject'  => '/C=ZZ/O=Trocla Inc./CN=foo.example.net/emailAddress=example at 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 at 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'         => 'sha1',
+        'keysize'      => 2048,
+        'days'         => 3650,
+        'subject'      => nil,
+        'C'            => 'AA',
+        'ST'           => 'Earth',
+        'L'            => 'Here',
+        'O'            => 'SSLTrocla',
+        'OU'           => 'root',
+        'CN'           => 'www.test',
+        'emailAddress' => 'test at 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']}/)
+      hash_match = (defined?(RUBY_ENGINE) &&RUBY_ENGINE == 'jruby') ? 'RSA-SHA1' : 'sha1WithRSAEncryption'
+      expect(cert.signature_algorithm).to eq(hash_match)
+      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
+      expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }.value).to eq('DNS:www.test, DNS:test, DNS:test1, DNS:test2, DNS:test3')
+
+      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 at 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
diff --git a/spec/trocla/store/memory_spec.rb b/spec/trocla/store/memory_spec.rb
new file mode 100644
index 0000000..6522c2a
--- /dev/null
+++ b/spec/trocla/store/memory_spec.rb
@@ -0,0 +1,6 @@
+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
diff --git a/spec/trocla/store/moneta_spec.rb b/spec/trocla/store/moneta_spec.rb
new file mode 100644
index 0000000..ad7e2e4
--- /dev/null
+++ b/spec/trocla/store/moneta_spec.rb
@@ -0,0 +1,6 @@
+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
diff --git a/spec/trocla/util_spec.rb b/spec/trocla/util_spec.rb
index 4d49150..d96d0ae 100644
--- a/spec/trocla/util_spec.rb
+++ b/spec/trocla/util_spec.rb
@@ -4,31 +4,43 @@ describe "Trocla::Util" do
 
   { :random_str => 12, :salt => 8 }.each do |m,length|
     describe m do
-      it "should be random" do
-        Trocla::Util.send(m).should_not eql(Trocla::Util.send(m))
+      it "is random" do
+        expect(Trocla::Util.send(m)).not_to eq(Trocla::Util.send(m))
       end
 
-      it "should default to length #{length}" do
-        Trocla::Util.send(m).length.should == length
+      it "defaults to length #{length}" do
+        expect(Trocla::Util.send(m).length).to eq(length)
       end
 
-      it "should be possible to change length" do
-        Trocla::Util.send(m,8).length.should == 8
-        Trocla::Util.send(m,32).length.should == 32
-        Trocla::Util.send(m,1).length.should == 1
+      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
-    it "should create random numeric password" do
-      Trocla::Util.send(:random_str, 12, 'numeric' ).should =~ /^[0-9]{12}$/
+    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 :salt do
-    it "should only contain characters and numbers" do
-      Trocla::Util.salt =~ /^[a-z0-9]+$/i
+    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
diff --git a/spec/trocla_spec.rb b/spec/trocla_spec.rb
index dbd4abd..ea702f9 100644
--- a/spec/trocla_spec.rb
+++ b/spec/trocla_spec.rb
@@ -1,128 +1,161 @@
 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)
     @trocla = Trocla.new
   end
-  
+
   describe "password" do
-    it "should generate random passwords by default" do
-      @trocla.password('random1','plain').should_not eql(@trocla.password('random2','plain'))
+    it "generates random passwords by default" do
+      expect(@trocla.password('random1','plain')).not_to eq(@trocla.password('random2','plain'))
     end
 
-    it "should generate passwords of length #{default_config['options']['length']}" do
-      @trocla.password('random1','plain').length.should eql(default_config['options']['length'])
+    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|
       describe "#{format} password format" do
-        it "should return a password hashed in the #{format} format" do
-          @trocla.password('some_test',format,format_options[format]).should_not be_empty
+        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 "should return the same hashed for the #{format} format on multiple invocations" do
-          (round1=@trocla.password('some_test',format,format_options[format])).should_not be_empty
-          @trocla.password('some_test',format,format_options[format]).should eql(round1)
+
+        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 "should also store the plain password by default" do
+
+        it "also stores the plain password by default" do
           pwd = @trocla.password('some_test','plain')
-          pwd.should_not be_empty
-          pwd.length.should eql(12)
+          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 "should raise an exception if not a random password is asked but plain password is not present for format #{format}" do
-        lambda{ @trocla.password('not_random',format, 'random' => false) }.should raise_error
+      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_test','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_test','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
+        pwd = @trocla.password('some_test','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 "should reset hashed passwords on a new plain password" do
-      @trocla.password('set_test','mysql').should_not be_empty
-      @trocla.get_password('set_test','mysql').should_not be_nil
-      (old_plain=@trocla.password('set_test','mysql')).should_not be_empty
-      
-      @trocla.set_password('set_test','plain','foobar').should_not eql(old_plain)
-      @trocla.get_password('set_test','mysql').should be_nil
+    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 "should otherwise only update the hash" do
-      (mysql = @trocla.password('set_test2','mysql')).should_not be_empty
-      (md5crypt = @trocla.password('set_test2','md5crypt')).should_not be_empty
-      (plain = @trocla.get_password('set_test2','plain')).should_not be_empty
-      
-      (new_mysql = @trocla.set_password('set_test2','mysql','foo')).should_not eql(mysql)
-      @trocla.get_password('set_test2','mysql').should eql(new_mysql)
-      @trocla.get_password('set_test2','md5crypt').should eql(md5crypt)
-      @trocla.get_password('set_test2','plain').should eql(plain)
+
+    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
   end
-  
+
   describe "reset_password" do
-    it "should reset a password" do
+    it "resets a password" do
       plain1 = @trocla.password('reset_pwd','plain')
       plain2 = @trocla.reset_password('reset_pwd','plain')
-      
-      plain1.should_not eql(plain2)
+
+      expect(plain1).not_to eq(plain2)
     end
-    
-    it "should not reset other formats" do
-      (mysql = @trocla.password('reset_pwd2','mysql')).should_not be_empty
-      (md5crypt1 = @trocla.password('reset_pwd2','md5crypt')).should_not be_empty
-      
-      (md5crypt2 = @trocla.reset_password('reset_pwd2','md5crypt')).should_not be_empty
-      md5crypt2.should_not eql(md5crypt1)
-      
-      @trocla.get_password('reset_pwd2','mysql').should eql(mysql)
+
+    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 "delete_password" do
-    it "should delete all passwords if no format is given" do
-      @trocla.password('delete_test1','mysql').should_not be_nil
-      @trocla.get_password('delete_test1','plain').should_not be_nil
-      
+    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')
-      @trocla.get_password('delete_test1','plain').should be_nil
-      @trocla.get_password('delete_test1','mysql').should be_nil
+      expect(@trocla.get_password('delete_test1','plain')).to be_nil
+      expect(@trocla.get_password('delete_test1','mysql')).to be_nil
     end
-    
-    it "should delete only a given format" do
-      @trocla.password('delete_test2','mysql').should_not be_nil
-      @trocla.get_password('delete_test2','plain').should_not be_nil
-      
+
+    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')
-      @trocla.get_password('delete_test2','plain').should be_nil
-      @trocla.get_password('delete_test2','mysql').should_not be_nil
+      expect(@trocla.get_password('delete_test2','plain')).to be_nil
+      expect(@trocla.get_password('delete_test2','mysql')).not_to be_nil
     end
-    
-    it "should delete only a given non-plain format" do
-      @trocla.password('delete_test3','mysql').should_not be_nil
-      @trocla.get_password('delete_test3','plain').should_not be_nil
-      
+
+    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')
-      @trocla.get_password('delete_test3','mysql').should be_nil
-      @trocla.get_password('delete_test3','plain').should_not be_nil
+      expect(@trocla.get_password('delete_test3','mysql')).to be_nil
+      expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
     end
   end
-  
+
   def format_options
     @format_options ||= Hash.new({}).merge({
       'pgsql' => { 'username' => 'test' },
       'x509'  => { 'CN' => 'test' },
     })
   end
-  
+
 end
 
 describe "VERSION" do
-  it "should return a version" do
-    Trocla::VERSION::STRING.should_not be_empty
+  it "returns a version" do
+    expect(Trocla::VERSION::STRING).not_to be_empty
   end
 end
diff --git a/trocla.gemspec b/trocla.gemspec
index e718ca8..dcea727 100644
--- a/trocla.gemspec
+++ b/trocla.gemspec
@@ -2,14 +2,16 @@
 # DO NOT EDIT THIS FILE DIRECTLY
 # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
 # -*- encoding: utf-8 -*-
+# stub: trocla 0.2.3 ruby lib
 
 Gem::Specification.new do |s|
   s.name = "trocla"
-  s.version = "0.1.2"
+  s.version = "0.2.3"
 
   s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+  s.require_paths = ["lib"]
   s.authors = ["mh"]
-  s.date = "2015-06-05"
+  s.date = "2016-02-15"
   s.description = "Trocla helps you to generate random passwords and to store them in various formats (plain, MD5, bcrypt) for later retrival."
   s.email = "mh+trocla at immerda.ch"
   s.executables = ["trocla"]
@@ -21,11 +23,13 @@ Gem::Specification.new do |s|
     ".document",
     ".rspec",
     ".travis.yml",
+    "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",
@@ -43,49 +47,56 @@ Gem::Specification.new do |s|
     "lib/trocla/formats/sha512crypt.rb",
     "lib/trocla/formats/ssha.rb",
     "lib/trocla/formats/x509.rb",
+    "lib/trocla/store.rb",
+    "lib/trocla/stores.rb",
+    "lib/trocla/stores/memory.rb",
+    "lib/trocla/stores/moneta.rb",
     "lib/trocla/util.rb",
     "lib/trocla/version.rb",
     "spec/data/.keep",
     "spec/spec_helper.rb",
+    "spec/trocla/encryptions/none_spec.rb",
     "spec/trocla/encryptions/ssl_spec.rb",
+    "spec/trocla/formats/x509_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/"
   s.licenses = ["GPLv3"]
-  s.require_paths = ["lib"]
-  s.rubygems_version = "2.0.15"
+  s.rubygems_version = "2.2.2"
   s.summary = "Trocla a simple password generator and storage"
 
   if s.respond_to? :specification_version then
     s.specification_version = 4
 
     if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
-      s.add_runtime_dependency(%q<moneta>, ["~> 0.7.20"])
-      s.add_runtime_dependency(%q<highline>, ["~> 1.6.2"])
+      s.add_runtime_dependency(%q<moneta>, [">= 0"])
+      s.add_runtime_dependency(%q<highline>, [">= 0"])
       s.add_runtime_dependency(%q<bcrypt>, [">= 0"])
-      s.add_development_dependency(%q<mocha>, [">= 0"])
-      s.add_development_dependency(%q<rspec>, ["~> 2.4"])
-      s.add_development_dependency(%q<rdoc>, ["~> 3.8"])
-      s.add_development_dependency(%q<jeweler>, ["~> 1.6"])
+      s.add_development_dependency(%q<rspec>, [">= 0"])
+      s.add_development_dependency(%q<rdoc>, [">= 0"])
+      s.add_development_dependency(%q<jeweler>, [">= 0"])
+      s.add_development_dependency(%q<rspec-pending_for>, [">= 0"])
     else
-      s.add_dependency(%q<moneta>, ["~> 0.7.20"])
-      s.add_dependency(%q<highline>, ["~> 1.6.2"])
+      s.add_dependency(%q<moneta>, [">= 0"])
+      s.add_dependency(%q<highline>, [">= 0"])
       s.add_dependency(%q<bcrypt>, [">= 0"])
-      s.add_dependency(%q<mocha>, [">= 0"])
-      s.add_dependency(%q<rspec>, ["~> 2.4"])
-      s.add_dependency(%q<rdoc>, ["~> 3.8"])
-      s.add_dependency(%q<jeweler>, ["~> 1.6"])
+      s.add_dependency(%q<rspec>, [">= 0"])
+      s.add_dependency(%q<rdoc>, [">= 0"])
+      s.add_dependency(%q<jeweler>, [">= 0"])
+      s.add_dependency(%q<rspec-pending_for>, [">= 0"])
     end
   else
-    s.add_dependency(%q<moneta>, ["~> 0.7.20"])
-    s.add_dependency(%q<highline>, ["~> 1.6.2"])
+    s.add_dependency(%q<moneta>, [">= 0"])
+    s.add_dependency(%q<highline>, [">= 0"])
     s.add_dependency(%q<bcrypt>, [">= 0"])
-    s.add_dependency(%q<mocha>, [">= 0"])
-    s.add_dependency(%q<rspec>, ["~> 2.4"])
-    s.add_dependency(%q<rdoc>, ["~> 3.8"])
-    s.add_dependency(%q<jeweler>, ["~> 1.6"])
+    s.add_dependency(%q<rspec>, [">= 0"])
+    s.add_dependency(%q<rdoc>, [">= 0"])
+    s.add_dependency(%q<jeweler>, [">= 0"])
+    s.add_dependency(%q<rspec-pending_for>, [">= 0"])
   end
 end
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/trocla.git



More information about the Pkg-ruby-extras-commits mailing list