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/trocla/util.rb CHANGED
@@ -24,6 +24,7 @@ class Trocla
24
24
  'numeric' => numeric,
25
25
  'hexadecimal' => hexadecimal,
26
26
  'consolesafe' => consolesafe,
27
+ 'typesafe' => typesafe,
27
28
  }
28
29
  h.each { |k, v| h[k] = v.uniq }
29
30
  end
@@ -50,6 +51,9 @@ class Trocla
50
51
  def numeric
51
52
  @numeric ||= ('0'..'9').to_a
52
53
  end
54
+ def typesafe
55
+ @typesafe ||= ('a'..'x').to_a - ['i'] - ['l'] + ('A'..'X').to_a - ['I'] - ['L'] + ('1'..'9').to_a
56
+ end
53
57
  def special_chars
54
58
  @special_chars ||= "*()&![]{}-".split(//)
55
59
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'jruby' if Object.const_defined?(:RUBY_ENGINE) and RUBY_ENGINE == 'jruby'
3
4
  require 'rspec'
4
5
  require 'rspec/pending_for'
5
6
  require 'yaml'
@@ -35,6 +36,11 @@ RSpec.shared_examples "encryption_basics" do
35
36
  expect(@trocla.get_password('some_pass', 'plain')).to eql('super secret')
36
37
  end
37
38
 
39
+ it "resets passwords" do
40
+ @trocla.set_password('some_pass', 'plain', 'super secret')
41
+ expect(@trocla.reset_password('some_pass', 'plain')).not_to eql('super secret')
42
+ end
43
+
38
44
  end
39
45
  describe 'deleting' do
40
46
  it "plain" do
@@ -225,7 +231,13 @@ RSpec.shared_examples 'store_validation' do |store|
225
231
  end
226
232
 
227
233
  def default_config
228
- @default_config ||= YAML.load(File.read(File.expand_path(base_dir+'/lib/trocla/default_config.yaml')))
234
+ @default_config ||= begin
235
+ config_path = [
236
+ File.expand_path(base_dir+'/lib/trocla/default_config.yaml'),
237
+ File.expand_path(File.dirname($LOADED_FEATURES.grep(/trocla.rb/)[0])+'/trocla/default_config.yaml'),
238
+ ].find { |p| File.exists?(p) }
239
+ YAML.load(File.read(config_path))
240
+ end
229
241
  end
230
242
 
231
243
  def test_config
@@ -282,3 +294,9 @@ end
282
294
  def remove_yaml_store
283
295
  File.unlink(trocla_yaml_file)
284
296
  end
297
+ class Trocla::Formats::Sleep < Trocla::Formats::Base
298
+ def format(plain_password,options={})
299
+ sleep options['sleep'] ||= 0
300
+ (options['sleep'] + 1 ).times.collect{ plain_password }.join(' ')
301
+ end
302
+ end
@@ -0,0 +1,52 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe "Trocla::Format::Sshkey" do
4
+
5
+ before(:each) do
6
+ expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
7
+ @trocla = Trocla.new
8
+ end
9
+
10
+ let(:sshkey_options) do
11
+ {
12
+ 'type' => 'rsa',
13
+ 'bits' => 4096,
14
+ 'comment' => 'My ssh key'
15
+ }
16
+ end
17
+
18
+ describe "sshkey" do
19
+ it "is able to create an ssh keypair without options" do
20
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', {})
21
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
22
+ expect(sshkey).to match(/ssh-/)
23
+ end
24
+
25
+ it "is able to create an ssh keypair with options" do
26
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', sshkey_options)
27
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
28
+ expect(sshkey).to match(/ssh-/)
29
+ expect(sshkey).to end_with('My ssh key')
30
+ end
31
+
32
+ it 'supports fetching only the priv key' do
33
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'privonly' => true }})
34
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
35
+ expect(sshkey).not_to match(/ssh-/)
36
+ end
37
+
38
+ it 'supports fetching only the pub key' do
39
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'pubonly' => true }})
40
+ expect(sshkey).to start_with('ssh-rsa')
41
+ expect(sshkey).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
42
+ end
43
+
44
+ it "is able to create an ssh keypair with a passphrase" do
45
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'passphrase' => 'spec' })
46
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
47
+ expect(sshkey).to match(/ssh-/)
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -42,9 +42,11 @@ describe "Trocla::Format::X509" do
42
42
  # default size
43
43
  # https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
44
44
  expect(cert.public_key.n.num_bytes * 8).to eq(4096)
45
- expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
45
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
46
46
  # it's a self signed cert and NOT a CA
