1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
|
# frozen_string_literal: true
require_relative 'test_helper'
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
def create_tables
ActiveRecord::Schema.define(version: 1) do
self.verbose = false
create_table :people do |t|
t.string :encrypted_email
t.string :password
t.string :encrypted_credentials
t.binary :salt
t.binary :key_iv
t.string :encrypted_email_salt
t.string :encrypted_credentials_salt
t.string :encrypted_email_iv
t.string :encrypted_credentials_iv
end
create_table :accounts do |t|
t.string :encrypted_password
t.string :encrypted_password_iv
t.string :encrypted_password_salt
t.string :key
end
create_table :users do |t|
t.string :login
t.string :encrypted_password
t.string :encrypted_password_iv
t.boolean :is_admin
end
create_table :prime_ministers do |t|
t.string :encrypted_name
t.string :encrypted_name_iv
end
create_table :addresses do |t|
t.binary :encrypted_street
t.binary :encrypted_street_iv
t.binary :encrypted_zipcode
t.string :mode
end
end
end
ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)
if ::ActiveRecord::VERSION::STRING > "4.0"
module Rack
module Test
class UploadedFile; end
end
end
require 'action_controller/metal/strong_parameters'
end
class Person < ActiveRecord::Base
self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
attr_encrypted :email, key: SECRET_KEY
attr_encrypted :credentials, key: Proc.new { |user| Encryptor.encrypt(value: user.salt, key: SECRET_KEY, iv: user.key_iv) }, marshal: true
after_initialize :initialize_salt_and_credentials
protected
def initialize_salt_and_credentials
self.key_iv ||= SecureRandom.random_bytes(12)
self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15]
self.credentials ||= { username: 'example', password: 'test' }
end
end
class PersonWithValidation < Person
validates_presence_of :email
end
class PersonWithProcMode < Person
attr_encrypted :email, key: SECRET_KEY, mode: Proc.new { :per_attribute_iv_and_salt }
attr_encrypted :credentials, key: SECRET_KEY, mode: Proc.new { :single_iv_and_salt }, insecure_mode: true
end
class PersonWithInstanceAttribute < Person
attr_encrypted :age
end
class Account < ActiveRecord::Base
ACCOUNT_ENCRYPTION_KEY = SecureRandom.urlsafe_base64(24)
attr_encrypted :password, key: :password_encryption_key
def encrypting?(attr)
attr_encrypted_attributes[attr][:operation] == :encrypting
end
def password_encryption_key
if encrypting?(:password)
self.key = ACCOUNT_ENCRYPTION_KEY
else
self.key
end
end
end
class PersonWithSerialization < ActiveRecord::Base
self.table_name = 'people'
attr_encrypted :email, key: SECRET_KEY
serialize :password
end
class UserWithProtectedAttribute < ActiveRecord::Base
self.table_name = 'users'
attr_encrypted :password, key: SECRET_KEY
attr_protected :is_admin if ::ActiveRecord::VERSION::STRING < "4.0"
end
class PersonUsingAlias < ActiveRecord::Base
self.table_name = 'people'
attr_encryptor :email, key: SECRET_KEY
end
class PrimeMinister < ActiveRecord::Base
attr_encrypted :name, marshal: true, key: SECRET_KEY
end
class Address < ActiveRecord::Base
self.attr_encrypted_options[:marshal] = false
self.attr_encrypted_options[:encode] = false
attr_encrypted :street, encode_iv: false, key: SECRET_KEY
attr_encrypted :zipcode, key: SECRET_KEY, mode: Proc.new { |address| address.mode.to_sym }, insecure_mode: true
end
class ActiveRecordTest < Minitest::Test
def setup
drop_all_tables
create_tables
end
def test_should_encrypt_email
@person = Person.create(email: 'test@example.com')
refute_nil @person.encrypted_email
refute_equal @person.email, @person.encrypted_email
assert_equal @person.email, Person.first.email
end
def test_should_marshal_and_encrypt_credentials
@person = Person.create
refute_nil @person.encrypted_credentials
refute_equal @person.credentials, @person.encrypted_credentials
assert_equal @person.credentials, Person.first.credentials
end
def test_should_encode_by_default
assert Person.attr_encrypted_options[:encode]
end
def test_should_validate_presence_of_email
@person = PersonWithValidation.new
assert !@person.valid?
assert !@person.errors[:email].empty? || @person.errors.on(:email)
end
def test_should_encrypt_decrypt_with_iv
@person = Person.create(email: 'test@example.com')
@person2 = Person.find(@person.id)
refute_nil @person2.encrypted_email_iv
assert_equal 'test@example.com', @person2.email
end
def test_should_ensure_attributes_can_be_deserialized
@person = PersonWithSerialization.new(email: 'test@example.com', password: %w(an array of strings))
@person.save
assert_equal @person.password, %w(an array of strings)
end
def test_should_create_an_account_regardless_of_arguments_order
Account.create!(key: SECRET_KEY, password: "password")
Account.create!(password: "password" , key: SECRET_KEY)
end
def test_should_set_attributes_regardless_of_arguments_order
# minitest does not implement `assert_nothing_raised` https://github.com/seattlerb/minitest/issues/112
Account.new.attributes = { password: "password", key: SECRET_KEY }
end
def test_should_create_changed_predicate
person = Person.create!(email: 'test@example.com')
refute person.email_changed?
person.email = 'test@example.com'
refute person.email_changed?
person.email = nil
assert person.email_changed?
person.email = 'test2@example.com'
assert person.email_changed?
end
# PENDING - this test is failing because attr_encrypted does not adhere to the
# interface contract for ActiveModel::Dirty as of ActiveRecord 6.1:
# https://devdocs.io/rails~6.1/activemodel/dirty
def pending_test_should_create_was_predicate
original_email = 'test@example.com'
person = Person.create!(email: original_email)
assert_equal original_email, person.email_was
person.email = 'test2@example.com'
assert_equal original_email, person.email_was
old_pm_name = "Winston Churchill"
pm = PrimeMinister.create!(name: old_pm_name)
assert_equal old_pm_name, pm.name_was
old_zipcode = "90210"
address = Address.create!(zipcode: old_zipcode, mode: "single_iv_and_salt")
assert_equal old_zipcode, address.zipcode_was
end
# PENDING - this test is failing because attr_encrypted does not adhere to the
# interface contract for ActiveModel::Dirty as of ActiveRecord 6.1:
# https://devdocs.io/rails~6.1/activemodel/dirty
def pending_test_attribute_was_works_when_options_for_old_encrypted_value_are_different_than_options_for_new_encrypted_value
pw = 'password'
crypto_key = SecureRandom.urlsafe_base64(24)
old_iv = SecureRandom.random_bytes(12)
account = Account.create
encrypted_value = Encryptor.encrypt(value: pw, iv: old_iv, key: crypto_key)
Account.where(id: account.id).update_all(key: crypto_key, encrypted_password_iv: [old_iv].pack('m'), encrypted_password: [encrypted_value].pack('m'))
account = Account.find(account.id)
assert_equal pw, account.password
account.password = pw.reverse
assert_equal pw, account.password_was
account.save
account.reload
assert_equal Account::ACCOUNT_ENCRYPTION_KEY, account.key
assert_equal pw.reverse, account.password
end
if ::ActiveRecord::VERSION::STRING > "4.0"
def test_should_assign_attributes
@user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
@user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login)
assert_equal 'modified', @user.login
end
def test_should_not_assign_protected_attributes
@user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
@user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login)
assert !@user.is_admin?
end
def test_should_raise_exception_if_not_permitted
@user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
assert_raises ActiveModel::ForbiddenAttributesError do
@user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true)
end
end
def test_should_raise_exception_on_init_if_not_permitted
assert_raises ActiveModel::ForbiddenAttributesError do
@user = UserWithProtectedAttribute.new ActionController::Parameters.new(login: 'modified', is_admin: true)
end
end
else
def test_should_assign_attributes
@user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
@user.attributes = { login: 'modified', is_admin: true }
assert_equal 'modified', @user.login
end
def test_should_not_assign_protected_attributes
@user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
@user.attributes = { login: 'modified', is_admin: true }
assert !@user.is_admin?
end
def test_should_assign_protected_attributes
@user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
if ::ActiveRecord::VERSION::STRING > "3.1"
@user.send(:assign_attributes, { login: 'modified', is_admin: true }, without_protection: true)
else
@user.send(:attributes=, { login: 'modified', is_admin: true }, false)
end
assert @user.is_admin?
end
end
def test_should_allow_assignment_of_nil_attributes
@person = Person.new
assert_nil(@person.attributes = nil)
end
def test_should_allow_proc_based_mode
@person = PersonWithProcMode.create(email: 'test@example.com', credentials: 'password123')
# Email is :per_attribute_iv_and_salt
assert_equal @person.class.attr_encrypted_attributes[:email][:mode].class, Proc
assert_equal @person.class.attr_encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt
refute_nil @person.encrypted_email_salt
refute_nil @person.encrypted_email_iv
# Credentials is :single_iv_and_salt
assert_equal @person.class.attr_encrypted_attributes[:credentials][:mode].class, Proc
assert_equal @person.class.attr_encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt
assert_nil @person.encrypted_credentials_salt
assert_nil @person.encrypted_credentials_iv
end
if ::ActiveRecord::VERSION::STRING > "3.1"
def test_should_allow_assign_attributes_with_nil
@person = Person.new
assert_nil(@person.assign_attributes nil)
end
end
def test_that_alias_encrypts_column
user = PersonUsingAlias.new
user.email = 'test@example.com'
user.save
refute_nil user.encrypted_email
refute_equal user.email, user.encrypted_email
assert_equal user.email, PersonUsingAlias.first.email
end
# See https://github.com/attr-encrypted/attr_encrypted/issues/68
def test_should_invalidate_virtual_attributes_on_reload
old_pm_name = 'Winston Churchill'
new_pm_name = 'Neville Chamberlain'
pm = PrimeMinister.create!(name: old_pm_name)
assert_equal old_pm_name, pm.name
pm.name = new_pm_name
assert_equal new_pm_name, pm.name
result = pm.reload
assert_equal pm, result
assert_equal old_pm_name, pm.name
end
def test_should_save_encrypted_data_as_binary
street = '123 Elm'
address = Address.create!(street: street)
refute_equal address.encrypted_street, street
assert_equal Address.first.street, street
end
def test_should_evaluate_proc_based_mode
street = '123 Elm'
zipcode = '12345'
address = Address.create(street: street, zipcode: zipcode, mode: :single_iv_and_salt)
address.reload
refute_equal address.encrypted_zipcode, zipcode
assert_equal address.zipcode, zipcode
end
# See https://github.com/attr-encrypted/attr_encrypted/issues/332
def test_attribute_instance_methods_as_symbols_available_returns_false
assert_equal false, ActiveRecord::Base.__send__(:attribute_instance_methods_as_symbols_available?)
end
# See https://github.com/attr-encrypted/attr_encrypted/issues/332
def test_does_not_define_virtual_attributes
instance = Person.new
%w[
encrypted_age encrypted_age=
encrypted_age_iv encrypted_age_iv=
encrypted_age_salt encrypted_age_salt=
].each do |method_name|
assert_equal false, instance.respond_to?(method_name), "should not define #{method_name}"
end
end
end
|