diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index cd01fe1e9c43..fadd54d6fb6c 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,4 +1,24 @@
-* Add ENV["SKIP_TEST_DATABASE_TRUNCATE"] flag to speed up multi-process test runs on large DBs when all tests run within default txn. (This cuts ~10s from the test run of HEY when run by 24 processes against the 178 tables, since ~4,000 table truncates can then be skipped.)
+* `ActiveRecord::Encryption::Encryptor` now supports a `:compressor` option to customize the compression algorithm used.
+
+ ```ruby
+ module ZstdCompressor
+ def self.inflate(data)
+ Zstd.decompress(data)
+ end
+
+ def self.deflate(data)
+ Zstd.compress(data)
+ end
+ end
+
+ class User
+ encrypts :name, encryptor: ActiveRecord::Encryption::Encryptor.new(compressor: ZstdCompressor)
+ end
+ ```
+
+ *heka1024*
+
+* Add ENV["SKIP_TEST_DATABASE_TRUNCATE"] flag to speed up multi-process test runs on large DBs when all tests run within default txn. (This cuts ~10s from the test run of HEY when run by 24 processes against the 178 tables, since ~4,000 table truncates can then be skipped.)
*DHH*
diff --git a/activerecord/lib/active_record/encryption.rb b/activerecord/lib/active_record/encryption.rb
index 73ec3087767f..bc408ff0864a 100644
--- a/activerecord/lib/active_record/encryption.rb
+++ b/activerecord/lib/active_record/encryption.rb
@@ -34,6 +34,7 @@ module Encryption
autoload :Properties
autoload :ReadOnlyNullEncryptor
autoload :Scheme
+ autoload :Compressor
end
class Cipher
diff --git a/activerecord/lib/active_record/encryption/compressor.rb b/activerecord/lib/active_record/encryption/compressor.rb
new file mode 100644
index 000000000000..d69ddbbf92a1
--- /dev/null
+++ b/activerecord/lib/active_record/encryption/compressor.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Encryption
+ # The algorithm used for compressing and uncompressing data.
+ #
+ # It uses Zlib by default. If you want to use a different compressor, you can
+ # implement the +compress+ and +uncompress+ methods in a module and pass it to
+ # +ActiveRecord::Encryption::Encryptor+.
+ module Compressor
+ module Zlib # :nodoc:
+ def self.compress(data)
+ ::Zlib::Deflate.deflate(data)
+ end
+
+ def self.uncompress(data)
+ ::Zlib::Inflate.inflate(data)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/encryption/config.rb b/activerecord/lib/active_record/encryption/config.rb
index 368bbffb8262..5eb7eb55d61e 100644
--- a/activerecord/lib/active_record/encryption/config.rb
+++ b/activerecord/lib/active_record/encryption/config.rb
@@ -8,7 +8,8 @@ module Encryption
class Config
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt, :hash_digest_class,
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
- :excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
+ :excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption,
+ :compressor
def initialize
set_defaults
@@ -55,6 +56,7 @@ def set_defaults
self.previous_schemes = []
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
self.hash_digest_class = OpenSSL::Digest::SHA1
+ self.compressor = Compressor::Zlib
# TODO: Setting to false for now as the implementation is a bit experimental
self.extend_queries = false
diff --git a/activerecord/lib/active_record/encryption/encryptor.rb b/activerecord/lib/active_record/encryption/encryptor.rb
index baead108d235..340633b6d2a4 100644
--- a/activerecord/lib/active_record/encryption/encryptor.rb
+++ b/activerecord/lib/active_record/encryption/encryptor.rb
@@ -16,8 +16,13 @@ class Encryptor
#
# * :compress - Boolean indicating whether records should be compressed before encryption.
# Defaults to +true+.
- def initialize(compress: true)
+ # * :compressor - The compressor to use.
+ # 1. If compressor is provided, it will be used.
+ # 2. If not, it will use ActiveRecord::Encryption.config.compressor which default value is +ActiveRecord::Encryption::Compressor::Zlib+
+ # If you want to use a custom compressor, it must implement the +compress+ and +uncompress+ methods.
+ def initialize(compress: true, compressor: nil)
@compress = compress
+ @compressor = compressor || ActiveRecord::Encryption.config.compressor
end
# Encrypts +clean_text+ and returns the encrypted result
@@ -135,7 +140,7 @@ def compress?
end
def compress(data)
- Zlib::Deflate.deflate(data).tap do |compressed_data|
+ @compressor.compress(data).tap do |compressed_data|
compressed_data.force_encoding(data.encoding)
end
end
@@ -149,7 +154,7 @@ def uncompress_if_needed(data, compressed)
end
def uncompress(data)
- Zlib::Inflate.inflate(data).tap do |uncompressed_data|
+ @compressor.uncompress(data).tap do |uncompressed_data|
uncompressed_data.force_encoding(data.encoding)
end
end
diff --git a/activerecord/test/cases/encryption/encryptor_test.rb b/activerecord/test/cases/encryption/encryptor_test.rb
index 583ecaad2046..f431a8291e66 100644
--- a/activerecord/test/cases/encryption/encryptor_test.rb
+++ b/activerecord/test/cases/encryption/encryptor_test.rb
@@ -88,6 +88,22 @@ class ActiveRecord::Encryption::EncryptorTest < ActiveRecord::EncryptionTestCase
assert_equal Encoding::ISO_8859_1, decrypted_text.encoding
end
+ test "accept a custom compressor" do
+ compressor = Module.new do
+ def self.compress(data)
+ "compressed #{data}"
+ end
+
+ def self.uncompress(data)
+ data.sub(/\Acompressed /, "")
+ end
+ end
+ @encryptor = ActiveRecord::Encryption::Encryptor.new(compressor: compressor)
+ content = SecureRandom.hex(5.kilobytes)
+
+ assert_encrypt_text content
+ end
+
private
def assert_encrypt_text(clean_text)
encrypted_text = @encryptor.encrypt(clean_text)
diff --git a/guides/source/active_record_encryption.md b/guides/source/active_record_encryption.md
index e649b38e89de..227f2bc859fd 100644
--- a/guides/source/active_record_encryption.md
+++ b/guides/source/active_record_encryption.md
@@ -298,6 +298,34 @@ And you can disable this behavior and preserve the encoding in all cases with:
config.active_record.encryption.forced_encoding_for_deterministic_encryption = nil
```
+### Compression
+
+The library compresses encrypted payloads by default. This can save up to 30% of the storage space for larger payloads. You can disable compression by settings `compress: false` to encrypted attributes:
+
+```ruby
+class Article < ApplicationRecord
+ encrypts :content, encryptor: ActiveRecord::Encryption::Encryptor.new(compress: false)
+end
+```
+
+Also, you can configure the algorithm used for the compression. The default is `Zlib`. You can implement your own compressor by creating a class that responds to `def compress(data)` and `def uncompress(data)`.
+
+```ruby
+require "zstd-ruby"
+
+module ZstdCompressor
+ def self.compress(data)
+ Zstd.compress(data)
+ end
+
+ def self.uncompress(data)
+ Zstd.decompress(data)
+ end
+end
+
+config.active_record.encryption.compressor = ZstdCompressor
+```
+
## Key Management
Key providers implement key management strategies. You can configure key providers globally, or on a per attribute basis.
@@ -497,6 +525,10 @@ The digest algorithm used to derive keys. `OpenSSL::Digest::SHA256` by default.
Supports decrypting data encrypted non-deterministically with a digest class SHA1. Default is false, which
means it will only support the digest algorithm configured in `config.active_record.encryption.hash_digest_class`.
+#### `config.active_record.encryption.compressor`
+
+The compressor used to compress encrypted payloads. It should respond to `compress` and `uncompress`. Default is `Zlib`. You can find more information about compressors in the [Compression](#compression) section.
+
### Encryption Contexts
An encryption context defines the encryption components that are used in a given moment. There is a default encryption context based on your global configuration, but you can configure a custom context for a given attribute or when running a specific block of code.