Skip to content

Commit 71e14aa

Browse files
authored
Merge pull request rails#51735 from heka1024/encryption-compressor
Introduce `compressor` option to `ActiveRecord::Encryption::Encryptor`
2 parents 738fde4 + 7542160 commit 71e14aa

File tree

12 files changed

+169
-14
lines changed

12 files changed

+169
-14
lines changed

activerecord/CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
* `ActiveRecord::Encryption::Encryptor` now supports a `:compressor` option to customize the compression algorithm used.
2+
3+
```ruby
4+
module ZstdCompressor
5+
def self.deflate(data)
6+
Zstd.compress(data)
7+
end
8+
9+
def self.inflate(data)
10+
Zstd.decompress(data)
11+
end
12+
end
13+
14+
class User
15+
encrypts :name, compressor: ZstdCompressor
16+
end
17+
```
18+
19+
You disable compression by passing `compress: false`.
20+
21+
```ruby
22+
class User
23+
encrypts :name, compress: false
24+
end
25+
```
26+
27+
*heka1024*
28+
129
* Add condensed `#inspect` for `ConnectionPool`, `AbstractAdapter`, and
230
`DatabaseConfig`.
331

activerecord/lib/active_record/encryption/config.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ module Encryption
88
class Config
99
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt, :hash_digest_class,
1010
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
11-
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
11+
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption,
12+
:compressor
1213

1314
def initialize
1415
set_defaults
@@ -55,6 +56,7 @@ def set_defaults
5556
self.previous_schemes = []
5657
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
5758
self.hash_digest_class = OpenSSL::Digest::SHA1
59+
self.compressor = Zlib
5860

5961
# TODO: Setting to false for now as the implementation is a bit experimental
6062
self.extend_queries = false

activerecord/lib/active_record/encryption/encryptable_record.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ module EncryptableRecord
4646
# * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read
4747
# the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic
4848
# encryption is used, they will be used to generate additional ciphertexts to check in the queries.
49-
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
49+
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [],
50+
compress: true, compressor: nil, **context_properties)
5051
self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
5152

5253
names.each do |name|
53-
encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
54+
encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous,
55+
compress: compress, compressor: compressor, **context_properties
5456
end
5557
end
5658

@@ -81,12 +83,13 @@ def global_previous_schemes_for(scheme)
8183
end
8284
end
8385

84-
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
86+
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [],
87+
compress: true, compressor: nil, **context_properties)
8588
encrypted_attributes << name.to_sym
8689

8790
decorate_attributes([name]) do |name, cast_type|
8891
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, \
89-
downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
92+
downcase: downcase, ignore_case: ignore_case, previous: previous, compress: compress, compressor: compressor, **context_properties
9093

9194
ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: scheme, cast_type: cast_type, default: columns_hash[name.to_s]&.default)
9295
end

activerecord/lib/active_record/encryption/encryptor.rb

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ module Encryption
1212
# It interacts with a KeyProvider for getting the keys, and delegate to
1313
# ActiveRecord::Encryption::Cipher the actual encryption algorithm.
1414
class Encryptor
15+
# The compressor to use for compressing the payload
16+
attr_reader :compressor
17+
1518
# === Options
1619
#
1720
# * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
1821
# Defaults to +true+.
19-
def initialize(compress: true)
22+
# * <tt>:compressor</tt> - The compressor to use.
23+
# 1. If compressor is provided, it will be used.
24+
# 2. If not, it will use ActiveRecord::Encryption.config.compressor which default value is +Zlib+.
25+
# If you want to use a custom compressor, it must respond to +deflate+ and +inflate+.
26+
def initialize(compress: true, compressor: nil)
2027
@compress = compress
28+
@compressor = compressor || ActiveRecord::Encryption.config.compressor
2129
end
2230

2331
# Encrypts +clean_text+ and returns the encrypted result
@@ -78,6 +86,10 @@ def binary?
7886
serializer.binary?
7987
end
8088

89+
def compress? # :nodoc:
90+
@compress
91+
end
92+
8193
private
8294
DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
8395
ENCODING_ERRORS = [EncodingError, Errors::Encoding]
@@ -130,12 +142,8 @@ def compress_if_worth_it(string)
130142
end
131143
end
132144

133-
def compress?
134-
@compress
135-
end
136-
137145
def compress(data)
138-
Zlib::Deflate.deflate(data).tap do |compressed_data|
146+
@compressor.deflate(data).tap do |compressed_data|
139147
compressed_data.force_encoding(data.encoding)
140148
end
141149
end
@@ -149,7 +157,7 @@ def uncompress_if_needed(data, compressed)
149157
end
150158

151159
def uncompress(data)
152-
Zlib::Inflate.inflate(data).tap do |uncompressed_data|
160+
@compressor.inflate(data).tap do |uncompressed_data|
153161
uncompressed_data.force_encoding(data.encoding)
154162
end
155163
end

activerecord/lib/active_record/encryption/scheme.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Scheme
1111
attr_accessor :previous_schemes
1212

1313
def initialize(key_provider: nil, key: nil, deterministic: nil, support_unencrypted_data: nil, downcase: nil, ignore_case: nil,
14-
previous_schemes: nil, **context_properties)
14+
previous_schemes: nil, compress: true, compressor: nil, **context_properties)
1515
# Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
1616
# can merge schemes without overriding values with defaults. See +#merge+
1717

