Skip to content

Commit 7d4a39c

Browse files
authored
Support encrypting binary columns (rails#50920)
* Support encrypting binary columns ActiveRecord Encryption doesn't prevent you from encrypting binary columns but it doesn't have proper support for it either. When the data is fed through encrypt/decrypt it is converted to a String. This means that the the encryption layer is not transparent to binary data - which should be passed as Type::Binary::Data. As a result the data is not properly escaped in the SQL queries or deserialized correctly after decryption. However it just happens to work fine for MySQL and SQLite because the MessageSerializer doesn't use any characters that need to be encoded. However if you try to use a custom serializer that does then it breaks. PostgreSQL on the other hand does not work - because the Bytea type is passed a String rather than a Type::Binary::Data to deserialize, it attempts to unescape the data and either mangles it or raises an error if it contains null bytes. The commit fixes the issue, by reserializing the data after encryption and decryption. For text data that's a no-op, but for binary data we'll convert it back to a Type::Binary::Data. * Extract decrypt_as_text/encrypt_as_text * Handle serialized binary data in encrypted columns Calling `serialize` is not always possible, because the column type might not expect to be serializing a String, for example when declared as serialzed or store attribute. With binary data the encryptor was passed an `ActiveModel::Type::Binary::Data`` and returned a `String``. In order to remain transparent we need to turn the data back into a `ActiveModel::Type::Binary::Data` before passing it on. We'll also rename `serialize`` to `text_to_database_type` to be a bit more descriptive.
1 parent ed76d0f commit 7d4a39c

File tree

5 files changed

+60
-2
lines changed

5 files changed

+60
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Add support for encrypting binary columns
2+
3+
Ensure encryption and decryption pass `Type::Binary::Data` around for binary data.
4+
5+
Previously encrypting binary columns with the `ActiveRecord::Encryption::MessageSerializer`
6+
incidentally worked for MySQL and SQLite, but not PostgreSQL.
7+
8+
*Donal McBreen*
9+
110
* Deprecated `ENV["SCHEMA_CACHE"]` in favor of `schema_cache_path` in the database configuration.
211

312
*Rafael Mendonça França*

activerecord/lib/active_record/encryption/encrypted_attribute_type.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def previous_type?
8181
@previous_type
8282
end
8383

84-
def decrypt(value)
84+
def decrypt_as_text(value)
8585
with_context do
8686
unless value.nil?
8787
if @default && @default == value
@@ -99,6 +99,10 @@ def decrypt(value)
9999
end
100100
end
101101

102+
def decrypt(value)
103+
text_to_database_type decrypt_as_text(value)
104+
end
105+
102106
def try_to_deserialize_with_previous_encrypted_types(value)
103107
previous_types.each.with_index do |type, index|
104108
break type.deserialize(value)
@@ -129,12 +133,16 @@ def serialize_with_current(value)
129133
encrypt(casted_value.to_s) unless casted_value.nil?
130134
end
131135

132-
def encrypt(value)
136+
def encrypt_as_text(value)
133137
with_context do
134138
encryptor.encrypt(value, **encryption_options)
135139
end
136140
end
137141

142+
def encrypt(value)
143+
text_to_database_type encrypt_as_text(value)
144+
end
145+
138146
def encryptor
139147
ActiveRecord::Encryption.encryptor
140148
end
@@ -150,6 +158,14 @@ def decryption_options
150158
def clean_text_scheme
151159
@clean_text_scheme ||= ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
152160
end
161+
162+
def text_to_database_type(value)
163+
if value && cast_type.binary?
164+
ActiveModel::Type::Binary::Data.new(value)
165+
else
166+
value
167+
end
168+
end
153169
end
154170
end
155171
end

activerecord/test/cases/encryption/encryptable_record_test.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,25 @@ def name
394394
assert_predicate OtherEncryptedPost.type_for_attribute(:title).scheme.previous_schemes, :one?
395395
end
396396

397+
test "binary data can be encrypted" do
398+
all_bytes = (0..255).map(&:chr).join
399+
assert_equal all_bytes, EncryptedBookWithBinary.create!(logo: all_bytes).logo
400+
assert_nil EncryptedBookWithBinary.create!(logo: nil).logo
401+
assert_equal "", EncryptedBookWithBinary.create!(logo: "").logo
402+
end
403+
404+
test "binary data can be encrypted uncompressed" do
405+
low_bytes = (0..127).map(&:chr).join
406+
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
409+
end
410+
411+
test "serialized binary data can be encrypted" do
412+
json_bytes = (32..127).map(&:chr)
413+
assert_equal json_bytes, EncryptedBookWithSerializedBinary.create!(logo: json_bytes).logo
414+
end
415+
397416
private
398417
def build_derived_key_provider_with(hash_digest_class)
399418
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,16 @@ class EncryptedBookWithUnencryptedDataOptedIn < ActiveRecord::Base
4343
validates :name, uniqueness: true
4444
encrypts :name, deterministic: true, support_unencrypted_data: true
4545
end
46+
47+
class EncryptedBookWithBinary < ActiveRecord::Base
48+
self.table_name = "encrypted_books"
49+
50+
encrypts :logo
51+
end
52+
53+
class EncryptedBookWithSerializedBinary < ActiveRecord::Base
54+
self.table_name = "encrypted_books"
55+
56+
serialize :logo, coder: JSON
57+
encrypts :logo
58+
end

activerecord/test/schema/schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
t.string :format
164164
t.column :name, :string, default: "<untitled>"
165165
t.column :original_name, :string
166+
t.column :logo, :binary
166167

167168
t.datetime :created_at
168169
t.datetime :updated_at

0 commit comments

Comments
 (0)