trocla 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/VERSION CHANGED
@@ -1,4 +1,4 @@
1
1
  major:0
2
- minor:2
2
+ minor:4
3
3
  patch:0
4
4
  build:
data/lib/trocla.rb CHANGED
@@ -13,6 +13,17 @@ class Trocla
13
13
  end
14
14
  end
15
15
 
16
+ def self.open(config_file=nil)
17
+ trocla = Trocla.new(config_file)
18
+
19
+ if block_given?
20
+ yield trocla
21
+ trocla.close
22
+ else
23
+ trocla
24
+ end
25
+ end
26
+
16
27
  def password(key,format,options={})
17
28
  # respect a default profile, but let the
18
29
  # profiles win over the default options
@@ -24,43 +35,59 @@ class Trocla
24
35
 
25
36
  raise "Format #{format} is not supported! Supported formats: #{Trocla::Formats.all.join(', ')}" unless Trocla::Formats::available?(format)
26
37
 
27
- unless (password=get_password(key,format)).nil?
38
+ unless (password=get_password(key,format,options)).nil?
28
39
  return password
29
40
  end
30
41
 
31
- plain_pwd = get_password(key,'plain')
42
+ plain_pwd = get_password(key,'plain',options)
32
43
  if options['random'] && plain_pwd.nil?
33
44
  plain_pwd = Trocla::Util.random_str(options['length'].to_i,options['charset'])
34
45
  set_password(key,'plain',plain_pwd,options) unless format == 'plain'
35
46
  elsif !options['random'] && plain_pwd.nil?
36
47
  raise "Password must be present as plaintext if you don't want a random password"
37
48
  end
38
- set_password(key,format,self.formats(format).format(plain_pwd,options),options)
49
+ pwd = self.formats(format).format(plain_pwd,options)
50
+ # it's possible that meanwhile another thread/process was faster in
51
+ # formating the password. But we want todo that second lookup
52
+ # only for expensive formats
53
+ if self.formats(format).expensive?
54
+ get_password(key,format,options) || set_password(key, format, pwd, options)
55
+ else
56
+ set_password(key, format, pwd, options)
57
+ end
39
58
  end
40
59
 
41
- def get_password(key, format)
42
- decrypt(store.get(key,format))
60
+ def get_password(key, format, options={})
61
+ render(format,decrypt(store.get(key,format)),options)
43
62
  end
44
63
 
45
64
  def reset_password(key,format,options={})
46
- set_password(key,format,nil,options)
65
+ delete_password(key,format)
47
66
  password(key,format,options)
48
67
  end
49
68
 
50
- def delete_password(key,format=nil)
69
+ def delete_password(key,format=nil,options={})
51
70
  v = store.delete(key,format)
52
71
  if v.is_a?(Hash)
53
72
  Hash[*v.map do |f,encrypted_value|
54
- [f,decrypt(encrypted_value)]
73
+ [f,render(format,decrypt(encrypted_value),options)]
55
74
  end.flatten]
56
75
  else
57
- decrypt(v)
76
+ render(format,decrypt(v),options)
58
77
  end
59
78
  end
60
79
 
61
80
  def set_password(key,format,password,options={})
62
81
  store.set(key,format,encrypt(password),options)
63
- password
82
+ render(format,password,options)
83
+ end
84
+
85
+ def available_format(key, options={})
86
+ render(false,store.formats(key),options)
87
+ end
88
+
89
+ def search_key(key, options={})
90
+ render(false,store.search(key),options)
64
91
  end
65
92
 
66
93
  def formats(format)
@@ -75,6 +102,10 @@ class Trocla
75
102
  @config ||= read_config
76
103
  end
77
104
 
105
+ def close
106
+ store.close
107
+ end
108
+
78
109
  private
79
110
  def store
80
111
  @store ||= build_store
@@ -116,6 +147,14 @@ class Trocla
116
147
  encryption.decrypt(value)
117
148
  end
118
149
 
150
+ def render(format,output,options={})
151
+ if format && output && f=self.formats(format)
152
+ f.render(output,options['render']||{})
153
+ else
154
+ output
155
+ end
156
+ end
157
+
119
158
  def default_config
120
159
  require 'yaml'
121
160
  YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__),'trocla','default_config.yaml'))))
@@ -21,6 +21,16 @@ profiles:
21
21
  login:
