Skip to content

Commit bac5556

Browse files
authored
Merge pull request rails#52729 from djmb/binary-decryption
Fix binary decryption on Postgres
2 parents a8800c0 + 1d996ee commit bac5556

File tree

6 files changed

+85
-9
lines changed

6 files changed

+85
-9
lines changed

activerecord/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
* Deserialize binary data before decrypting
2+
3+
This ensures that we call `PG::Connection.unescape_bytea` on PostgreSQL before decryption.
4+
5+
*Donal McBreen*
6+
17
* Ensure `ActiveRecord::Encryption.config` is always ready before access.
28

39
Previously, `ActiveRecord::Encryption` configuration was deferred until `ActiveRecord::Base`

activerecord/lib/active_record/encryption/encrypted_attribute_type.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def decrypt_as_text(value)
100100
end
101101

102102
def decrypt(value)
103-
text_to_database_type decrypt_as_text(value)
103+
text_to_database_type decrypt_as_text(database_type_to_text(value))
104104
end
105105

106106
def try_to_deserialize_with_previous_encrypted_types(value)
@@ -170,6 +170,15 @@ def text_to_database_type(value)
170170
value
171171
end
172172
end
173+
174+
def database_type_to_text(value)
175+
if value && cast_type.binary?
176+
binary_cast_type = cast_type.serialized? ? cast_type.subtype : cast_type
177+
binary_cast_type.deserialize(value)
178+
else
179+
value
180+
end
181+
end
173182
end
174183
end
175184
end

activerecord/test/cases/encryption/encryptable_record_message_pack_serialized_test.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@
55
require "models/book_encrypted"
66
require "active_record/encryption/message_pack_message_serializer"
77

8-
class ActiveRecord::Encryption::EncryptableRecordTest < ActiveRecord::EncryptionTestCase
8+
class ActiveRecord::Encryption::EncryptableRecordMessagePackSerializedTest < ActiveRecord::EncryptionTestCase
99
fixtures :encrypted_books
1010

1111
test "binary data can be serialized with message pack" do
1212
all_bytes = (0..255).map(&:chr).join
13-
assert_equal all_bytes, EncryptedBookWithBinaryMessagePackSerialized.create!(logo: all_bytes).logo
13+
book = EncryptedBookWithBinaryMessagePackSerialized.create!(logo: all_bytes)
14+
assert_encrypted_attribute(book, :logo, all_bytes)
1415
end
1516

1617
test "binary data can be encrypted uncompressed and serialized with message pack" do
18+
# Strings below 140 bytes are not compressed
1719
low_bytes = (0..127).map(&:chr).join
1820
high_bytes = (128..255).map(&:chr).join
19-
assert_equal low_bytes, EncryptedBookWithBinaryMessagePackSerialized.create!(logo: low_bytes).logo
20-
assert_equal high_bytes, EncryptedBookWithBinaryMessagePackSerialized.create!(logo: high_bytes).logo
21+
22+
assert_encrypted_attribute(EncryptedBookWithBinaryMessagePackSerialized.create!(logo: low_bytes), :logo, low_bytes)
23+
assert_encrypted_attribute(EncryptedBookWithBinaryMessagePackSerialized.create!(logo: high_bytes), :logo, high_bytes)
2124
end
2225

2326
test "text columns cannot be serialized with message pack" do

activerecord/test/cases/encryption/encryptable_record_test.rb

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ class ActiveRecord::Encryption::EncryptableRecordTest < ActiveRecord::Encryption
9292
assert_encrypted_attribute(traffic_light, :state, states)
9393
end
9494

95+
test "encrypts serialized attributes where encrypts is declared first" do
96+
states = ["green", "red"]
97+
traffic_light = EncryptedFirstTrafficLight.create!(state: states, long_state: states)
98+
assert_encrypted_attribute(traffic_light, :state, states)
99+
end
100+
95101
test "encrypts store attributes with accessors" do
96102
traffic_light = EncryptedTrafficLightWithStoreState.create!(color: "red", long_state: ["green", "red"])
97103
assert_equal "red", traffic_light.color
@@ -404,13 +410,14 @@ def name
404410
test "binary data can be encrypted uncompressed" do
405411
low_bytes = (0..127).map(&:chr).join
406412
high_bytes = (128..255).map(&:chr).join
407-
assert_equal low_bytes, EncryptedBookWithBinary.create!(logo: low_bytes).logo
408-
assert_equal high_bytes, EncryptedBookWithBinary.create!(logo: high_bytes).logo
413+
assert_encrypted_attribute EncryptedBookWithBinary.create!(logo: low_bytes), :logo, low_bytes
414+
assert_encrypted_attribute EncryptedBookWithBinary.create!(logo: high_bytes), :logo, high_bytes
409415
end
410416