47
- expect(verify(cert,cert)).to be false
47
+ # Change of behavior on openssl side: https://github.com/openssl/openssl/issues/15146
48
+ validates_self_even_if_no_ca = Gem::Version.new(%x{openssl version}.split(' ')[1]) > Gem::Version.new('1.1.1g')
49
+ expect(verify(cert,cert)).to be validates_self_even_if_no_ca
48
50
 
49
51
  v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
50
52
  expect(v).to eq('CA:FALSE')
@@ -60,7 +62,7 @@ describe "Trocla::Format::X509" do
60
62
  ca = OpenSSL::X509::Certificate.new(ca_str)
61
63
  # selfsigned?
62
64
  expect(ca.issuer.to_s).to eq(ca.subject.to_s)
63
- expect((Date.parse(ca.not_after.to_s) - Date.today).to_i).to eq(365)
65
+ expect((Date.parse(ca.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
64
66
  expect(verify(ca,ca)).to be true
65
67
 
66
68
  v = ca.extensions.find{|e| e.oid == 'basicConstraints' }.value
@@ -69,6 +71,47 @@ describe "Trocla::Format::X509" do
69
71
  expect(ku).to match(/Certificate Sign/)
70
72
  expect(ku).to match(/CRL Sign/)
71
73
  end
74
+ it "is able to create a self signed cert without any keyUsage restrictions" do
75
+ cert_str = @trocla.password('my_shiny_selfsigned_without restrictions', 'x509', {
76
+ 'CN' => 'This is my self-signed certificate',
77
+ 'key_usages' => [],
78
+ })
79
+ cert = OpenSSL::X509::Certificate.new(cert_str)
80
+ # selfsigned?
81
+ expect(cert.issuer.to_s).to eq(cert.subject.to_s)
82
+ # default size
83
+ # https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
84
+ expect(cert.public_key.n.num_bytes * 8).to eq(4096)
85
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
86
+ # it's a self signed cert and NOT a CA, but has no keyUsage limitation
87
+ expect(verify(cert,cert)).to be true
88
+
89
+ v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
90
+ expect(v).to_not eq('CA:TRUE')
91
+ expect(cert.extensions.find{|e| e.oid == 'keyUsage' }).to be_nil
92
+ end
93
+
94
+ it "is able to create a self signed cert with custom keyUsage restrictions" do
95
+ cert_str = @trocla.password('my_shiny_selfsigned_without restrictions', 'x509', {
96
+ 'CN' => 'This is my self-signed certificate',
97
+ 'key_usages' => [ 'cRLSign', ],
98
+ })
99
+ cert = OpenSSL::X509::Certificate.new(cert_str)
100
+ # selfsigned?
101
+ expect(cert.issuer.to_s).to eq(cert.subject.to_s)
102
+ # default size
103
+ # https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
104
+ expect(cert.public_key.n.num_bytes * 8).to eq(4096)
105
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
106
+ # it's a self signed cert and NOT a CA, as it's key is restricted to only CRL Sign
107
+ expect(verify(cert,cert)).to be false
108
+
109
+ v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
110
+ expect(v).to_not eq('CA:TRUE')
111
+ ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
112
+ expect(ku).to match(/CRL Sign/)
113
+ expect(ku).not_to match(/Certificate Sign/)
114
+ end
72
115
 
73
116
  end
74
117
  describe "x509 signed by a ca" do
@@ -80,7 +123,7 @@ describe "Trocla::Format::X509" do
80
123
  cert_str = @trocla.password('mycert', 'x509', cert_options)
81
124
  cert = OpenSSL::X509::Certificate.new(cert_str)
82
125
  expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
83
- expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
126
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
84
127
  expect(verify(@ca,cert)).to be true
85
128
 
86
129
  v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
@@ -90,6 +133,30 @@ describe "Trocla::Format::X509" do
90
133
  expect(ku).not_to match(/CRL Sign/)
91
134
  end
92
135
 
136
+ it 'supports fetching only the key' do
137
+ cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'keyonly' => true }))
138
+ expect(cert_str).not_to match(/-----BEGIN CERTIFICATE-----/)
139
+ expect(cert_str).to match(/-----BEGIN RSA PRIVATE KEY-----/)
140
+ end
141
+ it 'supports fetching only the publickey' do
142
+ pkey_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'publickeyonly' => true }))
143
+ expect(pkey_str).not_to match(/-----BEGIN CERTIFICATE-----/)
144
+ expect(pkey_str).to match(/-----BEGIN PUBLIC KEY-----/)
145
+ end
146
+ it 'supports fetching only the cert' do
147
+ cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
148
+ expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
149
+ expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
150
+ end
151
+ it 'supports fetching only the cert even a second time' do
152
+ cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
153
+ expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
154
+ expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
155
+ cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
156
+ expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
157
+ expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
158
+ end
159
+
93
160
  it 'does not simply increment the serial' do
