Skip to content

Commit

Permalink
Introduce compressor option to ActiveRecord::Encryption::Encryptor
Browse files Browse the repository at this point in the history
  • Loading branch information
heka1024 committed May 19, 2024
1 parent 965b8c3 commit 3a52769
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 4 deletions.
19 changes: 19 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
* `ActiveRecord::Encryption::Encryptor` now supports a `:compressor` option to customize the compression algorithm used.

```ruby
module ZstdCompressor
def self.compress(data)
Zstd.compress(data)
end

def self.uncompress(data)
Zstd.decompress(data)
end
end

class User
encrypts :name, encryptor: ActiveRecord::Encryption::Encryptor.new(compressor: ZstdCompressor)
end
```

*heka1024*

Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activerecord/CHANGELOG.md) for previous changes.
1 change: 1 addition & 0 deletions activerecord/lib/active_record/encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module Encryption
autoload :Properties
autoload :ReadOnlyNullEncryptor
autoload :Scheme
autoload :Compressor
end

class Cipher
Expand Down
22 changes: 22 additions & 0 deletions activerecord/lib/active_record/encryption/compressor.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion activerecord/lib/active_record/encryption/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions activerecord/lib/active_record/encryption/encryptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ class Encryptor
#
# * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
# Defaults to +true+.
def initialize(compress: true)
# * <tt>:compressor</tt> - 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions activerecord/test/cases/encryption/encryptor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions guides/source/active_record_encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 3a52769

Please sign in to comment.