trocla 0.1.2 → 0.2.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.
@@ -0,0 +1,39 @@
1
+ require 'trocla/store'
2
+ # store management
3
+ class Trocla::Stores
4
+ class << self
5
+ def [](store)
6
+ stores[store.to_s.downcase]
7
+ end
8
+
9
+ def all
10
+ @all ||= Dir[ path '*' ].collect do |store|
11
+ File.basename(store, '.rb').downcase
12
+ end
13
+ end
14
+
15
+ def available?(store)
16
+ all.include?(store.to_s.downcase)
17
+ end
18
+
19
+ private
20
+ def stores
21
+ @@stores ||= Hash.new do |hash, store|
22
+ store = store.to_s.downcase
23
+ if File.exists?(path(store))
24
+ require "trocla/stores/#{store}"
25
+ class_name = "Trocla::Stores::#{store.capitalize}"
26
+ hash[store] = (eval class_name)
27
+ else
28
+ raise "Store #{store} is not supported!"
29
+ end
30
+ end
31
+ end
32
+
33
+ def path(store)
34
+ File.expand_path(
35
+ File.join(File.dirname(__FILE__), 'stores', "#{store}.rb")
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ # a simple in memory store just as an example
2
+ class Trocla::Stores::Memory < Trocla::Store
3
+ attr_reader :memory
4
+ def initialize(config,trocla)
5
+ super(config,trocla)
6
+ @memory = Hash.new({})
7
+ end
8
+
9
+ def get(key,format)
10
+ unless expired?(key)
11
+ memory[key][format]
12
+ else
13
+ delete_all(key)
14
+ nil
15
+ end
16
+ end
17
+ def set(key,format,value,options={})
18
+ super(key,format,value,options)
19
+ set_expires(key,options['expires'])
20
+ end
21
+
22
+ private
23
+ def set_plain(key,value,options)
24
+ memory[key] = { 'plain' => value }
25
+ end
26
+
27
+ def set_format(key,format,value,options)
28
+ memory[key].merge!({ format => value })
29
+ end
30
+
31
+ def delete_all(key)
32
+ memory.delete(key)
33
+ end
34
+ def delete_format(key,format)
35
+ old_val = (h = memory[key]).delete(format)
36
+ h.empty? ? memory.delete(key) : memory[key] = h
37
+ set_expires(key,nil)
38
+ old_val
39
+ end
40
+ private
41
+ def set_expires(key,expires)
42
+ expires = memory[key]['_expires'] if expires.nil?
43
+ if expires && expires > 0
44
+ memory[key]['_expires'] = expires
45
+ memory[key]['_expires_at'] = Time.now + expires
46
+ else
47
+ memory[key].delete('_expires')
48
+ memory[key].delete('_expires_at')
49
+ end
50
+ end
51
+ def expired?(key)
52
+ memory.key?(key) &&
53
+ (a = memory[key]['_expires_at']).is_a?(Time) && \
54
+ (a < Time.now)
55
+ end
56
+ end
@@ -0,0 +1,54 @@
1
+ # the default moneta based store
2
+ class Trocla::Stores::Moneta < Trocla::Store
3
+ attr_reader :moneta
4
+ def initialize(config,trocla)
5
+ super(config,trocla)
6
+ require 'moneta'
7
+ # load expire support by default
8
+ adapter_options = { :expires => true }.merge(
9
+ store_config['adapter_options']||{})
10
+ @moneta = Moneta.new(store_config['adapter'],adapter_options)
11
+ end
12
+
13
+ def get(key,format)
14
+ moneta.fetch(key, {})[format]
15
+ end
16
+
17
+ private
18
+ def set_plain(key,value,options)
19
+ h = { 'plain' => value }
20
+ mo = moneta_options(key,options)
21
+ if options['expires'] && options['expires'] > 0
22
+ h['_expires'] = options['expires']
23
+ else
24
+ # be sure that we disable the existing
25
+ # expires if nothing is set.
26
+ mo[:expires] = false
27
+ end
28
+ moneta.store(key,h,mo)
29
+ end
30
+
31
+ def set_format(key,format,value,options)
32
+ moneta.store(key,
33
+ moneta.fetch(key,{}).merge({ format => value }),
34
+ moneta_options(key,options))
35
+ end
36
+
37
+ def delete_all(key)
38
+ moneta.delete(key)
39
+ end
40
+ def delete_format(key,format)
41
+ old_val = (h = moneta.fetch(key,{})).delete(format)
42
+ h.empty? ? moneta.delete(key) : moneta.store(key,h,moneta_options(key,{}))
43
+ old_val
44
+ end
45
+ def moneta_options(key,options)
46
+ res = {}
47
+ if options.key?('expires')
48
+ res[:expires] = options['expires']
49
+ elsif e = moneta.fetch(key, {})['_expires']
50
+ res[:expires] = e
51
+ end
52
+ res
53
+ end
54
+ end
data/lib/trocla/util.rb CHANGED
@@ -15,13 +15,18 @@ class Trocla
15
15
  private
16
16
 
17
17
  def charsets
18
- @charsets ||= {
19
- 'default' => chars,
20
- 'alphanumeric' => alphanumeric,
21
- 'shellsafe' => shellsafe,
22
- 'windowssafe' => windowssafe,
23
- 'numeric' => numeric,
24
- }
18
+ @charsets ||= begin
19
+ h = {
20
+ 'default' => chars,
21
+ 'alphanumeric' => alphanumeric,
22
+ 'shellsafe' => shellsafe,
23
+ 'windowssafe' => windowssafe,
24
+ 'numeric' => numeric,
25
+ 'hexadecimal' => hexadecimal,
26
+ 'consolesafe' => consolesafe,
27
+ }
28
+ h.each { |k, v| h[k] = v.uniq }
29
+ end
25
30
  end
26
31
 
27
32
  def chars
@@ -33,8 +38,14 @@ class Trocla
33
38
  def windowssafe
34
39
  @windowssafe ||= alphanumeric + windowssafe_chars
35
40
  end
41
+ def consolesafe
42
+ @consolesafe ||= alphanumeric + consolesafe_chars
43
+ end
44
+ def hexadecimal
45
+ @hexadecimal ||= numeric + ('a'..'f').to_a
46
+ end
36
47
  def alphanumeric
37
- @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
48
+ @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + numeric
38
49
  end
39
50
  def numeric
40
51
  @numeric ||= ('0'..'9').to_a
@@ -48,6 +59,9 @@ class Trocla
48
59
  def windowssafe_chars
49
60
  @windowssafe_chars ||= "+%/@=?_.,".split(//)
50
61
  end
62
+ def consolesafe_chars
63
+ @consolesafe_chars ||= '+.-,_'.split(//)
64
+ end
51
65
  end
52
66
  end
53
67
  end
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
3
  require 'rspec'
4
- require 'mocha'
4
+ require 'rspec/pending_for'
5
5
  require 'yaml'
6
6
  require 'trocla'
7
7
 
@@ -9,8 +9,219 @@ require 'trocla'
9
9
  # in ./support/ and its subdirectories.
10
10
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
11
 
12
- RSpec.configure do |config|
13
-
12
+ RSpec.shared_examples "encryption_basics" do
13
+ describe 'storing' do
14
+ it "random passwords" do
15
+ expect(@trocla.password('random1', 'plain').length).to eql(16)
16
+ end
17
+
18
+ it "long random passwords" do
19
+ expect(@trocla.set_password('random1_long','plain',4096.times.collect{|s| 'x' }.join('')).length).to eql(4096)
20
+ end
21
+ end
22
+
23
+ describe 'retrieve' do
24
+ it "random passwords" do
25
+ stored = @trocla.password('random1', 'plain')
26
+ retrieved = @trocla.password('random1', 'plain')
27
+ retrieved_again = @trocla.password('random1', 'plain')
28
+ expect(retrieved).to eql(stored)
29
+ expect(retrieved_again).to eql(stored)
30
+ expect(retrieved_again).to eql(retrieved)
31
+ end
32
+
33
+ it "encrypted passwords" do
34
+ @trocla.set_password('some_pass', 'plain', 'super secret')
35
+ expect(@trocla.get_password('some_pass', 'plain')).to eql('super secret')
36
+ end
37
+
38
+ end
39
+ describe 'deleting' do
40
+ it "plain" do
41
+ @trocla.set_password('some_pass', 'plain', 'super secret')
42
+ expect(@trocla.delete_password('some_pass', 'plain')).to eql('super secret')
43
+ end
44
+ it "delete formats" do
45
+ plain = @trocla.password('some_mysqlpass', 'plain')
46
+ mysql = @trocla.password('some_mysqlpass', 'mysql')
47
+ expect(@trocla.delete_password('some_mysqlpass', 'mysql')).to eql(mysql)
48
+ expect(@trocla.delete_password('some_mysqlpass', 'plain')).to eql(plain)
49
+ expect(@trocla.get_password('some_mysqlpass','plain')).to be_nil
50
+ expect(@trocla.get_password('some_mysqlpass','mysql')).to be_nil
51
+ end
52
+
53
+ it "all passwords" do
54
+ plain = @trocla.password('some_mysqlpass', 'plain')
55
+ mysql = @trocla.password('some_mysqlpass', 'mysql')
56
+ deleted = @trocla.delete_password('some_mysqlpass')
57
+ expect(deleted).to be_a_kind_of(Hash)
58
+ expect(deleted['plain']).to eql(plain)
59
+ expect(deleted['mysql']).to eql(mysql)
60
+ end
61
+ end
62
+ end
63
+ RSpec.shared_examples "verify_encryption" do
64
+ it "does not store plaintext passwords" do
65
+ @trocla.set_password('noplain', 'plain', 'plaintext_password')
66
+ expect(File.readlines(trocla_yaml_file).grep(/plaintext_password/)).to be_empty
67
+ end
68
+
69
+ it "makes sure identical passwords do not match when stored" do
70
+ @trocla.set_password('one_key', 'plain', 'super secret')
71
+ @trocla.set_password('another_key', 'plain', 'super secret')
72
+ yaml = YAML.load_file(trocla_yaml_file)
73
+ expect(yaml['one_key']['plain']).not_to eq(yaml['another_key']['plain'])
74
+ end
75
+ end
76
+
77
+ RSpec.shared_examples 'store_validation' do |store|
78
+ describe '.get' do
79
+ it { expect(store.get('some_key','plain')).to be_nil }
80
+ end
81
+ describe '.set' do
82
+ it 'stores nil values' do
83
+ store.set('some_nil_value','plain',nil)
84
+ expect(store.get('some_nil_value','plain')).to be_nil
85
+ end
86
+ it 'stores plain format' do
87
+ store.set('some_value','plain','value')
88
+ expect(store.get('some_value','plain')).to eql('value')
89
+ end
90
+ it 'stores other formats' do
91
+ store.set('some_value','foo','bla')
92
+ expect(store.get('some_value','foo')).to eql('bla')
93
+ end
94
+ it 'resets other formats on setting plain' do
95
+ store.set('some_value','foo','bla')
96
+ store.set('some_value','plain','value')
97
+ expect(store.get('some_value','plain')).to eql('value')
98
+ expect(store.get('some_value','foo')).to be_nil
99
+ end
100
+ end
101
+ describe '.delete' do
102
+ it { expect(store.delete('something','foo')).to be_nil }
103
+ it { expect(store.delete('something')).to be_empty }
104
+ it 'deletes the value of a format' do
105
+ store.set('some_value','foo','bla')
106
+ expect(store.delete('some_value','foo')).to eql('bla')
107
+ expect(store.get('some_value','foo')).to be_nil
108
+ end
109
+ it 'deletes only the value of a format' do
110
+ store.set('some_value','plain','value')
111
+ store.set('some_value','foo','bla')
112
+ expect(store.delete('some_value','plain')).to eql('value')
113
+ expect(store.get('some_value','plain')).to be_nil
114
+ expect(store.get('some_value','foo')).to eql('bla')
115
+ end
116
+ it 'deletes all values without a format' do
117
+ store.set('some_value','plain','value')
118
+ store.set('some_value','foo','bla')
119
+ hash = store.delete('some_value')
120
+ expect(hash).to be_a_kind_of(Hash)
121
+ expect(hash['plain']).to eql('value')
122
+ expect(hash['foo']).to eql('bla')
123
+ expect(store.get('some_value','plain')).to be_nil
124
+ expect(store.get('some_value','foo')).to be_nil
125
+ end
126
+ end
127
+ describe 'expiration' do
128
+ it 'will not return an expired key' do
129
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
130
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
131
+ sleep 3
132
+ expect(store.get('some_expiring_value','plain')).to be_nil
133
+ end
134
+ it 'increases expiration when setting anything for that key' do
135
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
136
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
137
+ sleep 1
138
+ store.set('some_expiring_value','bla','bla_to_be_expired',{ 'expires' => 3 })
139
+ sleep 2
140
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
141
+ sleep 2
142
+ expect(store.get('some_expiring_value','plain')).to be_nil
143
+ end
144
+ it 'keeps expiration when setting another value' do
145
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
146
+ store.set('some_expiring_value','foo','to_be_expired_foo')
147
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
148
+ sleep 3
149
+ expect(store.get('some_expiring_value','plain')).to be_nil
150
+ expect(store.get('some_expiring_value','foo')).to be_nil
151
+ end
152
+ it 'setting plain clears everything including expiration' do
153
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
154
+ sleep 1
155
+ store.set('some_expiring_value','plain','to_be_expired2')
156
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
157
+ sleep 3
158
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
159
+ end
160
+ it 'extends expiration when setting another value' do
161
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 4 })
162
+ sleep 2
163
+ store.set('some_expiring_value','foo','to_be_expired_foo')
164
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
165
+ sleep 3
166
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
167
+ sleep 2
168
+ expect(store.get('some_expiring_value','plain')).to be_nil
169
+ end
170
+ it 'extends expiration when deleting a format' do
171
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 4 })
172
+ store.set('some_expiring_value','foo','to_be_expired2')
173
+ sleep 2
174
+ expect(store.delete('some_expiring_value','foo')).to eql('to_be_expired2')
175
+ sleep 3
176
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
177
+ sleep 2
178
+ expect(store.get('some_expiring_value','plain')).to be_nil
179
+ end
180
+ it 'keeps expiration although we\'re fetching a value' do
181
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 3 })
182
+ sleep 2
183
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
184
+ sleep 2
185
+ expect(store.get('some_expiring_value','plain')).to be_nil
186
+ end
187
+ it 'readding a value with an expiration makes it expiring in the future' do
188
+ store.set('some_expiring_value','plain','to_be_expired')
189
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
190
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
191
+ sleep 3
192
+ expect(store.get('some_expiring_value','plain')).to be_nil
193
+ end
194
+ it 'setting an expires of false removes expiration' do
195
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
196
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
197
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => false })
198
+ sleep 3
199
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
200
+ end
201
+ it 'setting an expires of 0 removes expiration' do
202
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
203
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
204
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 0 })
205
+ sleep 3
206
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
207
+ end
208
+ it 'setting an expires of false removes expiration even if it\'s for a different format' do
209
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
210
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
211
+ store.set('some_expiring_value','foo','to_be_expired_foo',{ 'expires' => false })
212
+ sleep 3
213
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
214
+ expect(store.get('some_expiring_value','foo')).to eql('to_be_expired_foo')
215
+ end
216
+ it 'setting an expires of 0 removes expiration even if it\'s for a different format' do
217
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
218
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
219
+ store.set('some_expiring_value','foo','to_be_expired_foo',{ 'expires' => 0 })
220
+ sleep 3
221
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
222
+ expect(store.get('some_expiring_value','foo')).to eql('to_be_expired_foo')
223
+ end
224
+ end
14
225
  end
