symmetric-encryption 3.4.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +102 -55
  3. data/Rakefile +13 -8
  4. data/lib/rails/generators/symmetric_encryption/config/config_generator.rb +1 -1
  5. data/lib/rails/generators/symmetric_encryption/heroku_config/templates/symmetric-encryption.yml +2 -2
  6. data/lib/rails/generators/symmetric_encryption/new_keys/new_keys_generator.rb +2 -2
  7. data/lib/symmetric_encryption.rb +7 -6
  8. data/lib/symmetric_encryption/cipher.rb +4 -4
  9. data/lib/symmetric_encryption/extensions/active_record/base.rb +6 -46
  10. data/lib/symmetric_encryption/extensions/mongo_mapper/plugins/encrypted_key.rb +129 -0
  11. data/lib/symmetric_encryption/{mongoid.rb → extensions/mongoid/encrypted.rb} +12 -46
  12. data/lib/symmetric_encryption/generator.rb +54 -0
  13. data/lib/symmetric_encryption/railtie.rb +3 -3
  14. data/lib/symmetric_encryption/railties/symmetric_encryption.rake +1 -1
  15. data/lib/symmetric_encryption/railties/symmetric_encryption_validator.rb +1 -1
  16. data/lib/symmetric_encryption/reader.rb +3 -3
  17. data/lib/symmetric_encryption/symmetric_encryption.rb +25 -15
  18. data/lib/symmetric_encryption/version.rb +1 -1
  19. data/lib/symmetric_encryption/writer.rb +4 -4
  20. data/test/active_record_test.rb +474 -0
  21. data/test/cipher_test.rb +15 -15
  22. data/test/config/mongo_mapper.yml +7 -0
  23. data/test/{field_encrypted_test.rb → mongo_mapper_test.rb} +68 -67
  24. data/test/mongoid_test.rb +535 -0
  25. data/test/reader_test.rb +10 -10
  26. data/test/symmetric_encryption_test.rb +27 -27
  27. data/test/test_db.sqlite3 +0 -0
  28. data/test/test_helper.rb +0 -1
  29. data/test/writer_test.rb +2 -2
  30. metadata +14 -8
  31. data/test/attr_encrypted_test.rb +0 -622