22
22
  charset: consolesafe
23
23
  length: 16
24
+ x509veryverylong:
25
+ # 15 years
26
+ days: 5475
27
+ # 5475 days
28
+ expires: 466560000
29
+ x509verylong:
30
+ # 10 years
31
+ days: 3650
32
+ # 3600 days
33
+ expires: 311040000
24
34
  x509long:
25
35
  # 5 years
26
36
  days: 1825
@@ -5,6 +5,20 @@ class Trocla::Formats
5
5
  def initialize(trocla)
6
6
  @trocla = trocla
7
7
  end
8
+ def render(output,render_options={})
9
+ output
10
+ end
11
+ def expensive?
12
+ self.class.expensive?
13
+ end
14
+ class << self
15
+ def expensive(is_expensive)
16
+ @expensive = is_expensive
17
+ end
18
+ def expensive?
19
+ @expensive == true
20
+ end
21
+ end
8
22
  end
9
23
 
10
24
  class << self
@@ -1,6 +1,7 @@
1
1
  class Trocla::Formats::Bcrypt < Trocla::Formats::Base
2
+ expensive true
2
3
  require 'bcrypt'
3
4
  def format(plain_password,options={})
4
- BCrypt::Password.create(plain_password).to_s
5
+ BCrypt::Password.create(plain_password, :cost => options['cost']||BCrypt::Engine.cost).to_s
5
6
  end
6
7
  end
@@ -0,0 +1,46 @@
1
+ require 'sshkey'
2
+
3
+ class Trocla::Formats::Sshkey < Trocla::Formats::Base
4
+
5
+ expensive true
6
+
7
+ def format(plain_password,options={})
8
+
9
+ if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY/m)
10
+ # Import, validate ssh key
11
+ begin
12
+ sshkey = ::SSHKey.new(plain_password)
13
+ rescue Exception => e
14
+ raise "SSH key import failed: #{e.message}"
15
+ end
16
+ return sshkey.private_key + sshkey.ssh_public_key
17
+ end
18
+
19
+ type = options['type'] || 'rsa'
20
+ bits = options['bits'] || 2048
21
+
22
+ begin
23
+ sshkey = ::SSHKey.generate(
24
+ type: type,
25
+ bits: bits,
26
+ comment: options['comment'],
27
+ passphrase: options['passphrase']
28
+ )
29
+ rescue Exception => e
30
+ raise "SSH key creation failed: #{e.message}"
31
+ end
32
+
33
+ sshkey.private_key + sshkey.ssh_public_key
34
+ end
35
+
36
+ def render(output,render_options={})
37
+ if render_options['privonly']
38
+ ::SSHKey.new(output).private_key
39
+ elsif render_options['pubonly']
40
+ ::SSHKey.new(output).ssh_public_key
41
+ else
42
+ super(output,render_options)
43
+ end
44
+ end
45
+
46
+ end
@@ -1,5 +1,6 @@
1
1
  require 'openssl'
2
2
  class Trocla::Formats::X509 < Trocla::Formats::Base
3
+ expensive true
3
4
  def format(plain_password,options={})
4
5
 
5
6
  if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
@@ -28,6 +29,8 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
28
29
  keysize = options['keysize'] || 4096
29
30
  days = options['days'].nil? ? 365 : options['days'].to_i
30
31
  name_constraints = Array(options['name_constraints'])
32
+ key_usages = options['key_usages']
33
+ key_usages = Array(key_usages) if key_usages
31
34
 
32
35
  altnames = if become_ca || (an = options['altnames']) && Array(an).empty?
33
36
  []
@@ -49,6 +52,7 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
49
52
  raise "Private key for #{subject} creation failed: #{e.message}"
50
53
  end
51
54
 
55
+ cert = nil
52
56
  if sign_with # certificate signed with CA
53
57
  begin
54
58
  ca_str = trocla.get_password(sign_with,'x509')
@@ -68,28 +72,39 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
68
72
  end
69
73
 
70
74
  begin
71
- csr_cert = mkcert(caserial, request.subject, ca, request.public_key, days, altnames, name_constraints, become_ca)
72
- csr_cert.sign(cakey, signature(hash))
75
+ cert = mkcert(caserial, request.subject, ca, request.public_key, days,
76
+ altnames, key_usages, name_constraints, become_ca)
77
+ cert.sign(cakey, signature(hash))
73
78
  addserial(sign_with, caserial)
