1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
# frozen_string_literal: true
require "openssl"
require "zlib"
require "active_support/core_ext/numeric"
module ActiveRecord
module Encryption
# An encryptor exposes the encryption API that ActiveRecord::Encryption::EncryptedAttributeType
# uses for encrypting and decrypting attribute values.
#
# It interacts with a KeyProvider for getting the keys, and delegate to
# ActiveRecord::Encryption::Cipher the actual encryption algorithm.
class Encryptor
# === Options
#
# * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
# Defaults to +true+.
def initialize(compress: true)
@compress = compress
end
# Encrypts +clean_text+ and returns the encrypted result
#
# Internally, it will:
#
# 1. Create a new ActiveRecord::Encryption::Message
# 2. Compress and encrypt +clean_text+ as the message payload
# 3. Serialize it with +ActiveRecord::Encryption.message_serializer+ (+ActiveRecord::Encryption::SafeMarshal+
# by default)
# 4. Encode the result with Base 64
#
# === Options
#
# [:key_provider]
# Key provider to use for the encryption operation. It will default to
# +ActiveRecord::Encryption.key_provider+ when not provided.
#
# [:cipher_options]
# Cipher-specific options that will be passed to the Cipher configured in
# +ActiveRecord::Encryption.cipher+
def encrypt(clear_text, key_provider: default_key_provider, cipher_options: {})
clear_text = force_encoding_if_needed(clear_text) if cipher_options[:deterministic]
validate_payload_type(clear_text)
serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
end
# Decrypts an +encrypted_text+ and returns the result as clean text
#
# === Options
#
# [:key_provider]
# Key provider to use for the encryption operation. It will default to
# +ActiveRecord::Encryption.key_provider+ when not provided
#
# [:cipher_options]
# Cipher-specific options that will be passed to the Cipher configured in
# +ActiveRecord::Encryption.cipher+
def decrypt(encrypted_text, key_provider: default_key_provider, cipher_options: {})
message = deserialize_message(encrypted_text)
keys = key_provider.decryption_keys(message)
raise Errors::Decryption unless keys.present?
uncompress_if_needed(cipher.decrypt(message, key: keys.collect(&:secret), **cipher_options), message.headers.compressed)
rescue *(ENCODING_ERRORS + DECRYPT_ERRORS)
raise Errors::Decryption
end
# Returns whether the text is encrypted or not
def encrypted?(text)
deserialize_message(text)
true
rescue Errors::Encoding, *DECRYPT_ERRORS
false
end
def binary?
serializer.binary?
end
private
DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
ENCODING_ERRORS = [EncodingError, Errors::Encoding]
THRESHOLD_TO_JUSTIFY_COMPRESSION = 140.bytes
def default_key_provider
ActiveRecord::Encryption.key_provider
end
def validate_payload_type(clear_text)
unless clear_text.is_a?(String)
raise ActiveRecord::Encryption::Errors::ForbiddenClass, "The encryptor can only encrypt string values (#{clear_text.class})"
end
end
def cipher
ActiveRecord::Encryption.cipher
end
def build_encrypted_message(clear_text, key_provider:, cipher_options:)
key = key_provider.encryption_key
clear_text, was_compressed = compress_if_worth_it(clear_text)
cipher.encrypt(clear_text, key: key.secret, **cipher_options).tap do |message|
message.headers.add(key.public_tags)
message.headers.compressed = true if was_compressed
end
end
def serialize_message(message)
serializer.dump(message)
end
def deserialize_message(message)
serializer.load message
rescue ArgumentError, TypeError, Errors::ForbiddenClass
raise Errors::Encoding
end
def serializer
ActiveRecord::Encryption.message_serializer
end
# Under certain threshold, ZIP compression is actually worse that not compressing
def compress_if_worth_it(string)
if compress? && string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
[compress(string), true]
else
[string, false]
end
end
def compress?
@compress
end
def compress(data)
Zlib::Deflate.deflate(data).tap do |compressed_data|
compressed_data.force_encoding(data.encoding)
end
end
def uncompress_if_needed(data, compressed)
if compressed
uncompress(data)
else
data
end
end
def uncompress(data)
Zlib::Inflate.inflate(data).tap do |uncompressed_data|
uncompressed_data.force_encoding(data.encoding)
end
end
def force_encoding_if_needed(value)
if forced_encoding_for_deterministic_encryption && value && value.encoding != forced_encoding_for_deterministic_encryption
value.encode(forced_encoding_for_deterministic_encryption, invalid: :replace, undef: :replace)
else
value
end
end
def forced_encoding_for_deterministic_encryption
ActiveRecord::Encryption.config.forced_encoding_for_deterministic_encryption
end
end
end
end
|