411417
test "serialized binary data can be encrypted" do
412418
json_bytes = (32..127).map(&:chr)
413-
assert_equal json_bytes, EncryptedBookWithSerializedBinary.create!(logo: json_bytes).logo
419+
assert_encrypted_attribute EncryptedBookWithSerializedFirstBinary.create!(logo: json_bytes), :logo, json_bytes
420+
assert_encrypted_attribute EncryptedBookWithSerializedSecondBinary.create!(logo: json_bytes), :logo, json_bytes
414421
end
415422

416423
test "can compress data with custom compressor" do
@@ -423,6 +430,17 @@ def name
423430
assert_equal :text, EncryptedPost.type_for_attribute(:body).type
424431
end
425432

433+
test "encrypts normalized data" do
434+
assert_encrypted_attribute EncryptedBookNormalizedFirst.create!(name: "Book"), :name, "book"
435+
assert_encrypted_attribute EncryptedBookNormalizedSecond.create!(name: "Book"), :name, "book"
436+
assert_encrypted_attribute EncryptedBookNormalizedFirst.create!(logo: "Book"), :logo, "book"
437+
assert_encrypted_attribute EncryptedBookNormalizedSecond.create!(logo: "Book"), :logo, "book"
438+
end
439+
440+
test "encrypts attribute data" do
441+
assert_encrypted_attribute EncryptedBookAttribute.create!(name: "2024-01-01"), :name, Date.new(2024, 1, 1)
442+
end
443+
426444
private
427445
def build_derived_key_provider_with(hash_digest_class)
428446
ActiveRecord::Encryption.with_encryption_context(key_generator: ActiveRecord::Encryption::KeyGenerator.new(hash_digest_class: hash_digest_class)) do

activerecord/test/models/book_encrypted.rb

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ class EncryptedBookWithDowncaseName < ActiveRecord::Base
2424
encrypts :name, deterministic: true, downcase: true
2525
end
2626

27+
class EncryptedBookNormalizedFirst < ActiveRecord::Base
28+
self.table_name = "encrypted_books"
29+
30+
normalizes :name, with: ->(value) { value.to_s.downcase }
31+
encrypts :name
32+
normalizes :logo, with: ->(value) { value.to_s.downcase }
33+
encrypts :logo
34+
end
35+
36+
class EncryptedBookNormalizedSecond < ActiveRecord::Base
37+
self.table_name = "encrypted_books"
38+
39+
encrypts :name
40+
normalizes :name, with: ->(value) { value.to_s.downcase }
41+
encrypts :logo
42+
normalizes :logo, with: ->(value) { value.to_s.downcase }
43+
end
44+
45+
class EncryptedBookAttribute < ActiveRecord::Base
46+
self.table_name = "encrypted_books"
47+
48+
attribute :name, :date
49+
encrypts :name
50+
end
51+
2752
class EncryptedBookThatIgnoresCase < ActiveRecord::Base
2853
self.table_name = "encrypted_books"
2954

@@ -50,13 +75,20 @@ class EncryptedBookWithBinary < ActiveRecord::Base
5075
encrypts :logo
5176
end
5277

53-
class EncryptedBookWithSerializedBinary < ActiveRecord::Base
78+
class EncryptedBookWithSerializedFirstBinary < ActiveRecord::Base
5479
self.table_name = "encrypted_books"
5580

5681
serialize :logo, coder: JSON
5782
encrypts :logo
5883
end
5984

85+
class EncryptedBookWithSerializedSecondBinary < ActiveRecord::Base
86+
self.table_name = "encrypted_books"
87+
88+
encrypts :logo
89+
serialize :logo, coder: JSON
90+
end
91+
6092
class EncryptedBookWithCustomCompressor < ActiveRecord::Base
6193
module CustomCompressor
6294
def self.deflate(value)

activerecord/test/models/traffic_light_encrypted.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ class EncryptedTrafficLight < TrafficLight
77
encrypts :state
88
end
99

10+
class EncryptedFirstTrafficLight < ActiveRecord::Base
11+
self.table_name = "traffic_lights"
12+
13+
encrypts :state
14+
serialize :state, type: Array
15+
serialize :long_state, type: Array
16+
end
17+
1018
class EncryptedTrafficLightWithStoreState < TrafficLight
1119
store :state, accessors: %i[ color ], coder: ActiveRecord::Coders::JSON
1220
encrypts :state

0 commit comments

Comments
 (0)