74
79
  rescue Exception => e
75
80
  raise "Certificate #{subject} signing failed: #{e.message}"
76
81
  end
77
-
78
- key.to_pem + csr_cert.to_pem
79
82
  else # self-signed certificate
80
83
  begin
81
84
  subj = OpenSSL::X509::Name.parse(subject)
82
- cert = mkcert(getserial(subj), subj, nil, key.public_key, days, altnames, name_constraints, become_ca)
85
+ cert = mkcert(getserial(subj), subj, nil, key.public_key, days,
86
+ altnames, key_usages, name_constraints, become_ca)
83
87
  cert.sign(key, signature(hash))
84
88
  rescue Exception => e
85
89
  raise "Self-signed certificate #{subject} creation failed: #{e.message}"
86
90
  end
91
+ end
92
+ key.to_pem + cert.to_pem
93
+ end
87
94
 
88
- key.to_pem + cert.to_pem
95
+ def render(output,render_options={})
96
+ if render_options['keyonly']
97
+ OpenSSL::PKey::RSA.new(output).to_pem
98
+ elsif render_options['certonly']
99
+ OpenSSL::X509::Certificate.new(output).to_pem
100
+ elsif render_options['publickeyonly']
101
+ OpenSSL::PKey::RSA.new(output).public_key.to_pem
102
+ else
103
+ super(output,render_options)
89
104
  end
90
105
  end
91
- private
92
106
 
107
+ private
93
108
  # nice help: https://gist.github.com/mitfik/1922961
94
109
 
95
110
  def signature(hash = 'sha2')
@@ -120,7 +135,7 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
120
135
  request
121
136
  end
122
137
 
123
- def mkcert(serial,subject,issuer,public_key,days,altnames, name_constraints = [], become_ca = false)
138
+ def mkcert(serial,subject,issuer,public_key,days,altnames, key_usages = nil, name_constraints = [], become_ca = false)
124
139
  cert = OpenSSL::X509::Certificate.new
125
140
  issuer = cert if issuer == nil
126
141
  cert.subject = subject
@@ -138,14 +153,18 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
138
153
 
139
154
  if become_ca
140
155
  cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true)
141
- cert.add_extension ef.create_extension("keyUsage", "keyCertSign, cRLSign, nonRepudiation, digitalSignature, keyEncipherment", true)
156
+ unless (ku = key_usages || ca_key_usages).empty?
157
+ cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
158
+ end
142
159
  if name_constraints && !name_constraints.empty?
143
160
  cert.add_extension ef.create_extension("nameConstraints","permitted;DNS:#{name_constraints.join(',permitted;DNS:')}",true)
144
161
  end
145
162
  else
146
163
  cert.add_extension ef.create_extension("subjectAltName", altnames, true) unless altnames.empty?
147
164
  cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true)
148
- cert.add_extension ef.create_extension("keyUsage", "nonRepudiation, digitalSignature, keyEncipherment", true)
165
+ unless (ku = key_usages || cert_key_usages).empty?
166
+ cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
167
+ end
149
168
  end
150
169
  cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
151
170
 
@@ -169,4 +188,12 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
169
188
  serials = all_serials(ca) << serial
170
189
  trocla.set_password("#{ca}_all_serials",'plain',YAML.dump(serials))
171
190
  end
191
+
192
+ def cert_key_usages
193
+ ['nonRepudiation', 'digitalSignature', 'keyEncipherment']
194
+ end
195
+ def ca_key_usages
196
+ ['keyCertSign', 'cRLSign', 'nonRepudiation',
197
+ 'digitalSignature', 'keyEncipherment' ]
198
+ end
172
199
  end
data/lib/trocla/store.rb CHANGED
@@ -6,6 +6,12 @@ class Trocla::Store
6
6
  @trocla = trocla
7
7
  end
8
8
 
9
+ # closes the store
10
+ # when called do whatever "closes" your
11
+ # store, e.g. close database connections.
12
+ def close
13
+ end
14
+
9
15
  # should return value for key & format
10
16
  # returns nil if nothing or a nil value
11
17
  # was found.
@@ -44,6 +50,16 @@ class Trocla::Store
44
50
  format.nil? ? (delete_all(key)||{}) : delete_format(key,format)
