strongbox 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Rakefile +7 -16
- data/lib/strongbox.rb +1 -1
- data/strongbox.gemspec +23 -0
- data/test/database.yml +4 -0
- data/test/fixtures/encrypted +0 -0
- data/test/fixtures/keypair.pem +24 -0
- data/test/missing_attributes_test.rb +77 -0
- data/test/strongbox_test.rb +303 -0
- data/test/test_helper.rb +73 -0
- metadata +59 -23
- data/README.html +0 -88
data/.gitignore
ADDED
data/Rakefile
CHANGED
@@ -24,22 +24,13 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
24
24
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
25
|
end
|
26
26
|
|
27
|
-
spec = Gem::Specification.new do |s|
|
28
|
-
s.name = "strongbox"
|
29
|
-
s.version = Strongbox::VERSION
|
30
|
-
s.summary = "Secures ActiveRecord fields with public key encryption."
|
31
|
-
s.authors = ["Spike Ilacqua"]
|
32
|
-
s.email = "spike@stuff-things.net"
|
33
|
-
s.homepage = "http://stuff-things.net/strongbox"
|
34
|
-
s.files = FileList["[A-Z]*", "init.rb", "{lib,rails}/**/*"]
|
35
|
-
s.add_runtime_dependency 'activerecord'
|
36
|
-
s.add_development_dependency 'thoughtbot-shoulda'
|
37
|
-
s.add_development_dependency 'sqlite3'
|
38
|
-
end
|
39
|
-
|
40
27
|
desc "Generate a gemspec file for GitHub"
|
41
28
|
task :gemspec do
|
42
|
-
File.
|
43
|
-
|
44
|
-
|
29
|
+
$spec = eval(File.read('strongbox.gemspec'))
|
30
|
+
$spec.validate
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Build the gem"
|
34
|
+
task :build => :gemspec do
|
35
|
+
Gem::Builder.new($spec).build
|
45
36
|
end
|
data/lib/strongbox.rb
CHANGED
data/strongbox.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
require 'strongbox'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "strongbox"
|
7
|
+
s.version = Strongbox::VERSION
|
8
|
+
s.summary = "Secures ActiveRecord fields with public key encryption."
|
9
|
+
s.authors = ["Spike Ilacqua"]
|
10
|
+
s.email = "spike@stuff-things.net"
|
11
|
+
s.description = <<-EOF
|
12
|
+
Strongbox provides Public Key Encryption for ActiveRecord. By using a
|
13
|
+
public key sensitive information can be encrypted and stored automatically.
|
14
|
+
Once stored a password is required to access the information. dependencies
|
15
|
+
are specified in standard Ruby syntax.
|
16
|
+
EOF
|
17
|
+
s.homepage = "http://stuff-things.net/strongbox"
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
20
|
+
s.add_runtime_dependency 'activerecord'
|
21
|
+
s.add_development_dependency 'thoughtbot-shoulda'
|
22
|
+
s.add_development_dependency 'sqlite3'
|
23
|
+
end
|
data/test/database.yml
ADDED
Binary file
|
@@ -0,0 +1,24 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
Proc-Type: 4,ENCRYPTED
|
3
|
+
DEK-Info: DES-EDE3-CBC,317921A00FB0882F
|
4
|
+
|
5
|
+
f+GWBkcLJLsBUElOEKhqrtYgT1X4nixaZHD5x0VhmW2FrREz4vcqXrxwLTaRQJK/
|
6
|
+
vHFJ/7IVmEHScwEognSfw/wX2HMIHczoQT3ugsa29Nt7t1VLGy9jvN1+1f+g90xe
|
7
|
+
02jC7CYEKUJ3agZPox49i0/UN9OCIgdtKfecdDHYWyziob8yYTsUdDGyAXlPv0Kx
|
8
|
+
0MPSCRDtEh4UJ2PIFyw2HowkYeNss6uIte9rxJGINI11D9vmXR0pH0XyCwHQn+2T
|
9
|
+
ScHWg8BJ1rkBKydbKQ4vnfhGMjG+bZyrJXrJSoazXroseuhHu8QRUONm5Kl/zW1f
|
10
|
+
GP1CjIfTCQQZECYIa2tXTFdL9y2ZOCn8xit57SwEpmJMvZC58PkQX5+/aHPcOXhl
|
11
|
+
YrF+6FEfNpdBz9PUmv4Af2kTa88xZqm1Q3GtTOk7wsJpfeTMhU71KjA1pL9xNPrT
|
12
|
+
DnKhtfLGvcgo8Z9BGOiLFe9uQvhhprX7isc1XdysbMigsVIWLvZp9RxRp/zAn7fy
|
13
|
+
y56C6mc3tUwcq89RcxAn+bC75gwZO/hyVrnkhManOMfHTEiZXVybU9Ril3SZ+ry6
|
14
|
+
8AxMid0ZWbbtCHdDc5rHfXsGeFhJZxBbg/WtMxBPGHNByqs8sWUM9Z8YoK8WMYxV
|
15
|
+
GvC9RB4m0jgA4S3MEOMmKOXDuJxa7IgTgApVmLPl+sDOHGK3xAItYJJawJqOZQ1f
|
16
|
+
r+x/8g19CuehuflCxDo+D4/RJMqkOEq+0FGUqI8lHv6vR6+YpkGdrQQXUohBy67f
|
17
|
+
3Qym1ztZ8ygsttgJwnhwAfMh8FdIrVJc7NZ8pDiBZbg=
|
18
|
+
-----END RSA PRIVATE KEY-----
|
19
|
+
-----BEGIN PUBLIC KEY-----
|
20
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9F1ipsLL+V68bGSJFqFLQKgXq
|
21
|
+
Glyyplx0s9KxgLbmbDICXpV7DceKaIBUkPZDx2DrlvjZmG+rG5ehdWNI7q/hupao
|
22
|
+
NF0WzEiOp+30gISeyl81Z/NAmhcwcOnZpbS9nl4JLaWrN7iGC1geNBNDo+lVbsm1
|
23
|
+
O2+Tlt8rjHsNjzgIzQIDAQAB
|
24
|
+
-----END PUBLIC KEY-----
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class MissingAttribuesTest < Test::Unit::TestCase
|
4
|
+
context 'A Class with a secured field without a matching database column' do
|
5
|
+
setup do
|
6
|
+
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
7
|
+
table.string :in_the_clear
|
8
|
+
end
|
9
|
+
rebuild_class {}
|
10
|
+
end
|
11
|
+
|
12
|
+
should 'raise' do
|
13
|
+
assert_raise(Strongbox::StrongboxError) do
|
14
|
+
Dummy.class_eval do
|
15
|
+
encrypt_with_public_key :secret, :key_pair =>
|
16
|
+
File.join(FIXTURES_DIR,'keypair.pem')
|
17
|
+
end
|
18
|
+
@dummy = Dummy.new
|
19
|
+
@dummy.secret = 'Shhhh'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
teardown do
|
24
|
+
rebuild_model
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'A Class with a secured field missing symmetric database columns' do
|
29
|
+
setup do
|
30
|
+
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
31
|
+
table.string :in_the_clear
|
32
|
+
table.string :secret
|
33
|
+
end
|
34
|
+
rebuild_class {}
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'raise' do
|
38
|
+
assert_raise(Strongbox::StrongboxError) do
|
39
|
+
Dummy.class_eval do
|
40
|
+
encrypt_with_public_key :secret, :key_pair =>
|
41
|
+
File.join(FIXTURES_DIR,'keypair.pem')
|
42
|
+
end
|
43
|
+
@dummy = Dummy.new
|
44
|
+
@dummy.secret = 'Shhhh'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
teardown do
|
49
|
+
rebuild_model
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'A Class with a secured field without a matching database column told not to check columns' do
|
54
|
+
setup do
|
55
|
+
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
56
|
+
table.string :in_the_clear
|
57
|
+
end
|
58
|
+
rebuild_class {}
|
59
|
+
end
|
60
|
+
|
61
|
+
should 'not raise' do
|
62
|
+
assert_nothing_raised do
|
63
|
+
Dummy.class_eval do
|
64
|
+
encrypt_with_public_key(:secret,
|
65
|
+
:key_pair => File.join(FIXTURES_DIR,'keypair.pem'),
|
66
|
+
:ensure_required_columns => false)
|
67
|
+
end
|
68
|
+
@dummy = Dummy.new
|
69
|
+
@dummy.secret = 'Shhhh'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
teardown do
|
74
|
+
rebuild_model
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'test/test_helper'
|
3
|
+
|
4
|
+
class StrongboxTest < Test::Unit::TestCase
|
5
|
+
context 'A Class with a secured field' do
|
6
|
+
setup do
|
7
|
+
@password = 'boost facile'
|
8
|
+
rebuild_model :key_pair => File.join(FIXTURES_DIR,'keypair.pem')
|
9
|
+
end
|
10
|
+
|
11
|
+
should 'not error when trying to also create a secure field' do
|
12
|
+
assert_nothing_raised do
|
13
|
+
Dummy.class_eval do
|
14
|
+
encrypt_with_public_key :secret
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'that is valid' do
|
20
|
+
setup do
|
21
|
+
@dummy = Dummy.new
|
22
|
+
@dummy.secret = 'Shhhh'
|
23
|
+
@dummy.in_the_clear = 'Hey you guys!'
|
24
|
+
end
|
25
|
+
|
26
|
+
should 'not change unencrypted fields' do
|
27
|
+
assert_equal 'Hey you guys!', @dummy.in_the_clear
|
28
|
+
end
|
29
|
+
|
30
|
+
should 'return "*encrypted*" when locked' do
|
31
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
32
|
+
end
|
33
|
+
|
34
|
+
should 'return secret when unlocked' do
|
35
|
+
assert_equal 'Shhhh', @dummy.secret.decrypt(@password)
|
36
|
+
end
|
37
|
+
|
38
|
+
should 'generate and store symmetric encryption key and IV' do
|
39
|
+
assert_not_nil @dummy.attributes['secret_key']
|
40
|
+
assert_not_nil @dummy.attributes['secret_iv']
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'raise on bad password' do
|
44
|
+
assert_raises(OpenSSL::PKey::RSAError) do
|
45
|
+
@dummy.secret.decrypt('letmein')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'updating unencrypted fields' do
|
50
|
+
setup do
|
51
|
+
@dummy.in_the_clear = 'I see you...'
|
52
|
+
@dummy.save
|
53
|
+
end
|
54
|
+
|
55
|
+
should 'not effect the secret' do
|
56
|
+
assert_equal 'Shhhh', @dummy.secret.decrypt(@password)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'updating the secret' do
|
61
|
+
setup do
|
62
|
+
@dummy.secret = @new_secret = 'Don\'t tell'
|
63
|
+
@dummy.save
|
64
|
+
end
|
65
|
+
|
66
|
+
should 'update the secret' do
|
67
|
+
assert_equal @new_secret, @dummy.secret.decrypt(@password)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'with symmetric encryption disabled' do
|
72
|
+
setup do
|
73
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'),
|
74
|
+
:symmetric => :never)
|
75
|
+
@dummy = Dummy.new
|
76
|
+
@dummy.secret = 'Shhhh'
|
77
|
+
end
|
78
|
+
|
79
|
+
should 'return "*encrypted*" when locked' do
|
80
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
81
|
+
end
|
82
|
+
|
83
|
+
should 'return secret when unlocked' do
|
84
|
+
assert_equal 'Shhhh', @dummy.secret.decrypt(@password)
|
85
|
+
end
|
86
|
+
|
87
|
+
should 'allow decryption of other strings encrypted with the same key' do
|
88
|
+
encrypted_text = File.read(File.join(FIXTURES_DIR,'encrypted'))
|
89
|
+
assert_equal 'Setec Astronomy', @dummy.secret.decrypt(@password, encrypted_text)
|
90
|
+
end
|
91
|
+
|
92
|
+
should 'not generate and store symmetric encryption key and IV' do
|
93
|
+
assert_nil @dummy.attributes['secret_key']
|
94
|
+
assert_nil @dummy.attributes['secret_iv']
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'with Base64 encoding enabled' do
|
100
|
+
setup do
|
101
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'),
|
102
|
+
:base64 => true)
|
103
|
+
@dummy = Dummy.new
|
104
|
+
@dummy.secret = 'Shhhh'
|
105
|
+
end
|
106
|
+
|
107
|
+
should 'Base64 encode the ciphertext' do
|
108
|
+
# Base64 encoded text is limited to the charaters A–Z, a–z, and 0–9,
|
109
|
+
# and is padded with 0 to 2 equal-signs
|
110
|
+
assert_match /^[0-9A-Za-z+\/]+={0,2}$/, @dummy.attributes['secret']
|
111
|
+
assert_match /^[0-9A-Za-z+\/]+={0,2}$/, @dummy.attributes['secret_key']
|
112
|
+
assert_match /^[0-9A-Za-z+\/]+={0,2}$/, @dummy.attributes['secret_iv']
|
113
|
+
end
|
114
|
+
|
115
|
+
should 'encrypt the data' do
|
116
|
+
assert_not_equal @dummy.attributes['secret'], 'Shhhh'
|
117
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
118
|
+
assert_equal 'Shhhh', @dummy.secret.decrypt(@password)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'using blowfish cipher instead of AES' do
|
124
|
+
setup do
|
125
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'),
|
126
|
+
:symmetric_cipher => 'bf-cbc')
|
127
|
+
@dummy = Dummy.new
|
128
|
+
@dummy.secret = 'Shhhh'
|
129
|
+
end
|
130
|
+
|
131
|
+
should 'encrypt the data' do
|
132
|
+
assert_not_equal @dummy.attributes['secret'], 'Shhhh'
|
133
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
134
|
+
assert_equal 'Shhhh', @dummy.secret.decrypt(@password)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'when a public key is not provided' do
|
140
|
+
setup do
|
141
|
+
rebuild_class
|
142
|
+
@dummy = Dummy.new
|
143
|
+
end
|
144
|
+
|
145
|
+
should 'raise on encrypt' do
|
146
|
+
assert_raises(Strongbox::StrongboxError) do
|
147
|
+
@dummy.secret = 'Shhhh'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when a private key is not provided' do
|
153
|
+
setup do
|
154
|
+
@password = 'boost facile'
|
155
|
+
rebuild_class(:public_key => File.join(FIXTURES_DIR,'keypair.pem'))
|
156
|
+
@dummy = Dummy.new(:secret => 'Shhhh')
|
157
|
+
end
|
158
|
+
|
159
|
+
should 'raise on decrypt with a password' do
|
160
|
+
assert_raises(Strongbox::StrongboxError) do
|
161
|
+
@dummy.secret.decrypt(@password)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
should 'return "*encrypted*" when still locked' do
|
166
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "when an unencrypted public key is used" do
|
171
|
+
setup do
|
172
|
+
rebuild_class(:public_key => generate_key_pair.public_key)
|
173
|
+
@dummy = Dummy.new(:secret => 'Shhhh')
|
174
|
+
end
|
175
|
+
|
176
|
+
should "encrypt the data" do
|
177
|
+
assert_not_equal @dummy.attributes['secret'], 'Shhhh'
|
178
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context "when an unencrypted key pair is used" do
|
183
|
+
setup do
|
184
|
+
rebuild_class(:key_pair => generate_key_pair)
|
185
|
+
@dummy = Dummy.new(:secret => 'Shhhh')
|
186
|
+
end
|
187
|
+
|
188
|
+
should "encrypt the data" do
|
189
|
+
assert_not_equal @dummy.attributes['secret'], 'Shhhh'
|
190
|
+
assert_equal "Shhhh", @dummy.secret.decrypt('')
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context 'with validations' do
|
195
|
+
context 'using validates_presence_of' do
|
196
|
+
setup do
|
197
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'))
|
198
|
+
Dummy.send(:validates_presence_of, :secret)
|
199
|
+
@valid = Dummy.new(:secret => 'Shhhh')
|
200
|
+
@invalid = Dummy.new(:secret => nil)
|
201
|
+
end
|
202
|
+
|
203
|
+
should 'not have an error on the secret when valid' do
|
204
|
+
assert @valid.valid?
|
205
|
+
assert_does_not_have_errors_on(@valid,:secret)
|
206
|
+
end
|
207
|
+
|
208
|
+
should 'have an error on the secret when invalid' do
|
209
|
+
assert !@invalid.valid?
|
210
|
+
assert_has_errors_on(@invalid,:secret)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context 'using validates_length_of' do
|
215
|
+
setup do
|
216
|
+
rebuild_class(:key_pair => File.join(FIXTURES_DIR,'keypair.pem'))
|
217
|
+
Dummy.send(:validates_length_of,
|
218
|
+
:secret,
|
219
|
+
:in => 5..10,
|
220
|
+
:allow_nil => true,
|
221
|
+
:allow_blank => true
|
222
|
+
)
|
223
|
+
@valid = Dummy.new(:secret => 'Shhhh')
|
224
|
+
@valid_nil = Dummy.new(:secret => nil)
|
225
|
+
@valid_blank = Dummy.new(:secret => '')
|
226
|
+
@invalid = Dummy.new(:secret => '1')
|
227
|
+
end
|
228
|
+
|
229
|
+
should 'not have an error on the secret when in range' do
|
230
|
+
assert @valid.valid?
|
231
|
+
assert_does_not_have_errors_on(@valid,:secret)
|
232
|
+
end
|
233
|
+
|
234
|
+
should 'not have an error on the secret when nil' do
|
235
|
+
assert @valid_nil.valid?
|
236
|
+
assert_does_not_have_errors_on(@valid_nil,:secret)
|
237
|
+
end
|
238
|
+
|
239
|
+
should 'not have an error on the secret when blank' do
|
240
|
+
assert @valid_blank.valid?
|
241
|
+
assert_does_not_have_errors_on(@valid_blank,:secret)
|
242
|
+
end
|
243
|
+
|
244
|
+
should 'have an error on the secret when invalid' do
|
245
|
+
assert !@invalid.valid?
|
246
|
+
assert_has_errors_on(@invalid,:secret)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context 'A Class with two secured fields' do
|
252
|
+
setup do
|
253
|
+
@password = 'boost facile'
|
254
|
+
key_pair = File.join(FIXTURES_DIR,'keypair.pem')
|
255
|
+
Dummy.class_eval do
|
256
|
+
encrypt_with_public_key :secret, :key_pair => key_pair
|
257
|
+
encrypt_with_public_key :segreto, :key_pair => key_pair,
|
258
|
+
:symmetric => :never
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
context 'that is valid' do
|
263
|
+
setup do
|
264
|
+
@dummy = Dummy.new
|
265
|
+
@dummy.secret = 'I have a secret...'
|
266
|
+
@dummy.segreto = 'Ho un segreto...'
|
267
|
+
end
|
268
|
+
|
269
|
+
should 'return "*encrypted*" when the record is locked' do
|
270
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
271
|
+
assert_equal '*encrypted*', @dummy.segreto.decrypt
|
272
|
+
end
|
273
|
+
|
274
|
+
should 'return the secrets when unlocked' do
|
275
|
+
assert_equal 'I have a secret...', @dummy.secret.decrypt(@password)
|
276
|
+
assert_equal 'Ho un segreto...', @dummy.segreto.decrypt(@password)
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
context 'Using strings for keys' do
|
283
|
+
setup do
|
284
|
+
@password = 'boost facile'
|
285
|
+
key_pair = File.read(File.join(FIXTURES_DIR,'keypair.pem'))
|
286
|
+
public_key = OpenSSL::PKey::RSA.new(key_pair,"")
|
287
|
+
private_key = OpenSSL::PKey::RSA.new(key_pair,@password)
|
288
|
+
Dummy.class_eval do
|
289
|
+
encrypt_with_public_key :secret, :public_key => public_key, :private_key => private_key
|
290
|
+
end
|
291
|
+
@dummy = Dummy.new
|
292
|
+
@dummy.secret = 'Shhhh'
|
293
|
+
end
|
294
|
+
|
295
|
+
should 'return "*encrypted*" when locked' do
|
296
|
+
assert_equal '*encrypted*', @dummy.secret.decrypt
|
297
|
+
end
|
298
|
+
|
299
|
+
should 'return secret when unlocked' do
|
300
|
+
assert_equal 'Shhhh', @dummy.secret.decrypt(@password)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
ROOT = File.join(File.dirname(__FILE__), '..')
|
2
|
+
RAILS_ROOT = ROOT
|
3
|
+
$LOAD_PATH << File.join(ROOT, 'lib')
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'sqlite3'
|
8
|
+
require 'active_record'
|
9
|
+
require 'logger'
|
10
|
+
gem 'thoughtbot-shoulda', ">= 2.9.0"
|
11
|
+
require 'shoulda'
|
12
|
+
begin require 'redgreen'; rescue LoadError; end
|
13
|
+
|
14
|
+
require 'strongbox'
|
15
|
+
|
16
|
+
ENV['RAILS_ENV'] ||= 'test'
|
17
|
+
|
18
|
+
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
|
19
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
20
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
21
|
+
ActiveRecord::Base.establish_connection(config['test'])
|
22
|
+
|
23
|
+
|
24
|
+
# rebuild_model and rebuild_class are borrowed directly from the Paperclip gem
|
25
|
+
#
|
26
|
+
# http://thoughtbot.com/projects/paperclip
|
27
|
+
|
28
|
+
# rebuild_model (re)creates a database table for our Dummy model.
|
29
|
+
# Call this to initial create a model, or to reset the database.
|
30
|
+
|
31
|
+
def rebuild_model options = {}
|
32
|
+
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
33
|
+
table.string :in_the_clear
|
34
|
+
table.binary :secret
|
35
|
+
table.binary :secret_key
|
36
|
+
table.binary :secret_iv
|
37
|
+
table.binary :segreto
|
38
|
+
end
|
39
|
+
rebuild_class options
|
40
|
+
end
|
41
|
+
|
42
|
+
# rebuild_class creates or replaces the Dummy ActiveRecord Model.
|
43
|
+
# Call this when changing the options to encrypt_with_public_key
|
44
|
+
|
45
|
+
def rebuild_class options = {}
|
46
|
+
ActiveRecord::Base.send(:include, Strongbox)
|
47
|
+
Object.send(:remove_const, "Dummy") rescue nil
|
48
|
+
Object.const_set("Dummy", Class.new(ActiveRecord::Base))
|
49
|
+
Dummy.class_eval do
|
50
|
+
include Strongbox
|
51
|
+
encrypt_with_public_key :secret, options
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def assert_has_errors_on(model,attribute)
|
56
|
+
# Rails 2.X && Rails 3.X
|
57
|
+
!model.errors[attribute].empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
def assert_does_not_have_errors_on(model,attribute)
|
61
|
+
# Rails 2.X Rails 3.X
|
62
|
+
model.errors[attribute].nil? || model.errors[attribute].empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate_key_pair(password = nil,size = 2048)
|
66
|
+
rsa_key = OpenSSL::PKey::RSA.new(size)
|
67
|
+
# If no password is provided, don't encrypt the key
|
68
|
+
return rsa_key if password.blank?
|
69
|
+
cipher = OpenSSL::Cipher::Cipher.new('des3')
|
70
|
+
key_pair = rsa_key.to_pem(cipher,password)
|
71
|
+
key_pair << rsa_key.public_key.to_pem
|
72
|
+
return key_pair
|
73
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strongbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 9
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 4
|
9
|
+
- 3
|
10
|
+
version: 0.4.3
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- Spike Ilacqua
|
@@ -9,40 +15,52 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date: 2011-
|
18
|
+
date: 2011-02-21 00:00:00 -07:00
|
13
19
|
default_executable:
|
14
20
|
dependencies:
|
15
21
|
- !ruby/object:Gem::Dependency
|
16
22
|
name: activerecord
|
17
|
-
|
18
|
-
|
19
|
-
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
20
26
|
requirements:
|
21
27
|
- - ">="
|
22
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
23
32
|
version: "0"
|
24
|
-
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
25
35
|
- !ruby/object:Gem::Dependency
|
26
36
|
name: thoughtbot-shoulda
|
27
|
-
|
28
|
-
|
29
|
-
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
30
40
|
requirements:
|
31
41
|
- - ">="
|
32
42
|
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
33
46
|
version: "0"
|
34
|
-
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id002
|
35
49
|
- !ruby/object:Gem::Dependency
|
36
50
|
name: sqlite3
|
37
|
-
|
38
|
-
|
39
|
-
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
40
54
|
requirements:
|
41
55
|
- - ">="
|
42
56
|
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
43
60
|
version: "0"
|
44
|
-
|
45
|
-
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
description: " Strongbox provides Public Key Encryption for ActiveRecord. By using a\n public key sensitive information can be encrypted and stored automatically.\n Once stored a password is required to access the information. dependencies\n are specified in standard Ruby syntax.\n"
|
46
64
|
email: spike@stuff-things.net
|
47
65
|
executables: []
|
48
66
|
|
@@ -51,14 +69,21 @@ extensions: []
|
|
51
69
|
extra_rdoc_files: []
|
52
70
|
|
53
71
|
files:
|
72
|
+
- .gitignore
|
54
73
|
- LICENSE
|
55
|
-
- Rakefile
|
56
|
-
- README.html
|
57
74
|
- README.textile
|
75
|
+
- Rakefile
|
58
76
|
- init.rb
|
59
|
-
- lib/strongbox/lock.rb
|
60
77
|
- lib/strongbox.rb
|
78
|
+
- lib/strongbox/lock.rb
|
61
79
|
- rails/init.rb
|
80
|
+
- strongbox.gemspec
|
81
|
+
- test/database.yml
|
82
|
+
- test/fixtures/encrypted
|
83
|
+
- test/fixtures/keypair.pem
|
84
|
+
- test/missing_attributes_test.rb
|
85
|
+
- test/strongbox_test.rb
|
86
|
+
- test/test_helper.rb
|
62
87
|
has_rdoc: true
|
63
88
|
homepage: http://stuff-things.net/strongbox
|
64
89
|
licenses: []
|
@@ -69,23 +94,34 @@ rdoc_options: []
|
|
69
94
|
require_paths:
|
70
95
|
- lib
|
71
96
|
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
72
98
|
requirements:
|
73
99
|
- - ">="
|
74
100
|
- !ruby/object:Gem::Version
|
101
|
+
hash: 3
|
102
|
+
segments:
|
103
|
+
- 0
|
75
104
|
version: "0"
|
76
|
-
version:
|
77
105
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
78
107
|
requirements:
|
79
108
|
- - ">="
|
80
109
|
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
81
113
|
version: "0"
|
82
|
-
version:
|
83
114
|
requirements: []
|
84
115
|
|
85
116
|
rubyforge_project:
|
86
|
-
rubygems_version: 1.
|
117
|
+
rubygems_version: 1.5.2
|
87
118
|
signing_key:
|
88
119
|
specification_version: 3
|
89
120
|
summary: Secures ActiveRecord fields with public key encryption.
|
90
|
-
test_files:
|
91
|
-
|
121
|
+
test_files:
|
122
|
+
- test/database.yml
|
123
|
+
- test/fixtures/encrypted
|
124
|
+
- test/fixtures/keypair.pem
|
125
|
+
- test/missing_attributes_test.rb
|
126
|
+
- test/strongbox_test.rb
|
127
|
+
- test/test_helper.rb
|
data/README.html
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
<h1>Strongbox</h1>
|
2
|
-
<p>Strongbox provides Public Key Encryption for ActiveRecord. By using a public key sensitive information can be encrypted and stored automatically. Once stored a password is required to access the information.</p>
|
3
|
-
<p>Because the largest amount of data that can practically be encrypted with a public key is 245 byte, by default Strongbox uses a two layer approach. First it encrypts the attribute using symmetric encryption with a randomly generated key and initialization vector (IV) (which can just be through to as a second key), then it encrypts those with the public key.</p>
|
4
|
-
<p>Strongbox stores the encrypted attribute in a database column by the same name, i.e. if you tell Strongbox to encrypt “secret” then it will be store in “secret” in the database, just as the unencrypted attribute would be. If symmetric encryption is used (the default) two additional columns “secret_key” and “secret_iv” are needed as well.</p>
|
5
|
-
<p>The attribute is automatically encrypted simply by setting it:</p>
|
6
|
-
user.secret = “Shhhhhhh…”
|
7
|
-
<p>and decrypted by calling the “decrypt” method with the private key password.</p>
|
8
|
-
plain_text = user.secret.decrypt ‘letmein’
|
9
|
-
<h2>Quick Start</h2>
|
10
|
-
<p>In your model:</p>
|
11
|
-
<pre><code>class User < ActiveRecord::Base
|
12
|
-
encrypt_with_public_key :secret,
|
13
|
-
:key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
|
14
|
-
end</code></pre>
|
15
|
-
<p>In your migrations:</p>
|
16
|
-
<pre><code>class AddSecretColumnsToUser < ActiveRecord::Migration
|
17
|
-
def self.up
|
18
|
-
add_column :users, :secret, :binary
|
19
|
-
add_column :users, :secret_key, :binary
|
20
|
-
add_column :users, :secret_iv, :binary
|
21
|
-
end
|
22
|
-
def self.down
|
23
|
-
remove_column :users, :secret
|
24
|
-
remove_column :users, :secret_key
|
25
|
-
remove_column :users, :secret_iv
|
26
|
-
end
|
27
|
-
end</code></pre>
|
28
|
-
<p>Generate a key pair:</p>
|
29
|
-
<p>(Choose a strong password.)</p>
|
30
|
-
<pre><code>openssl genrsa -des3 -out config/private.pem 2048
|
31
|
-
openssl rsa -in config/private.pem -out config/public.pem -outform PEM -pubout
|
32
|
-
cat config/private.pem config/public.pem >> config/keypair.pem</code></pre>
|
33
|
-
<p>In your views and forms you don’t need to do anything special to encrypt data. To decrypt call:</p>
|
34
|
-
<pre><code>user.secret.decrypt 'password'</code></pre>
|
35
|
-
<h2>Gem installation (Rails 2.1+)</h2>
|
36
|
-
<p>In config/environment.rb:</p>
|
37
|
-
<pre><code>config.gem "strongbox"</code></pre>
|
38
|
-
<h2>Usage</h2>
|
39
|
-
<p><em>encrypt_with_public_key</em> sets up the attribute it’s called on for automatic encryption. It’s simplest form is:</p>
|
40
|
-
<pre><code>class User < ActiveRecord::Base
|
41
|
-
encrypt_with_public_key :secret,
|
42
|
-
:key_pair => File.join(RAILS_ROOT,'config','keypair.pem')
|
43
|
-
end</code></pre>
|
44
|
-
<p>Which will encrypt the attribute “secret”. The attribute will be encrypted using symmetric encryption with an automatically generated key and IV encrypted using the public key. This requires three columns in the database “secret”, “secret_key”, and “secret_iv” (see below).</p>
|
45
|
-
<p>Options to encrypt_with_public_key are:</p>
|
46
|
-
<p>:public_key – Path to the public key file. Overrides :keypair.</p>
|
47
|
-
<p>:private_key – Path to the private key file. Overrides :keypair.</p>
|
48
|
-
<p>:keypair – Path to a file containing both the public and private keys.</p>
|
49
|
-
<p>:symmetric :always/:never – Encrypt the date using symmetric encryption. The public key is used to encrypt an automatically generated key and IV. This allows for large amounts of data to be encrypted. The size of data that can be encrypted directly with the public is limit to key size (in bytes) – 11. So a 2048 key can encrypt <strong>245 bytes</strong>. Defaults to <strong>:always</strong>.</p>
|
50
|
-
<p>:symmetric_cipher – Cipher to use for symmetric encryption. Defaults to <strong>‘aes-256-cbc’</strong>. Other ciphers support by OpenSSL may be used.</p>
|
51
|
-
<p>:base64 true/false – Use Base64 encoding to convert encrypted data to text. Use when binary save data storage is not available. Defaults to <strong>false</strong>.</p>
|
52
|
-
<p>:padding – Method used to pad data encrypted with the public key. Defaults to <strong>RSA_PKCS1_PADDING</strong>. The default should be fine unless you are dealing with legacy data.</p>
|
53
|
-
<p>:ensure_required_columns – Make sure the required database columns exist. Defaults to <strong>true</strong>, set to false if you want to encrypt/decrypt data stored outside of the database.</p>
|
54
|
-
<p>For example, encrypting a small attribute, providing only the public key for extra security, and Base64 encoding the encrypted data:</p>
|
55
|
-
<pre><code>class User < ActiveRecord::Base
|
56
|
-
validates_length_of :pin_code, :is => 4
|
57
|
-
encrypt_with_public_key :pin_code,
|
58
|
-
:symmetric => :never,
|
59
|
-
:base64 => true,
|
60
|
-
:public_key => File.join(RAILS_ROOT,'config','public.pem')
|
61
|
-
end</code></pre>
|
62
|
-
<h2>Key Generation</h2>
|
63
|
-
<p>Generate a key pair:</p>
|
64
|
-
<pre><code>openssl genrsa -des3 -out config/private.pem 2048
|
65
|
-
Generating RSA private key, 2048 bit long modulus
|
66
|
-
......+++
|
67
|
-
.+++
|
68
|
-
e is 65537 (0x10001)
|
69
|
-
Enter pass phrase for config/private.pem:
|
70
|
-
Verifying - Enter pass phrase for config/private.pem:</code></pre>
|
71
|
-
<p>and extract the the public key:</p>
|
72
|
-
<pre><code>openssl rsa -in config/private.pem -out config/public.pem -outform PEM -pubout
|
73
|
-
Enter pass phrase for config/private.pem:
|
74
|
-
writing RSA key</code></pre>
|
75
|
-
<p>If you are going to leave the private key installed it’s easiest to create a single key pair file:</p>
|
76
|
-
<pre><code>cat config/private.pem config/public.pem >> config/keypair.pem</code></pre>
|
77
|
-
<p>Or, for added security, store the private key file else where, leaving only the public key.</p>
|
78
|
-
<h2>Table Creation</h2>
|
79
|
-
<p>In it’s default configuration Strongbox requires three columns, one the encrypted data, one for the encrypted symmetric key, and one for the encrypted symmetric IV. If symmetric encryption is disabled then only the columns for the data being encrypted is needed.</p>
|
80
|
-
<p>If your underlying database allows, use the <strong>binary</strong> column type. If you must store your data in text format be sure to enable Base64 encoding and to use the <strong>text</strong> column type. If you use a <em>string</em> column and encrypt anything greater than 186 bytes (245 bytes if you don’t enable Base64 encoding) <strong>your data will be lost</strong>.</p>
|
81
|
-
<h2>Security Caveats</h2>
|
82
|
-
<p>If you don’t encrypt your data, then an attacker only needs to steal that data to get your secrets.</p>
|
83
|
-
<p>If encrypt your data using symmetric encrypts and a stored key, then the attacker needs the data and the key stored on the server.</p>
|
84
|
-
<p>If you use public key encryption, the attacker needs the data, the private key, and the password. This means the attacker has to sniff the password somehow, so that’s what you need to protect against.</p>
|
85
|
-
<h2>Authors</h2>
|
86
|
-
<p>Spike Ilacqua</p>
|
87
|
-
<h2>Thanks</h2>
|
88
|
-
<p>Strongbox’s implementation drew inspiration from Thoughtbot’s Paperclip gem http://www.thoughtbot.com/projects/paperclip</p>
|