/
encryptor.rb
170 lines (147 loc) · 5.74 KB
/
encryptor.rb
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 a +clean_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