15
226
 
16
227
  def default_config
@@ -18,26 +229,30 @@ def default_config
18
229
  end
19
230
 
20
231
  def test_config
21
- return @config unless @config.nil?
22
- @config = default_config
23
- @config.delete('adapter_options')
24
- @config['adapter'] = :Memory
25
- @config
232
+ @config ||= default_config.merge({
233
+ 'store' => :memory,
234
+ })
235
+ end
236
+
237
+ def test_config_persistent
238
+ @config ||= default_config.merge({
239
+ 'store_options' => {
240
+ 'adapter' => :YAML,
241
+ 'adapter_options' => {
242
+ :file => trocla_yaml_file
243
+ },
244
+ },
245
+ })
26
246
  end
27
247
 
28
248
  def ssl_test_config
29
- return @ssl_config unless @ssl_config.nil?
30
- @ssl_config = test_config
31
- @ssl_config['encryption'] = :ssl
32
- @ssl_config['ssl_options'] = {
33
- :private_key => data_dir('trocla.key'),
34
- :public_key => data_dir('trocla.pub')
35
- }
36
- @ssl_config['adapter'] = :YAML
37
- @ssl_config['adapter_options'] = {
38
- :file => trocla_yaml_file
39
- }
40
- @ssl_config
249
+ @ssl_config ||= test_config_persistent.merge({
250
+ 'encryption' => :ssl,
251
+ 'encryption_options' => {
252
+ :private_key => data_dir('trocla.key'),
253
+ :public_key => data_dir('trocla.pub'),
254
+ },
255
+ })
41
256
  end
42
257
 
43
258
  def base_dir