@@ -24,8 +24,13 @@ def initialize(key_provider: nil, key: nil, deterministic: nil, support_unencryp
2424
@previous_schemes_param = previous_schemes
2525
@previous_schemes = Array.wrap(previous_schemes)
2626
@context_properties = context_properties
27+
@compress = compress
28+
@compressor = compressor
2729

2830
validate_config!
31+
32+
@context_properties[:encryptor] = Encryptor.new(compress: @compress) unless @compress
33+
@context_properties[:encryptor] = Encryptor.new(compressor: compressor) if compressor
2934
end
3035

3136
def ignore_case?
@@ -78,6 +83,8 @@ def compatible_with?(other_scheme)
7883
def validate_config!
7984
raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
8085
raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
86+
raise Errors::Configuration, "compressor: can't be used with compress: false" if !@compress && @compressor
87+
raise Errors::Configuration, "compressor: can't be used with encryptor" if @compressor && @context_properties[:encryptor]
8188
end
8289

8390
def key_provider_from_key

activerecord/test/cases/encryption/encryptable_record_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,11 @@ def name
413413
assert_equal json_bytes, EncryptedBookWithSerializedBinary.create!(logo: json_bytes).logo
414414
end
415415

416+
test "can compress data with custom compressor" do
417+
name = "a" * 141
418+
assert EncryptedBookWithCustomCompressor.create!(name: name).name.start_with?("[compressed]")
419+
end
420+
416421
private
417422
def build_derived_key_provider_with(hash_digest_class)
418423
ActiveRecord::Encryption.with_encryption_context(key_generator: ActiveRecord::Encryption::KeyGenerator.new(hash_digest_class: hash_digest_class)) do

activerecord/test/cases/encryption/encryptor_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ class ActiveRecord::Encryption::EncryptorTest < ActiveRecord::EncryptionTestCase
8888
assert_equal Encoding::ISO_8859_1, decrypted_text.encoding
8989
end
9090

91+
test "accept a custom compressor" do
92+
compressor = Module.new do
93+
def self.deflate(data)
94+
"compressed #{data}"
95+
end
96+
97+
def self.inflate(data)
98+
data.sub(/\Acompressed /, "")
99+
end
100+
end
101+
@encryptor = ActiveRecord::Encryption::Encryptor.new(compressor: compressor)
102+
content = SecureRandom.hex(5.kilobytes)
103+
104+
assert_encrypt_text content
105+
end
106+
91107
private
92108
def assert_encrypt_text(clean_text)
93109
encrypted_text = @encryptor.encrypt(clean_text)

activerecord/test/cases/encryption/scheme_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,36 @@ class ActiveRecord::Encryption::SchemeTest < ActiveRecord::EncryptionTestCase
77
test "validates config options when using encrypted attributes" do
88
assert_invalid_declaration deterministic: false, ignore_case: true
99
assert_invalid_declaration key: "1234", key_provider: ActiveRecord::Encryption::DerivedSecretKeyProvider.new("my secret")
10+
assert_invalid_declaration compress: false, compressor: Zlib
11+
assert_invalid_declaration compressor: Zlib, encryptor: ActiveRecord::Encryption::Encryptor.new
1012

1113
assert_valid_declaration deterministic: true
1214
assert_valid_declaration key: "1234"
1315
assert_valid_declaration key_provider: ActiveRecord::Encryption::DerivedSecretKeyProvider.new("my secret")
1416
end
1517

18+
test "should create a encryptor well when compressor is given" do
19+
MyCompressor = Class.new do
20+
def self.deflate(data)
21+
"deflated #{data}"
22+
end
23+
24+
def self.inflate(data)
25+
data.sub("deflated ", "")
26+
end
27+
end
28+
29+
type = declare_encrypts_with compressor: MyCompressor
30+
31+
assert_equal MyCompressor, type.scheme.to_h[:encryptor].compressor
32+
end
33+
34+
test "should create a encryptor well when compress is false" do
35+
type = declare_encrypts_with compress: false
36+
37+
assert_not type.scheme.to_h[:encryptor].compress?
38+
end
39+
1640
private
1741
def assert_invalid_declaration(**options)
1842
assert_raises ActiveRecord::Encryption::Errors::Configuration do

activerecord/test/models/book_encrypted.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,19 @@ class EncryptedBookWithSerializedBinary < ActiveRecord::Base
5656
serialize :logo, coder: JSON
5757
encrypts :logo
5858
end
59+
60+
class EncryptedBookWithCustomCompressor < ActiveRecord::Base
61+
module CustomCompressor
62+
def self.deflate(value)
63+
"[compressed] #{value}"
64+
end
65+
66+
def self.inflate(value)
67+
value
68+
end
69+
end
70+
71+
self.table_name = "encrypted_books"
72+
73+
encrypts :name, compressor: CustomCompressor
74+
end

activerecord/test/schema/schema.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
create_table :encrypted_books, id: :integer, force: true do |t|
162162
t.references :author
163163
t.string :format
164-
t.column :name, :string, default: "<untitled>"
164+
t.column :name, :string, default: "<untitled>", limit: 1024
165165
t.column :original_name, :string
166166
t.column :logo, :binary
167167

0 commit comments

Comments
 (0)