-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
encryptable_record_test.rb
436 lines (343 loc) · 15.9 KB
/
encryptable_record_test.rb
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# frozen_string_literal: true
require "cases/encryption/helper"
require "models/author_encrypted"
require "models/book_encrypted"
require "models/post_encrypted"
require "models/traffic_light_encrypted"
class ActiveRecord::Encryption::EncryptableRecordTest < ActiveRecord::EncryptionTestCase
fixtures :encrypted_books, :posts
test "encrypts the attribute seamlessly when creating and updating records" do
post = EncryptedPost.create!(title: "The Starfleet is here!", body: "take cover!")
assert_encrypted_attribute(post, :title, "The Starfleet is here!")
post.update!(title: "The Klingons are coming!")
assert_encrypted_attribute(post, :title, "The Klingons are coming!")
post.title = "You sure?"
post.save!
assert_encrypted_attribute(post, :title, "You sure?")
post[:title] = "The Klingons are leaving!"
post.save!
assert_encrypted_attribute(post, :title, "The Klingons are leaving!")
end
test "attribute is not accessible with the wrong key" do
ActiveRecord::Encryption.config.support_unencrypted_data = false
post = EncryptedPost.create!(title: "The Starfleet is here!", body: "take cover!")
post.reload.tags_count # accessing regular attributes works
assert_invalid_key_cant_read_attribute(post, :body)
end
test "swapping key_providers via with_encryption_context" do
key_provider1 = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(SecureRandom.base64(32))
key_provider2 = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(SecureRandom.base64(32))
post1 = post2 = nil
ActiveRecord::Encryption.with_encryption_context key_provider: key_provider1 do
post1 = EncryptedPost.create!(title: "post1!", body: "first post!")
end
ActiveRecord::Encryption.with_encryption_context key_provider: key_provider2 do
post2 = EncryptedPost.create!(title: "post2!", body: "second post!")
end
post1.reload
assert_raises ActiveRecord::Encryption::Errors::Decryption do
post1.title
end
post2.reload
assert_raises ActiveRecord::Encryption::Errors::Decryption do
post2.title
end
ActiveRecord::Encryption.with_encryption_context key_provider: key_provider1 do
post1.reload
assert_equal "post1!", post1.title
post2.reload
assert_raises ActiveRecord::Encryption::Errors::Decryption do
post2.title
end
end
ActiveRecord::Encryption.with_encryption_context key_provider: key_provider2 do
post2.reload
assert_equal "post2!", post2.title
post1.reload
assert_raises ActiveRecord::Encryption::Errors::Decryption do
post1.title
end
end
end
test "ignores nil values" do
assert_nil EncryptedBook.create!(name: nil).name
end
test "ignores empty values" do
assert_equal "", EncryptedBook.create!(name: "").name
end
test "encrypts serialized attributes" do
states = ["green", "red"]
traffic_light = EncryptedTrafficLight.create!(state: states, long_state: states)
assert_encrypted_attribute(traffic_light, :state, states)
end
test "encrypts store attributes with accessors" do
traffic_light = EncryptedTrafficLightWithStoreState.create!(color: "red", long_state: ["green", "red"])
assert_equal "red", traffic_light.color
assert_encrypted_attribute(traffic_light, :state, { "color" => "red" })
end
test "can configure a custom key provider on a per-record-class basis through the :key_provider option" do
post = EncryptedPost.create!(title: "The Starfleet is here!", body: "take cover!")
assert_encrypted_attribute(post, :body, "take cover!")
end
test "can configure a custom key on a per-record-class basis through the :key option" do
author = EncryptedAuthorWithKey.create!(name: "Stephen King")
assert_encrypted_attribute(author, :name, "Stephen King")
end
test "encrypts multiple attributes with different options at the same time" do
post = EncryptedPost.create! \
title: title = "The Starfleet is here!",
body: body = "<p>the Starfleet is here, we are safe now!</p>"
assert_encrypted_attribute(post, :title, title)
assert_encrypted_attribute(post, :body, body)
end
test "encrypted_attributes returns the list of encrypted attributes in a model (each record class holds their own list)" do
assert_equal Set.new([:title, :body]), EncryptedPost.encrypted_attributes
assert_not_equal EncryptedAuthor.encrypted_attributes, EncryptedPost.encrypted_attributes
end
test "deterministic_encrypted_attributes returns the list of deterministic encrypted attributes in a model (each record class holds their own list)" do
assert_equal [:name], EncryptedBook.deterministic_encrypted_attributes
assert_not_equal EncryptedPost.deterministic_encrypted_attributes, EncryptedBook.deterministic_encrypted_attributes
end
test "by default, encryption is not deterministic" do
post_1 = EncryptedPost.create!(title: "the same title", body: "some body")
post_2 = EncryptedPost.create!(title: "the same title", body: "some body")
assert_not_equal post_1.ciphertext_for(:title), post_2.ciphertext_for(:title)
end
test "deterministic attributes can be searched with Active Record queries" do
EncryptedBook.create!(name: "Dune")
assert EncryptedBook.find_by(name: "Dune")
assert_not EncryptedBook.find_by(name: "not Dune")
assert_equal 1, EncryptedBook.where(name: "Dune").count
end
test "deterministic attributes can be created by passing deterministic: true" do
book_1 = EncryptedBook.create!(name: "Dune")
book_2 = EncryptedBook.create!(name: "Dune")
assert_equal book_1.ciphertext_for(:name), book_2.ciphertext_for(:name)
end
test "deterministic ciphertexts remain constant" do
# We need to make sure these don't change or existing apps will stop working
ciphertext = "{\"p\":\"DIohhw==\",\"h\":{\"iv\":\"wEPaDcJP3VNIxaiz\",\"at\":\"X7+2xvvcu1k1if6Dy28Esw==\"}}"
book = UnencryptedBook.create name: ciphertext
book = EncryptedBook.find(book.id)
assert_equal "Dune", book.name
end
test "encryption errors when saving records will raise the error and don't save anything" do
assert_no_changes -> { BookThatWillFailToEncryptName.count } do
assert_raises ActiveRecord::Encryption::Errors::Encryption do
BookThatWillFailToEncryptName.create!(name: "Dune")
end
end
end
test "can work with pre-encryption nil values" do
ActiveRecord::Encryption.config.support_unencrypted_data = true
book = ActiveRecord::Encryption.without_encryption { EncryptedBook.create!(name: nil) }
assert_nil book.name
end
test "can work with pre-encryption empty values" do
ActiveRecord::Encryption.config.support_unencrypted_data = true
book = ActiveRecord::Encryption.without_encryption { EncryptedBook.create!(name: "") }
assert_equal "", book.name
end
test "can't modify encrypted attributes when frozen_encryption is true" do
post = posts(:welcome).becomes(EncryptedPost)
post.title = "Some new title"
assert_predicate post, :valid?
ActiveRecord::Encryption.with_encryption_context frozen_encryption: true do
assert_not post.valid?
end
end
test "can only save unencrypted attributes when frozen encryption is true" do
book = encrypted_books(:awdr)
ActiveRecord::Encryption.with_encryption_context frozen_encryption: true do
book.update! updated_at: Time.now
end
ActiveRecord::Encryption.with_encryption_context frozen_encryption: true do
assert_raises ActiveRecord::RecordInvalid do
book.update! name: "Some new title"
end
end
end
test "won't change the encoding of strings" do
author_name = "Jorge"
encoding = author_name.encoding
author = EncryptedAuthor.create!(name: author_name)
assert_equal encoding, author.reload.name.encoding
end
test "by default, it's case sensitive" do
EncryptedBook.create!(name: "Dune")
assert EncryptedBook.find_by(name: "Dune")
assert_not EncryptedBook.find_by(name: "dune")
end
test "when using downcase: true it ignores case since everything will be downcase" do
EncryptedBookWithDowncaseName.create!(name: "Dune")
assert EncryptedBookWithDowncaseName.find_by(name: "Dune")
assert EncryptedBookWithDowncaseName.find_by(name: "dune")
assert EncryptedBookWithDowncaseName.find_by(name: "DUNE")
end
test "when downcase: true it creates content downcased" do
EncryptedBookWithDowncaseName.create!(name: "Dune")
assert EncryptedBookWithDowncaseName.find_by_name("dune")
end
test "when ignore_case: true, it ignores case in queries but keep it when reading the attribute" do
EncryptedBookThatIgnoresCase.create!(name: "Dune")
book = EncryptedBookThatIgnoresCase.find_by_name("dune")
assert book
assert_equal "Dune", book.name
end
test "when ignore_case: true, it keeps both the attribute and the _original counterpart encrypted" do
book = EncryptedBookThatIgnoresCase.create!(name: "Dune")
assert_encrypted_attribute book, :name, "Dune"
assert_encrypted_attribute book, :original_name, "Dune"
end
test "when ignore_case: true, it lets you update attributes normally" do
book = EncryptedBookThatIgnoresCase.create!(name: "Dune")
book.update!(name: "Dune II")
assert_equal "Dune II", book.name
end
test "when ignore_case: true, it returns the actual value when not encrypted" do
ActiveRecord::Encryption.config.support_unencrypted_data = true
book = create_unencrypted_book_ignoring_case name: "Dune"
assert_equal "Dune", book.name
end
test "when ignore_case: true, users can override accessors and call super" do
overriding_class = Class.new(EncryptedBookThatIgnoresCase) do
self.table_name = "books"
def name
"#{super}-overridden"
end
end
overriding_class.create!(name: "Dune")
book = overriding_class.find_by_name("dune")
assert book
assert_equal "Dune-overridden", book.reload.name
end
test "reading a not encrypted value will raise a Decryption error when :support_unencrypted_data is false" do
ActiveRecord::Encryption.config.support_unencrypted_data = false
book = ActiveRecord::Encryption.without_encryption do
EncryptedBookThatIgnoresCase.create!(name: "dune")
end
assert_raises(ActiveRecord::Encryption::Errors::Decryption) do
book.name
end
end
test "reading a not encrypted value won't raise a Decryption error when :support_unencrypted_data is true" do
ActiveRecord::Encryption.config.support_unencrypted_data = true
author = ActiveRecord::Encryption.without_encryption do
EncryptedAuthor.create!(name: "Stephen King")
end
assert_equal "Stephen King", author.name
end
# Only run for adapters that add a default string limit when not provided (MySQL, 255)
if author_name_limit = EncryptedAuthor.columns_hash["name"].limit
# No column limits in SQLite
test "validate column sizes" do
assert_predicate EncryptedAuthor.new(name: "jorge"), :valid?
assert_not EncryptedAuthor.new(name: "a" * (author_name_limit + 1)).valid?
author = EncryptedAuthor.create(name: "a" * (author_name_limit + 1))
assert_not author.valid?
end
end
test "track previous changes properly for encrypted attributes" do
ActiveRecord::Encryption.config.support_unencrypted_data = true
book = EncryptedBook.create!(name: "Dune")
book.update!(created_at: 1.hour.ago)
assert_not book.name_previously_changed?
book.update!(name: "A new title!")
assert_predicate book, :name_previously_changed?
end
test "forces UTF-8 encoding for deterministic attributes by default" do
book = EncryptedBook.create!(name: "Dune".encode("ASCII-8BIT"))
assert_equal Encoding::UTF_8, book.reload.name.encoding
end
test "forces encoding for deterministic attributes based on the configured option" do
ActiveRecord::Encryption.config.forced_encoding_for_deterministic_encryption = Encoding::US_ASCII
book = EncryptedBook.create!(name: "Dune".encode("ASCII-8BIT"))
assert_equal Encoding::US_ASCII, book.reload.name.encoding
end
test "forced encoding for deterministic attributes will replace invalid characters" do
book = EncryptedBook.create!(name: "Hello \x93\xfa".b)
assert_equal "Hello ��", book.reload.name
end
test "forced encoding for deterministic attributes can be disabled" do
ActiveRecord::Encryption.config.forced_encoding_for_deterministic_encryption = nil
book = EncryptedBook.create!(name: "Dune".encode("US-ASCII"))
assert_equal Encoding::US_ASCII, book.reload.name.encoding
end
test "support encrypted attributes defined on columns with default values" do
book = EncryptedBook.create!
assert_encrypted_attribute(book, :name, "<untitled>")
end
test "loading records with encrypted attributes defined on columns with default values" do
skip unless supports_insert_on_duplicate_update?
EncryptedBook.insert({ format: "ebook" })
book = EncryptedBook.last
assert_equal "<untitled>", book.name
end
test "can dump and load records that use encryption" do
book = EncryptedBook.create!
assert_equal book, Marshal.load(Marshal.dump(book))
end
test "supports decrypting data encrypted non deterministically with SHA1 when digest class is SHA256" do
ActiveRecord::Encryption.configure \
primary_key: "the primary key",
deterministic_key: "the deterministic key",
key_derivation_salt: "the salt",
support_sha1_for_non_deterministic_encryption: true
key_provider_sha1 = build_derived_key_provider_with OpenSSL::Digest::SHA1
key_provider_sha256 = build_derived_key_provider_with OpenSSL::Digest::SHA256
encrypted_post_class_sha_1 = Class.new(Post) do
self.table_name = "posts"
encrypts :title, key_provider: key_provider_sha1
end
encrypted_post_class_sha_1.create! title: "Post 1", body: "The post body", type: nil
encrypted_post_class_sha_256 = Class.new(Post) do
self.table_name = "posts"
encrypts :title, key_provider: key_provider_sha256
end
assert_equal "Post 1", encrypted_post_class_sha_256.last.title
end
test "encryption schemes are resolved when used, not when declared" do
OtherEncryptedPost = Class.new(Post) do
self.table_name = "posts"
encrypts :title
end
ActiveRecord::Encryption.configure \
primary_key: "the primary key",
deterministic_key: "the deterministic key",
key_derivation_salt: "the salt",
support_sha1_for_non_deterministic_encryption: true
assert_predicate OtherEncryptedPost.type_for_attribute(:title).scheme.previous_schemes, :one?
end
test "binary data can be encrypted" do
all_bytes = (0..255).map(&:chr).join
assert_equal all_bytes, EncryptedBookWithBinary.create!(logo: all_bytes).logo
assert_nil EncryptedBookWithBinary.create!(logo: nil).logo
assert_equal "", EncryptedBookWithBinary.create!(logo: "").logo
end
test "binary data can be encrypted uncompressed" do
low_bytes = (0..127).map(&:chr).join
high_bytes = (128..255).map(&:chr).join
assert_equal low_bytes, EncryptedBookWithBinary.create!(logo: low_bytes).logo
assert_equal high_bytes, EncryptedBookWithBinary.create!(logo: high_bytes).logo
end
test "serialized binary data can be encrypted" do
json_bytes = (32..127).map(&:chr)
assert_equal json_bytes, EncryptedBookWithSerializedBinary.create!(logo: json_bytes).logo
end
private
def build_derived_key_provider_with(hash_digest_class)
ActiveRecord::Encryption.with_encryption_context(key_generator: ActiveRecord::Encryption::KeyGenerator.new(hash_digest_class: hash_digest_class)) do
ActiveRecord::Encryption::DerivedSecretKeyProvider.new(ActiveRecord::Encryption.config.primary_key)
end
end
class FailingKeyProvider
def decryption_key(message) end
def encryption_key
raise ActiveRecord::Encryption::Errors::Encryption
end
end
class BookThatWillFailToEncryptName < UnencryptedBook
self.table_name = "encrypted_books"
encrypts :name, key_provider: FailingKeyProvider.new
end
end