trocla 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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