45
51
  end
46
52
 
53
+ # returns all formats for a key
54
+ def formats(key)
55
+ raise 'not implemented'
56
+ end
57
+
58
+ # def searches for a key
59
+ def search(key)
60
+ raise 'not implemented'
61
+ end
62
+
47
63
  private
48
64
  # sets a new plain value
49
65
  # *must* invalidate all
@@ -19,6 +19,15 @@ class Trocla::Stores::Memory < Trocla::Store
19
19
  set_expires(key,options['expires'])
20
20
  end
21
21
 
22
+ def formats(key)
23
+ memory[key].empty? ? nil : memory[key].keys
24
+ end
25
+
26
+ def search(key)
27
+ r = memory.keys.grep(/#{key}/)
28
+ r.empty? ? nil : r
29
+ end
30
+
22
31
  private
23
32
  def set_plain(key,value,options)
24
33
  memory[key] = { 'plain' => value }
@@ -10,10 +10,25 @@ class Trocla::Stores::Moneta < Trocla::Store
10
10
  @moneta = Moneta.new(store_config['adapter'],adapter_options)
11
11
  end
12
12
 
13
+ def close
14
+ moneta.close
15
+ end
16
+
13
17
  def get(key,format)
14
18
  moneta.fetch(key, {})[format]
15
19
  end
16
20
 
21
+ def formats(key)
22
+ r = moneta.fetch(key)
23
+ r.nil? ? nil : r.keys
24
+ end
25
+
26
+ def search(key)
27
+ raise 'The search option is not available for any adapter other than Sequel or YAML' unless store_config['adapter'] == :Sequel || store_config['adapter'] == :YAML
28
+ r = search_keys(key)
29
+ r.empty? ? nil : r
30
+ end
31
+
17
32
  private
18
33
  def set_plain(key,value,options)
19
34
  h = { 'plain' => value }
@@ -51,4 +66,19 @@ class Trocla::Stores::Moneta < Trocla::Store
51
66
  end
52
67
  res
53
68
  end
69
+ def search_keys(key)
70
+ _moneta = Moneta.new(store_config['adapter'], (store_config['adapter_options']||{}).merge({ :expires => false }))
71
+ a = []
72
+ if store_config['adapter'] == :Sequel
73
+ keys = _moneta.adapter.backend[:trocla].select_order_map {from_base64(:k)}
74
+ elsif store_config['adapter'] == :YAML
75
+ keys = _moneta.adapter.backend.transaction(true) { _moneta.adapter.backend.roots }
76
+ end
77
+ _moneta.close
78
+ regexp = Regexp.new("#{key}")
79
+ keys.each do |k|
80
+ a << k if regexp.match(k)
81
+ end
82
+ a
83
+ end
54
84
  end
@@ -0,0 +1,50 @@
1
+ # the default vault based store
2
+ class Trocla::Stores::Vault < Trocla::Store
3
+ attr_reader :vault, :mount
4
+ def initialize(config,trocla)
5
+ super(config,trocla)
6
+ require 'vault'
7
+ @mount = (config.delete(:mount) || 'kv')
8
+ # load expire support by default
9
+ @vault = Vault::Client.new(config)
10
+ end
11
+
12
+ def close
13
+ end
14
+
15
+ def get(key,format)
16
+ read(key)[format.to_sym]
17
+ end
18
+
19
+ def formats(key)
20
+ read(key).keys
21
+ end
22
+
23
+ private
24
+ def read(key)
25
+ k = vault.kv(mount).read(key)
26
+ k.nil? ? {} : k.data
27
+ end
28
+
29
+ def write(key, value)
30
+ vault.kv(mount).write(key, value)
31
+ end
32
+
33
+ def set_plain(key,value,options)
34
+ set_format(key,'plain',value,options)
35
+ end
36
+
37
+ def set_format(key,format,value,options)
38
+ write(key, read(key).merge({format.to_sym => value}))
39
+ end
40
+
41
+ def delete_all(key)
42
+ vault.kv(mount).delete(key)
43
+ end
44
+
45
+ def delete_format(key,format)
46
+ old = read(key)
47
+ write(key, old.reject { |k,v| k == format.to_sym })
48
+ old[format.to_sym]
49
+ end
50
+ end