94
161
  cert_str = @trocla.password('mycert', 'x509', cert_options)
95
162
  cert1 = OpenSSL::X509::Certificate.new(cert_str)
@@ -107,7 +174,7 @@ describe "Trocla::Format::X509" do
107
174
  }))
108
175
  cert = OpenSSL::X509::Certificate.new(cert_str)
109
176
  expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
110
- expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
177
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
111
178
  expect(verify(@ca,cert)).to be true
112
179
 
113
180
  expect(cert.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:TRUE')
@@ -123,7 +190,7 @@ describe "Trocla::Format::X509" do
123
190
  }))
124
191
  ca2 = OpenSSL::X509::Certificate.new(ca2_str)
125
192
  expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
126
- expect((Date.parse(ca2.not_after.to_s) - Date.today).to_i).to eq(365)
193
+ expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
127
194
  pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
128
195
  expect(verify(@ca,ca2)).to be true
129
196
  end
@@ -143,7 +210,7 @@ describe "Trocla::Format::X509" do
143
210
  valid_cert = OpenSSL::X509::Certificate.new(valid_cert_str)
144
211
  expect(valid_cert.issuer.to_s).to eq(ca2.subject.to_s)
145
212
  expect(verify([@ca,ca2],valid_cert)).to be true