@@ -0,0 +1,474 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ ActiveRecord::Base.logger = SemanticLogger[ActiveRecord]
4
+ ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read('test/config/database.yml')).result)
5
+ ActiveRecord::Base.establish_connection('test')
6
+
7
+ ActiveRecord::Schema.define version: 0 do
8
+ create_table :users, force: true do |t|
9
+ t.string :encrypted_bank_account_number
10
+ t.string :encrypted_social_security_number
11
+ t.string :encrypted_string
12
+ t.text :encrypted_long_string
13
+ t.text :encrypted_data_yaml
14
+ t.text :encrypted_data_json
15
+ t.string :name
16
+
17
+ t.string :encrypted_integer_value
18
+ t.string :encrypted_float_value
19
+ t.string :encrypted_decimal_value
20
+ t.string :encrypted_datetime_value
21
+ t.string :encrypted_time_value
22
+ t.string :encrypted_date_value
23
+ t.string :encrypted_true_value
24
+ t.string :encrypted_false_value
25
+
26
+ t.string :encrypted_text
27
+ t.string :encrypted_number
28
+ end
29
+ end
30
+
31
+ class User < ActiveRecord::Base
32
+ attr_encrypted :bank_account_number
33
+ attr_encrypted :social_security_number
34
+ attr_encrypted :string, random_iv: true
35
+ attr_encrypted :long_string, random_iv: true, compress: true
36
+ attr_encrypted :data_yaml, random_iv: true, compress: true, type: :yaml
37
+ attr_encrypted :data_json, random_iv: true, compress: true, type: :json
38
+
39
+ attr_encrypted :integer_value, type: :integer
40
+ attr_encrypted :float_value, type: :float
41
+ attr_encrypted :decimal_value, type: :decimal
42
+ attr_encrypted :datetime_value, type: :datetime
43
+ attr_encrypted :time_value, type: :time
44
+ attr_encrypted :date_value, type: :date
45
+ attr_encrypted :true_value, type: :boolean
46
+ attr_encrypted :false_value, type: :boolean
47
+
48
+ validates :encrypted_bank_account_number, symmetric_encryption: true
49
+ validates :encrypted_social_security_number, symmetric_encryption: true
50
+
51
+ attr_encrypted :text, type: :string
52
+ attr_encrypted :number, type: :integer
53
+
54
+ validates :text, format: { with: /\A[a-zA-Z ]+\z/, message: "only allows letters" }, presence: true
55
+ validates :number, presence: true
56
+ end
57
+
58
+ # Initialize the database connection
59
+ config_file = File.join(File.dirname(__FILE__), 'config', 'database.yml')
60
+ raise "database config not found. Create a config file at: test/config/database.yml" unless File.exists? config_file
61
+
62
+ cfg = YAML.load(ERB.new(File.new(config_file).read).result)['test']
63
+ raise("Environment 'test' not defined in test/config/database.yml") unless cfg
64
+
65
+ User.establish_connection(cfg)
66
+
67
+ #
68
+ # Unit Test for attr_encrypted extensions in ActiveRecord
69
+ #
70
+ class ActiveRecordTest < Test::Unit::TestCase
71
+ context 'ActiveRecord' do
72
+ INTEGER_VALUE = 12
73
+ FLOAT_VALUE = 88.12345
74
+ DECIMAL_VALUE = BigDecimal.new("22.51")
75
+ DATETIME_VALUE = DateTime.new(2001, 11, 26, 20, 55, 54, "-5")
76
+ TIME_VALUE = Time.new(2013, 01, 01, 22, 30, 00, "-04:00")
77
+ DATE_VALUE = Date.new(1927, 04, 02)
78
+
79
+ setup do
80
+ @bank_account_number = "1234567890"
81
+ @bank_account_number_encrypted = "QEVuQwIAL94ArJeFlJrZp6SYsvoOGA=="
82
+
83
+ @social_security_number = "987654321"
84
+ @social_security_number_encrypted = "QEVuQwIAS+8X1NRrqdfEIQyFHVPuVA=="
85
+
86
+ @string = "A string containing some data to be encrypted with a random initialization vector"
87
+ @long_string = "A string containing some data to be encrypted with a random initialization vector and compressed since it takes up so much space in plain text form"
88
+
89
+ @name = 'Joe Bloggs'
90
+
91
+ @h = { a: 'A', b: 'B' }
92
+
93
+ @user = User.new(
94
+ # Encrypted Attribute
95
+ bank_account_number: @bank_account_number,
96
+ # Encrypted Attribute
97
+ social_security_number: @social_security_number,
98
+ name: @name,
99
+ # data type specific fields
100
+ integer_value: INTEGER_VALUE,
101
+ float_value: FLOAT_VALUE,
102
+ decimal_value: DECIMAL_VALUE,
103
+ datetime_value: DATETIME_VALUE,
104
+ time_value: TIME_VALUE,
105
+ date_value: DATE_VALUE,
106
+ true_value: true,
107
+ false_value: false,
108
+ data_yaml: @h.dup,
109
+ data_json: @h.dup,
110
+ text: 'hello',
111
+ number: '21'
112
+ )
113
+ end
114
+
115
+ should 'have encrypted methods' do
116
+ assert_equal true, @user.respond_to?(:encrypted_bank_account_number)
117
+ assert_equal true, @user.respond_to?(:bank_account_number)
118
+ assert_equal true, @user.respond_to?(:encrypted_social_security_number)
119
+ assert_equal true, @user.respond_to?(:social_security_number)
120
+ assert_equal true, @user.respond_to?(:data_yaml)
121
+ assert_equal true, @user.respond_to?(:data_json)
122
+ assert_equal false, @user.respond_to?(:encrypted_name)
123
+ end
124
+
125
+ should 'have unencrypted values' do
126
+ assert_equal @bank_account_number, @user.bank_account_number
127
+ assert_equal @social_security_number, @user.social_security_number
128
+ end
129
+
130
+ should 'have encrypted values' do
131
+ assert_equal @bank_account_number_encrypted, @user.encrypted_bank_account_number
132
+ assert_equal @social_security_number_encrypted, @user.encrypted_social_security_number
133
+ end
134
+
135
+ should 'support same iv' do
136
+ @user.social_security_number = @social_security_number
137
+ assert first_value = @user.social_security_number
138
+ # Assign the same value
139
+ @user.social_security_number = @social_security_number
140
+ assert_equal first_value, @user.social_security_number
141
+ end
142
+
143
+ should 'support a random iv' do
144
+ @user.string = @string
145
+ assert first_value = @user.encrypted_string
146
+ # Assign the same value
147
+ @user.string = @string.dup
148
+ assert_equal true, first_value != @user.encrypted_string
149
+ end
150
+
151
+ should 'support a random iv and compress' do
152
+ @user.string = @long_string
153
+ @user.long_string = @long_string
154
+
155
+ assert_equal true, (@user.encrypted_long_string.length.to_f / @user.encrypted_string.length) < 0.8
156
+ end
157
+
158
+ should 'encrypt' do
159
+ user = User.new
160
+ user.bank_account_number = @bank_account_number
161
+ assert_equal @bank_account_number, user.bank_account_number
162
+ assert_equal @bank_account_number_encrypted, user.encrypted_bank_account_number
163
+ end
164
+
165
+ should 'allow lookups using unencrypted or encrypted column name' do
166
+ @user.save!
167
+
168
+ inq = User.find_by_bank_account_number(@bank_account_number)
169
+ assert_equal @bank_account_number, inq.bank_account_number
170
+ assert_equal @bank_account_number_encrypted, inq.encrypted_bank_account_number
171
+
172
+ @user.delete
173
+ end
174
+
175
+ should 'all paths should lead to the same result' do
176
+ assert_equal @bank_account_number_encrypted, (@user.encrypted_social_security_number = @bank_account_number_encrypted)
177
+ assert_equal @bank_account_number, @user.social_security_number
178
+ assert_equal @bank_account_number_encrypted, @user.encrypted_social_security_number
179
+ end
180
+
181
+ should 'all paths should lead to the same result 2' do
182
+ assert_equal @bank_account_number, (@user.social_security_number = @bank_account_number)
183
+ assert_equal @bank_account_number_encrypted, @user.encrypted_social_security_number
184
+ assert_equal @bank_account_number, @user.social_security_number
185
+ end
186
+
187
+ should 'all paths should lead to the same result, check uninitialized' do
188
+ user = User.new
189
+ assert_equal nil, user.social_security_number
190
+ assert_equal @bank_account_number, (user.social_security_number = @bank_account_number)
191
+ assert_equal @bank_account_number, user.social_security_number
192
+ assert_equal @bank_account_number_encrypted, user.encrypted_social_security_number
193
+
194
+ assert_equal nil, (user.social_security_number = nil)
195
+ assert_equal nil, user.social_security_number
196
+ assert_equal nil, user.encrypted_social_security_number
197
+ end
198
+
199
+ should 'allow unencrypted values to be passed to the constructor' do
200
+ user = User.new(bank_account_number: @bank_account_number, social_security_number: @social_security_number)
201
+ assert_equal @bank_account_number, user.bank_account_number
202
+ assert_equal @social_security_number, user.social_security_number
203
+ assert_equal @bank_account_number_encrypted, user.encrypted_bank_account_number
204
+ assert_equal @social_security_number_encrypted, user.encrypted_social_security_number
205
+ end
206
+
207
+ should 'return encrypted attributes for the class' do
208
+ expect = {social_security_number: :encrypted_social_security_number, bank_account_number: :encrypted_bank_account_number}
209
+ result = User.encrypted_attributes
210
+ expect.each_pair {|k,v| assert_equal expect[k], result[k]}
211
+ end
212
+
213
+ should 'return encrypted keys for the class' do
214
+ expect = [:social_security_number, :bank_account_number]
215
+ result = User.encrypted_keys
216
+ expect.each {|val| assert_equal true, result.include?(val)}
217
+
218
+ # Also check encrypted_attribute?
219
+ expect.each {|val| assert_equal true, User.encrypted_attribute?(val)}
220
+ end
221
+
222
+ should 'return encrypted columns for the class' do
223
+ expect = [:encrypted_social_security_number, :encrypted_bank_account_number]
224
+ result = User.encrypted_columns
225
+ expect.each {|val| assert_equal true, result.include?(val)}
226
+
227
+ # Also check encrypted_column?
228
+ expect.each {|val| assert_equal true, User.encrypted_column?(val)}
229
+ end
230
+
231
+ should 'validate encrypted data' do
232
+ assert_equal true, @user.valid?
233
+ @user.encrypted_bank_account_number = '123'
234
+ assert_equal false, @user.valid?
235
+ assert_equal ["must be a value encrypted using SymmetricEncryption.encrypt"], @user.errors[:encrypted_bank_account_number]
236
+ @user.encrypted_bank_account_number = SymmetricEncryption.encrypt('123')
237
+ assert_equal true, @user.valid?
238
+ @user.bank_account_number = '123'
239
+ assert_equal true, @user.valid?
240
+ end
241
+
242
+ should 'validate un-encrypted string data' do
243
+ assert_equal true, @user.valid?
244
+ @user.text = '123'
245
+ assert_equal false, @user.valid?
246
+ assert_equal ["only allows letters"], @user.errors[:text]
247
+ @user.text = nil
248
+ assert_equal false, @user.valid?
249
+ assert_equal ["only allows letters", "can't be blank"], @user.errors[:text]
250
+ @user.text = ''
251
+ assert_equal false, @user.valid?
252
+ assert_equal ["only allows letters", "can't be blank"], @user.errors[:text]
253
+ end
254
+
255
+ should 'validate un-encrypted integer data with coercion' do
256
+ assert_equal true, @user.valid?
257
+ @user.number = '123'
258
+ assert_equal true, @user.valid?
259
+ assert_equal 123, @user.number
260
+ assert_equal true, @user.valid?
261
+ @user.number = ''
262
+ assert_equal false, @user.valid?
263
+ assert_equal nil, @user.number
264
+ assert_equal ["can't be blank"], @user.errors[:number]
265
+ @user.number = nil
266
+ assert_equal nil, @user.number
267
+ assert_equal nil, @user.encrypted_number
268
+ assert_equal false, @user.valid?
269
+ assert_equal ["can't be blank"], @user.errors[:number]
270
+ end
271
+
272
+ context "with saved user" do
273
+ setup do
274
+ @user.save!
275
+ end
276
+
277
+ teardown do
278
+ @user.destroy
279
+ end
280
+
281
+ should "return correct data type before save" do
282
+ u = User.new(integer_value: "5")
283
+ assert_equal 5, u.integer_value
284
+ assert u.integer_value.kind_of?(Integer)
285
+ end
286
+
287
+ should "handle gsub! for non-encrypted_field" do
288
+ @user.name.gsub!('a', 'v')
289
+ new_name = @name.gsub('a', 'v')
290
+ assert_equal new_name, @user.name
291
+ @user.reload
292
+ assert_equal new_name, @user.name
293
+ end
294
+
295
+ should "prevent gsub! on non-encrypted value of encrypted_field" do
296
+ # can't modify frozen String
297
+ assert_raises RuntimeError do
298
+ @user.bank_account_number.gsub!('5', '4')
299
+ end
300
+ end
301
+
302
+ should "revert changes on reload" do
303
+ new_bank_account_number = '444444444'
304
+ @user.bank_account_number = new_bank_account_number
305
+ assert_equal new_bank_account_number, @user.bank_account_number
306
+
307
+ # Reload User model from the database
308
+ @user.reload
309
+ assert_equal @bank_account_number_encrypted, @user.encrypted_bank_account_number
310
+ assert_equal @bank_account_number, @user.bank_account_number
311
+ end
312
+
313
+ should "revert changes to encrypted field on reload" do
314
+ new_bank_account_number = '111111111'
315
+ new_encrypted_bank_account_number = SymmetricEncryption.encrypt(new_bank_account_number)
316
+ @user.encrypted_bank_account_number = new_encrypted_bank_account_number
317
+ assert_equal new_encrypted_bank_account_number, @user.encrypted_bank_account_number
318
+ assert_equal new_bank_account_number, @user.bank_account_number
319
+
320
+ # Reload User model from the database
321
+ @user.reload
322
+ assert_equal @bank_account_number_encrypted, @user.encrypted_bank_account_number
323
+ assert_equal @bank_account_number, @user.bank_account_number
324
+ end
325
+
326
+ context "data types" do
327
+ setup do
328
+ @user_clone = User.find(@user.id)
329
+ end
330
+
331
+ [
332
+ { attribute: :integer_value, klass: Integer, value: INTEGER_VALUE, new_value: 98 },
333
+ { attribute: :float_value, klass: Float, value: FLOAT_VALUE, new_value: 45.4321 },
334
+ { attribute: :decimal_value, klass: BigDecimal, value: DECIMAL_VALUE, new_value: BigDecimal.new("99.95"), coercible: "22.51"},
335
+ { attribute: :datetime_value, klass: DateTime, value: DATETIME_VALUE, new_value: DateTime.new(1998, 10, 21, 8, 33, 28, "+5"), coercible: DATETIME_VALUE.to_time},
336
+ { attribute: :time_value, klass: Time, value: TIME_VALUE, new_value: Time.new(2000, 01, 01, 22, 30, 00, "-04:00") },
337
+ { attribute: :date_value, klass: Date, value: DATE_VALUE, new_value: Date.new(2027, 04, 02), coercible: DATE_VALUE.to_time },
338
+ { attribute: :true_value, klass: TrueClass, value: true, new_value: false },
339
+ { attribute: :false_value, klass: FalseClass, value: false, new_value: true },
340
+ ].each do |value_test|
341
+ context "#{value_test[:klass]} values" do
342
+ setup do
343
+ @attribute = value_test[:attribute]
344
+ @klass = value_test[:klass]
345
+ @value = value_test[:value]
346
+ @coercible = value_test[:coercible] || @value.to_s
347
+ @new_value = value_test[:new_value]
348
+ end
349
+
350
+ should "return correct data type" do
351
+ assert_equal @value, @user_clone.send(@attribute)
352
+ assert @user.clone.send(@attribute).kind_of?(@klass)
353
+ end
354
+
355
+ should "coerce data type before save" do
356
+ u = User.new(@attribute => @value)
357
+ assert_equal @value, u.send(@attribute)
358
+ assert u.send(@attribute).kind_of?(@klass), "Value supposed to be coerced into #{@klass}, but is #{u.send(@attribute).class.name}"
359
+ end
360
+
361
+ should "permit replacing value with nil" do
362
+ @user_clone.send("#{@attribute}=".to_sym, nil)
363
+ @user_clone.save!
364
+
365
+ @user.reload
366
+ assert_nil @user.send(@attribute)
367
+ assert_nil @user.send("encrypted_#{@attribute}".to_sym)
368
+ end
369
+
370
+ should "permit replacing value with an empty string" do
371
+ @user_clone.send("#{@attribute}=".to_sym, '')
372
+ @user_clone.save!
373
+
374
+ @user.reload
375
+ assert_nil @user.send(@attribute)
376
+ assert_nil @user.send("encrypted_#{@attribute}".to_sym)
377
+ end
378
+
379
+ should "permit replacing value with a blank string" do
380
+ @user_clone.send("#{@attribute}=".to_sym, ' ')
381
+ @user_clone.save!
382
+
383
+ @user.reload
384
+ assert_nil @user.send(@attribute)
385
+ assert_nil @user.send("encrypted_#{@attribute}".to_sym)
386
+ end
387
+
388
+ should "permit replacing value" do
389
+ @user_clone.send("#{@attribute}=".to_sym, @new_value)
390
+ @user_clone.save!
391
+
392
+ @user.reload
393
+ assert_equal @new_value, @user.send(@attribute)
394
+ end
395
+ end
396
+ end
397
+
398
+ context "JSON Serialization" do
399
+ setup do
400
+ # JSON Does not support symbols, so they will come back as strings
401
+ # Convert symbols to string in the test
402
+ @h.keys.each do |k|
403
+ @h[k.to_s] = @h[k]
404
+ @h.delete(k)
405
+ end
406
+ end
407
+
408
+ should "return correct data type" do
409
+ assert_equal @h, @user_clone.data_json
410
+ assert @user.clone.data_json.kind_of?(Hash)
411
+ end
412
+
413
+ should "not coerce data type (leaves as hash) before save" do
414
+ u = User.new(data_json: @h)
415
+ assert_equal @h, u.data_json
416
+ assert u.data_json.kind_of?(Hash)
417
+ end
418
+
419
+ should "permit replacing value with nil" do
420
+ @user_clone.data_json = nil
421
+ @user_clone.save!
422
+
423
+ @user.reload
424
+ assert_nil @user.data_json
425
+ assert_nil @user.encrypted_data_json
426
+ end
427
+
428
+ should "permit replacing value" do
429
+ new_value = @h.clone
430
+ new_value['c'] = 'C'
431
+ @user_clone.data_json = new_value
432
+ @user_clone.save!
433
+
434
+ @user.reload
435
+ assert_equal new_value, @user.data_json
436
+ end
437
+ end
438
+
439
+ context "YAML Serialization" do
440
+ should "return correct data type" do
441
+ assert_equal @h, @user_clone.data_yaml
442
+ assert @user.clone.data_yaml.kind_of?(Hash)
443
+ end
444
+
445
+ should "not coerce data type (leaves as hash) before save" do
446
+ u = User.new(data_yaml: @h)
447
+ assert_equal @h, u.data_yaml
448
+ assert u.data_yaml.kind_of?(Hash)
449
+ end
450
+
451
+ should "permit replacing value with nil" do
452
+ @user_clone.data_yaml = nil
453
+ @user_clone.save!
454
+
455
+ @user.reload
456
+ assert_nil @user.data_yaml
457
+ assert_nil @user.encrypted_data_yaml
458
+ end
459
+
460
+ should "permit replacing value" do
461
+ new_value = @h.clone
462
+ new_value[:c] = 'C'
463
+ @user_clone.data_yaml = new_value
464
+ @user_clone.save!
465
+
466
+ @user.reload
467
+ assert_equal new_value, @user.data_yaml
468
+ end
469
+ end
470
+
471
+ end
472
+ end
473
+ end
474
+ end