symmetric-encryption 3.4.0 → 3.6.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.
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