146
- expect((Date.parse(valid_cert.not_after.to_s) - Date.today).to_i).to eq(365)
213
+ expect((Date.parse(valid_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
147
214
 
148
215
  false_cert_str = @trocla.password('myfalseexamplecert','x509', {
149
216
  'subject' => '/C=ZZ/O=Trocla Inc./CN=foo.example.net/emailAddress=example@example.com',
@@ -153,7 +220,7 @@ describe "Trocla::Format::X509" do
153
220
  false_cert = OpenSSL::X509::Certificate.new(false_cert_str)
154
221
  expect(false_cert.issuer.to_s).to eq(ca2.subject.to_s)
155
222
  expect(verify([@ca,ca2],false_cert)).to be false
156
- expect((Date.parse(false_cert.not_after.to_s) - Date.today).to_i).to eq(365)
223
+ expect((Date.parse(false_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
157
224
  end
158
225
 
159
226
  it 'supports simple name constraints for CAs with leading dots' do
@@ -163,7 +230,7 @@ describe "Trocla::Format::X509" do
163
230
  }))
164
231
  ca2 = OpenSSL::X509::Certificate.new(ca2_str)
165
232
  expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
166
- expect((Date.parse(ca2.not_after.to_s) - Date.today).to_i).to eq(365)
233
+ expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
167
234
  pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
168
235
  expect(verify(@ca,ca2)).to be true
169
236
  end
@@ -180,14 +247,14 @@ describe "Trocla::Format::X509" do
180
247
  })
181
248
  valid_cert = OpenSSL::X509::Certificate.new(valid_cert_str)
182
249
  expect(valid_cert.issuer.to_s).to eq(ca2.subject.to_s)
183
- expect((Date.parse(valid_cert.not_after.to_s) - Date.today).to_i).to eq(365)
250
+ expect((Date.parse(valid_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
184
251
  # workaround broken openssl
185
- if %x{openssl version} =~ /1\.0\.[2-9]/
186
- expect(verify([@ca,ca2],valid_cert)).to be true
187
- else
252
+ if Gem::Version.new(%x{openssl version}.split(' ')[1]) < Gem::Version.new('1.0.2')
188
253
  skip_for(:engine => 'ruby',:reason => 'NameConstraints verification is broken on older openssl versions https://rt.openssl.org/Ticket/Display.html?id=3562') do
189
254
  expect(verify([@ca,ca2],valid_cert)).to be true
190
255
  end
256
+ else
257
+ expect(verify([@ca,ca2],valid_cert)).to be true
191
258
  end
192
259
 
193
260
  false_cert_str = @trocla.password('myfalseexamplecert','x509', {
@@ -196,7 +263,7 @@ describe "Trocla::Format::X509" do
196
263
  })
197
264
  false_cert = OpenSSL::X509::Certificate.new(false_cert_str)
198
265
  expect(false_cert.issuer.to_s).to eq(ca2.subject.to_s)
199
- expect((Date.parse(false_cert.not_after.to_s) - Date.today).to_i).to eq(365)
266
+ expect((Date.parse(false_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
200
267
  expect(verify([@ca,ca2],false_cert)).to be false
201
268
  end
202
269
 
@@ -206,7 +273,7 @@ describe "Trocla::Format::X509" do
206
273
  }))
207
274
  ca2 = OpenSSL::X509::Certificate.new(ca2_str)
208
275
  expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
209
- expect((Date.parse(ca2.not_after.to_s) - Date.today).to_i).to eq(365)
276
+ expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
210
277
  expect(verify(@ca,ca2)).to be true
211
278
 
212
279
  cert2_str = @trocla.password('mycert', 'x509', {
@@ -216,7 +283,7 @@ describe "Trocla::Format::X509" do
216
283
  })
217
284
  cert2 = OpenSSL::X509::Certificate.new(cert2_str)
218
285
  expect(cert2.issuer.to_s).to eq(ca2.subject.to_s)
219
- expect((Date.parse(cert2.not_after.to_s) - Date.today).to_i).to eq(365)
286
+ expect((Date.parse(cert2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
220
287
  skip_for(:engine => 'jruby',:reason => 'Chained CA validation seems to be broken on jruby atm.') do
221
288
  expect(verify([@ca,ca2],cert2)).to be true
222
289
  end
@@ -247,11 +314,13 @@ describe "Trocla::Format::X509" do
247
314
  hash_match = (defined?(RUBY_ENGINE) &&RUBY_ENGINE == 'jruby') ? 'RSA-SHA1' : 'sha1WithRSAEncryption'
248
315
  expect(cert.signature_algorithm).to eq(hash_match)
249
316
  expect(cert.not_before).to be < Time.now
250
- expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(3650)
317
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(3650)
251
318
  # https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
252
319
  expect(cert.public_key.n.num_bytes * 8).to eq(2048)
253
320
  expect(verify(@ca,cert)).to be true
254
- expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }.value).to eq('DNS:www.test, DNS:test, DNS:test1, DNS:test2, DNS:test3')
321
+ skip_for(:engine => 'jruby',:reason => 'subjectAltName represenation is broken in jruby-openssl -> https://github.com/jruby/jruby-openssl/pull/123') do
322
+ expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }.value).to eq('DNS:www.test, DNS:test, DNS:test1, DNS:test2, DNS:test3')
323
+ end
255
324
 
256
325
  expect(cert.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:FALSE')
257
326
  ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
@@ -267,7 +336,7 @@ describe "Trocla::Format::X509" do
267
336
  cert_str = @trocla.password('mycert', 'x509', co)
268
337
  cert = OpenSSL::X509::Certificate.new(cert_str)
269
338
  expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
270
- expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
339
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
271
340
  expect(verify(@ca,cert)).to be true
272
341
  expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }).to be_nil
273
342
  end
@@ -288,8 +357,26 @@ describe "Trocla::Format::X509" do
288
357
  expect(cert.subject.to_s).not_to match(/#{field}=#{co[field]}/)
289
358
  end
290
359
  expect(cert.subject.to_s).not_to match(/(Email|emailAddress)=#{co['emailAddress']}/)
291
- expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
360
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
292
361
  expect(verify(@ca,cert)).to be true
293
362
  end
363
+ it "is able to create a signed cert with custom keyUsage restrictions" do
364
+ cert_str = @trocla.password('mycert_without_restrictions', 'x509', cert_options.merge({
365
+ 'CN' => 'sign only test',
366
+ 'key_usages' => [ ],
367
+ }))
368
+ cert = OpenSSL::X509::Certificate.new(cert_str)
369
+ # default size
370
+ # https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
371
+ expect(cert.public_key.n.num_bytes * 8).to eq(4096)
372
+ expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
373
+ expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
374
+ expect(verify(@ca,cert)).to be true
375
+
376
+ v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
377
+ expect(v).to_not eq('CA:TRUE')
378
+ expect(cert.extensions.find{|e| e.oid == 'keyUsage' }).to be_nil
379
+ end
380
+
294
381
  end
295
382
  end
@@ -36,6 +36,14 @@ describe "Trocla::Util" do
36
36
  end
37
37
  end
38
38
 
39
+ describe :typesafe_generator do
40
+ 10.times.each do |i|
41
+ it "creates random typesafe password #{i}" do
42
+ expect(Trocla::Util.random_str(12, 'typesafe')).to match(/^[1-9a-hj-km-xA-HJ-KM-X]{12}$/)
43
+ end
44
+ end
45
+ end
46
+
39
47
  describe :salt do
40
48
  10.times.each do |i|
41
49
  it "contains only characters and numbers #{i}" do
data/spec/trocla_spec.rb CHANGED
@@ -1,147 +1,274 @@
1
+ # -- encoding : utf-8 --
1
2
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
3
 
3
4
  describe "Trocla" do
4
5
 
5
6
  before(:each) do
6
7
  expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
7
- @trocla = Trocla.new
8
8
  end
9
-
10
- describe "password" do
11
- it "generates random passwords by default" do
12
- expect(@trocla.password('random1','plain')).not_to eq(@trocla.password('random2','plain'))
9
+ context 'in normal usage with' do
10
+ before(:each) do
11
+ @trocla = Trocla.new
12
+ @trocla.password('init','plain')
13
13
  end
14
14
 
15
- it "generates passwords of length #{default_config['options']['length']}" do
16
- expect(@trocla.password('random1','plain').length).to eq(default_config['options']['length'])
17
- end
15
+ describe "password" do
16
+ it "generates random passwords by default" do
17
+ expect(@trocla.password('random1','plain')).not_to eq(@trocla.password('random2','plain'))
18
+ end
19
+
20
+ it "generates passwords of length #{default_config['options']['length']}" do
21
+ expect(@trocla.password('random1','plain').length).to eq(default_config['options']['length'])
22
+ end
23
+
24
+ Trocla::Formats.all.each do |format|
25
+ describe "#{format} password format" do
26
+ it "retursn a password hashed in the #{format} format" do
27
+ expect(@trocla.password('some_test',format,format_options[format])).not_to be_empty
28
+ end
29
+
30
+ it "returns the same hashed for the #{format} format on multiple invocations" do
31
+ expect(round1=@trocla.password('some_test',format,format_options[format])).not_to be_empty
32
+ expect(@trocla.password('some_test',format,format_options[format])).to eq(round1)
33
+ end
18
34
 
19
- Trocla::Formats.all.each do |format|
20
- describe "#{format} password format" do
21
- it "retursn a password hashed in the #{format} format" do
22
- expect(@trocla.password('some_test',format,format_options[format])).not_to be_empty
35
+ it "also stores the plain password by default" do
36
+ pwd = @trocla.password('some_test','plain')
37
+ expect(pwd).not_to be_empty
38
+ expect(pwd.length).to eq(16)
39
+ end
23
40
  end
41
+ end
42
+
43
+ Trocla::Formats.all.reject{|f| f == 'plain' }.each do |format|
44
+ it "raises an exception if not a random password is asked but plain password is not present for format #{format}" do
45
+ expect{@trocla.password('not_random',format, 'random' => false)}.to raise_error(/Password must be present as plaintext/)
46
+ end
47
+ end
24
48
 
25
- it "returns the same hashed for the #{format} format on multiple invocations" do
26
- expect(round1=@trocla.password('some_test',format,format_options[format])).not_to be_empty
27
- expect(@trocla.password('some_test',format,format_options[format])).to eq(round1)
49
+ describe 'with profiles' do
50
+ it 'raises an exception on unknown profile' do
51
+ expect{@trocla.password('no profile known','plain',
52
+ 'profiles' => 'unknown_profile') }.to raise_error(/No such profile unknown_profile defined/)
53
+ end
54
+
55
+ it 'takes a profile and merge its options' do
56
+ pwd = @trocla.password('some_test','plain', 'profiles' => 'rootpw')
57
+ expect(pwd).not_to be_empty
58
+ expect(pwd.length).to eq(32)
59
+ expect(pwd).to_not match(/[={}\[\]\?%\*()&!]+/)
28
60
  end
29
61
 
30
- it "also stores the plain password by default" do
31
- pwd = @trocla.password('some_test','plain')
62
+ it 'is possible to combine profiles but first profile wins' do
63
+ pwd = @trocla.password('some_test1','plain', 'profiles' => ['rootpw','login'])
64
+ expect(pwd).not_to be_empty
65
+ expect(pwd.length).to eq(32)
66
+ expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
67
+ end
68
+ it 'is possible to combine profiles but first profile wins 2' do
69
+ pwd = @trocla.password('some_test2','plain', 'profiles' => ['login','mysql'])
32
70
  expect(pwd).not_to be_empty
33
71
  expect(pwd.length).to eq(16)
72
+ expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
73
+ end
74
+ it 'is possible to combine profiles but first profile wins 3' do
75
+ pwd = @trocla.password('some_test3','plain', 'profiles' => ['mysql','login'])
76
+ expect(pwd).not_to be_empty
77
+ expect(pwd.length).to eq(32)
78
+ expect(pwd).to match(/[+%\/@=\?_.,:]+/)
34
79
  end
35
80
  end
36
81
  end
37
82
 
38
- Trocla::Formats.all.reject{|f| f == 'plain' }.each do |format|
39
- it "raises an exception if not a random password is asked but plain password is not present for format #{format}" do
40
- expect{@trocla.password('not_random',format, 'random' => false)}.to raise_error(/Password must be present as plaintext/)
41
- end
42
- end
83
+ describe "set_password" do
84
+ it "resets hashed passwords on a new plain password" do
85
+ expect(@trocla.password('set_test','mysql')).not_to be_empty
86
+ expect(@trocla.get_password('set_test','mysql')).not_to be_nil
87
+ expect(old_plain=@trocla.password('set_test','mysql')).not_to be_empty
43
88
 
44
- describe 'with profiles' do
45
- it 'raises an exception on unknown profile' do
46
- expect{@trocla.password('no profile known','plain',
47
- 'profiles' => 'unknown_profile') }.to raise_error(/No such profile unknown_profile defined/)
89
+ expect(@trocla.set_password('set_test','plain','foobar')).not_to eq(old_plain)
90
+ expect(@trocla.get_password('set_test','mysql')).to be_nil
48
91
  end
49
92
 
50
- it 'takes a profile and merge its options' do
51
- pwd = @trocla.password('some_test','plain', 'profiles' => 'rootpw')
52
- expect(pwd).not_to be_empty
53
- expect(pwd.length).to eq(32)
54
- expect(pwd).to_not match(/[={}\[\]\?%\*()&!]+/)
55
- end
93
+ it "otherwise updates only the hash" do
94
+ expect(mysql = @trocla.password('set_test2','mysql')).not_to be_empty
95
+ expect(md5crypt = @trocla.password('set_test2','md5crypt')).not_to be_empty
96
+ expect(plain = @trocla.get_password('set_test2','plain')).not_to be_empty
56
97
 
57
- it 'is possible to combine profiles but first profile wins' do
58
- pwd = @trocla.password('some_test','plain', 'profiles' => ['rootpw','login'])
59
- expect(pwd).not_to be_empty
60
- expect(pwd.length).to eq(32)
61
- expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
98
+ expect(new_mysql = @trocla.set_password('set_test2','mysql','foo')).not_to eql(mysql)
99
+ expect(@trocla.get_password('set_test2','mysql')).to eq(new_mysql)
100
+ expect(@trocla.get_password('set_test2','md5crypt')).to eq(md5crypt)
101
+ expect(@trocla.get_password('set_test2','plain')).to eq(plain)
62
102
  end
63
- it 'is possible to combine profiles but first profile wins 2' do
64
- pwd = @trocla.password('some_test','plain', 'profiles' => ['login','mysql'])
65
- expect(pwd).not_to be_empty
66
- expect(pwd.length).to eq(16)
67
- expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
68
- end
69
- it 'is possible to combine profiles but first profile wins 3' do
70
- pwd = @trocla.password('some_test','plain', 'profiles' => ['mysql','login'])
71
- expect(pwd).not_to be_empty
72
- expect(pwd.length).to eq(32)
73
- expect(pwd).to match(/[+%\/@=\?_.,:]+/)
103
+
104
+ it 'is able to set password with umlauts and other UTF-8 charcters' do
105
+ expect(myumlaut = @trocla.set_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
106
+ expect(@trocla.get_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
74
107
  end
75
108
  end
76
- end
77
109
 
78
- describe "set_password" do
79
- it "resets hashed passwords on a new plain password" do
80
- expect(@trocla.password('set_test','mysql')).not_to be_empty
81
- expect(@trocla.get_password('set_test','mysql')).not_to be_nil
82
- expect(old_plain=@trocla.password('set_test','mysql')).not_to be_empty
110
+ describe "reset_password" do
111
+ it "resets a password" do
112
+ plain1 = @trocla.password('reset_pwd','plain')
113
+ plain2 = @trocla.reset_password('reset_pwd','plain')
83
114
 
84
- expect(@trocla.set_password('set_test','plain','foobar')).not_to eq(old_plain)
85
- expect(@trocla.get_password('set_test','mysql')).to be_nil
86
- end
115
+ expect(plain1).not_to eq(plain2)
116
+ end
87
117
 
88
- it "otherwise updates only the hash" do
89
- expect(mysql = @trocla.password('set_test2','mysql')).not_to be_empty
90
- expect(md5crypt = @trocla.password('set_test2','md5crypt')).not_to be_empty
91
- expect(plain = @trocla.get_password('set_test2','plain')).not_to be_empty
118
+ it "does not reset other formats" do
119
+ expect(mysql = @trocla.password('reset_pwd2','mysql')).not_to be_empty
120
+ expect(md5crypt1 = @trocla.password('reset_pwd2','md5crypt')).not_to be_empty
92
121
 
93
- expect(new_mysql = @trocla.set_password('set_test2','mysql','foo')).not_to eql(mysql)
94
- expect(@trocla.get_password('set_test2','mysql')).to eq(new_mysql)
95
- expect(@trocla.get_password('set_test2','md5crypt')).to eq(md5crypt)
96
- expect(@trocla.get_password('set_test2','plain')).to eq(plain)
97
- end
98
- end
122
+ expect(md5crypt2 = @trocla.reset_password('reset_pwd2','md5crypt')).not_to be_empty
123
+ expect(md5crypt2).not_to eq(md5crypt1)
99
124
 
100
- describe "reset_password" do
101
- it "resets a password" do
102
- plain1 = @trocla.password('reset_pwd','plain')
103
- plain2 = @trocla.reset_password('reset_pwd','plain')
125
+ expect(@trocla.get_password('reset_pwd2','mysql')).to eq(mysql)
126
+ end
127
+ end
104
128
 
105
- expect(plain1).not_to eq(plain2)
129
+ describe "search_key" do
130
+ it "search a specific key" do
131
+ keys = ['search_key','search_key1','key_search','key_search2']
132
+ keys.each do |k|
133
+ @trocla.password(k,'plain')
134
+ end
135
+ expect(@trocla.search_key('search_key1').length).to eq(1)
136
+ end
137
+ it "ensure search regex is ok" do
138
+ keys = ['search_key2','search_key3','key_search2','key_search4']
139
+ keys.each do |k|
140
+ @trocla.password(k,'plain')
141
+ end
142
+ expect(@trocla.search_key('key').length).to eq(4)
143
+ expect(@trocla.search_key('^search').length).to eq(2)
144
+ expect(@trocla.search_key('ch.*3').length).to eq(1)
145
+ expect(@trocla.search_key('ch.*[3-4]$').length).to eq(2)
146
+ expect(@trocla.search_key('ch.*1')).to be_nil
147
+ end
106
148
  end
107
149
 
108
- it "does not reset other formats" do
109
- expect(mysql = @trocla.password('reset_pwd2','mysql')).not_to be_empty
110
- expect(md5crypt1 = @trocla.password('reset_pwd2','md5crypt')).not_to be_empty
150
+ describe "list_format" do
151
+ it "list available formats for key" do
152
+ formats = ['plain','mysql']
153
+ formats.each do |f|
154
+ @trocla.password('list_key',f)
155
+ end
156
+ expect(@trocla.available_format('list_key')).to eq(formats)
157
+ end
158
+ it "no return if key doesn't exist" do
159
+ expect(@trocla.available_format('list_key1')).to be_nil
160
+ end
161
+ end
111
162
 
112
- expect(md5crypt2 = @trocla.reset_password('reset_pwd2','md5crypt')).not_to be_empty
113
- expect(md5crypt2).not_to eq(md5crypt1)
163
+ describe "delete_password" do
164
+ it "deletes all passwords if no format is given" do
165
+ expect(@trocla.password('delete_test1','mysql')).not_to be_nil
166
+ expect(@trocla.get_password('delete_test1','plain')).not_to be_nil
114
167
 
115
- expect(@trocla.get_password('reset_pwd2','mysql')).to eq(mysql)
116
- end
117
- end
168
+ @trocla.delete_password('delete_test1')
169
+ expect(@trocla.get_password('delete_test1','plain')).to be_nil
170
+ expect(@trocla.get_password('delete_test1','mysql')).to be_nil
171
+ end
118
172
 
119
- describe "delete_password" do
120
- it "deletes all passwords if no format is given" do
121
- expect(@trocla.password('delete_test1','mysql')).not_to be_nil
122
- expect(@trocla.get_password('delete_test1','plain')).not_to be_nil
173
+ it "deletes only a given format" do
174
+ expect(@trocla.password('delete_test2','mysql')).not_to be_nil
175
+ expect(@trocla.get_password('delete_test2','plain')).not_to be_nil
123
176
 
124
- @trocla.delete_password('delete_test1')
125
- expect(@trocla.get_password('delete_test1','plain')).to be_nil
126
- expect(@trocla.get_password('delete_test1','mysql')).to be_nil
127
- end
177
+ @trocla.delete_password('delete_test2','plain')
178
+ expect(@trocla.get_password('delete_test2','plain')).to be_nil
179
+ expect(@trocla.get_password('delete_test2','mysql')).not_to be_nil
180
+ end
128
181
 
129
- it "deletes only a given format" do
130
- expect(@trocla.password('delete_test2','mysql')).not_to be_nil
131
- expect(@trocla.get_password('delete_test2','plain')).not_to be_nil
182
+ it "deletes only a given non-plain format" do
183
+ expect(@trocla.password('delete_test3','mysql')).not_to be_nil
184
+ expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
132
185
 
133
- @trocla.delete_password('delete_test2','plain')
134
- expect(@trocla.get_password('delete_test2','plain')).to be_nil
135
- expect(@trocla.get_password('delete_test2','mysql')).not_to be_nil
186
+ @trocla.delete_password('delete_test3','mysql')
187
+ expect(@trocla.get_password('delete_test3','mysql')).to be_nil
188
+ expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
189
+ end
136
190
  end
137
191
 
138
- it "deletes only a given non-plain format" do
139
- expect(@trocla.password('delete_test3','mysql')).not_to be_nil
140
- expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
192
+ context 'concurrent access' do
193
+ context 'on expensive flagged formats' do
194
+ before(:each) do
195
+ expect(Trocla::Formats).to receive(:[]).with('sleep').at_least(:once).and_return(Trocla::Formats::Sleep)
196
+ expect(Trocla::Formats::Sleep).to receive(:expensive?).at_least(:once).and_return(true)
197
+ expect(Trocla::Formats).to receive(:available?).with('sleep').at_least(:once).and_return(true)
198
+ end
199
+ it 'should not overwrite a value if it takes longer' do
200
+ t1 = Thread.new{ @trocla.password('threadpwd','sleep','sleep' => 4) }
201
+ t2 = Thread.new{ @trocla.password('threadpwd','sleep','sleep' => 1) }
202
+ pwd1 = t1.value
203
+ pwd2 = t2.value
204
+ real_value = @trocla.password('threadpwd','sleep')
205
+ # as t2 finished first this should win
206
+ expect(pwd1).to eql(pwd2)
207
+ expect(real_value).to eql(pwd1)
208
+ expect(real_value).to eql(pwd2)
209
+ end
210
+ end
211
+ context 'on inexpensive flagged formats' do
212
+ before(:each) do
213
+ expect(Trocla::Formats).to receive(:[]).with('sleep').at_least(:once).and_return(Trocla::Formats::Sleep)
214
+ expect(Trocla::Formats::Sleep).to receive(:expensive?).at_least(:once).and_return(false)
215
+ expect(Trocla::Formats).to receive(:available?).with('sleep').at_least(:once).and_return(true)
216
+ end
217
+ it 'should not overwrite a value if it takes longer' do
218
+ t1 = Thread.new{ @trocla.password('threadpwd_inexp','sleep','sleep' => 4) }
219
+ t2 = Thread.new{ @trocla.password('threadpwd_inexp','sleep','sleep' => 1) }
220
+ pwd1 = t1.value
221
+ pwd2 = t2.value
222
+ real_value = @trocla.password('threadpwd_inexp','sleep')
223
+ # as t2 finished first but the format is inexpensive it gets overwritten
224
+ expect(pwd1).not_to eql(pwd2)
225
+ expect(real_value).to eql(pwd1)
226
+ expect(real_value).not_to eql(pwd2)
227
+ end
228
+ end
229
+ context 'real world example' do
230
+ it 'should store the quicker one' do
231
+ t1 = Thread.new{ @trocla.password('threadpwd_real','bcrypt','cost' => 17) }
232
+ t2 = Thread.new{ @trocla.password('threadpwd_real','bcrypt') }
233
+ pwd1 = t1.value
234
+ pwd2 = t2.value
235
+ real_value = @trocla.password('threadpwd_real','bcrypt')
236
+ # t2 should still win but both should be the same
237
+ expect(pwd1).to eql(pwd2)
238
+ expect(real_value).to eql(pwd1)
239
+ expect(real_value).to eql(pwd2)
240
+ end
241
+ it 'should store the quicker one test 2' do
242
+ t1 = Thread.new{ @trocla.password('my_shiny_selfsigned_ca', 'x509', {
243
+ 'CN' => 'This is my self-signed certificate',
244
+ 'become_ca' => false,
245
+ }) }
246
+ t2 = Thread.new{ @trocla.password('my_shiny_selfsigned_ca', 'x509', {
247
+ 'CN' => 'This is my self-signed certificate',
248
+ 'become_ca' => false,
249
+ }) }
250
+ cert1 = t1.value
251
+ cert2 = t2.value
252
+ real_value = @trocla.password('my_shiny_selfsigned_ca','x509')
253
+ # t2 should still win but both should be the same
254
+ expect(cert1).to eql(cert2)
255
+ expect(real_value).to eql(cert1)
256
+ expect(real_value).to eql(cert2)
257
+ end
258
+ end
259
+ end
141
260
 
142
- @trocla.delete_password('delete_test3','mysql')
143
- expect(@trocla.get_password('delete_test3','mysql')).to be_nil
144
- expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
261
+ end
262
+ context 'with .open' do
263
+ it 'closes the connection with a block' do
264
+ expect_any_instance_of(Trocla::Stores::Memory).to receive(:close)
265
+ Trocla.open{|t|
266
+ t.password('plain_open','plain')
267
+ }
268
+ end
269
+ it 'keeps the connection without a block' do
270
+ expect_any_instance_of(Trocla::Stores::Memory).not_to receive(:close)
271
+ Trocla.open.password('plain_open','plain')
145
272
  end
146
273
  end
147
274