symmetric-encryption 4.0.1 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +0 -7
- data/lib/symmetric_encryption/cipher.rb +11 -4
- data/lib/symmetric_encryption/cli.rb +39 -28
- data/lib/symmetric_encryption/config.rb +9 -6
- data/lib/symmetric_encryption/encoder.rb +6 -0
- data/lib/symmetric_encryption/extensions/mongoid/encrypted.rb +1 -0
- data/lib/symmetric_encryption/generator.rb +1 -1
- data/lib/symmetric_encryption/header.rb +7 -5
- data/lib/symmetric_encryption/key.rb +2 -62
- data/lib/symmetric_encryption/keystore/aws.rb +172 -0
- data/lib/symmetric_encryption/keystore/environment.rb +7 -30
- data/lib/symmetric_encryption/keystore/file.rb +8 -30
- data/lib/symmetric_encryption/keystore/heroku.rb +22 -0
- data/lib/symmetric_encryption/keystore/memory.rb +4 -3
- data/lib/symmetric_encryption/keystore.rb +151 -36
- data/lib/symmetric_encryption/railtie.rb +9 -4
- data/lib/symmetric_encryption/railties/symmetric_encryption_validator.rb +1 -0
- data/lib/symmetric_encryption/reader.rb +50 -58
- data/lib/symmetric_encryption/symmetric_encryption.rb +2 -1
- data/lib/symmetric_encryption/utils/aws.rb +141 -0
- data/lib/symmetric_encryption/utils/re_encrypt_files.rb +12 -5
- data/lib/symmetric_encryption/version.rb +1 -1
- data/lib/symmetric_encryption/writer.rb +33 -27
- data/lib/symmetric_encryption.rb +27 -6
- data/test/active_record_test.rb +25 -25
- data/test/cipher_test.rb +3 -3
- data/test/header_test.rb +1 -1
- data/test/key_test.rb +0 -157
- data/test/keystore/aws_test.rb +133 -0
- data/test/keystore/environment_test.rb +3 -51
- data/test/keystore/file_test.rb +13 -52
- data/test/keystore/heroku_test.rb +70 -0
- data/test/keystore_test.rb +200 -4
- data/test/mongoid_test.rb +15 -15
- data/test/reader_test.rb +28 -8
- data/test/symmetric_encryption_test.rb +2 -2
- data/test/test_db.sqlite3 +0 -0
- data/test/test_helper.rb +1 -0
- data/test/utils/aws_test.rb +74 -0
- data/test/writer_test.rb +48 -46
- metadata +29 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 825c28cf5b38d4cf22d26f4ed8196bbf1085ee0e09b372ab3c30aa055238902f
|
4
|
+
data.tar.gz: de736c34beb30c50e9316f0f85cade71b022caac4fe4ca94cc61c94d0c8fe1aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 711727ec509464e8807f798f82c4944c25f37a2cd48ec190c93f4f0134437887c8355fedf199157727ffd7faaa1023146fa95946ee6f2f9ec8a648426ec68f60
|
7
|
+
data.tar.gz: 834ae0b58bd0ca3011b846a94709203ac648a6cd341c5f86f18c82fa4b4c9b16abfc400af9d3c7c0f0348b4127db7c425de56345738bfde0de5dae97666a4031
|
data/README.md
CHANGED
@@ -29,13 +29,6 @@ Checkout the sister project [Rocket Job](http://rocketjob.io): Ruby's missing ba
|
|
29
29
|
|
30
30
|
Fully supports Symmetric Encryption to encrypt data in flight and at rest while running jobs in the background.
|
31
31
|
|
32
|
-
## Supports
|
33
|
-
|
34
|
-
Symmetric Encryption works with the following Ruby VMs:
|
35
|
-
|
36
|
-
- Ruby 2.1 and higher.
|
37
|
-
- JRuby 9.1 and higher.
|
38
|
-
|
39
32
|
## Upgrading to SymmetricEncryption V4
|
40
33
|
|
41
34
|
Version 4 of Symmetric Encryption has completely adopted the Ruby keyword arguments on most API's where
|
@@ -18,8 +18,8 @@ module SymmetricEncryption
|
|
18
18
|
encoding: :base64strict,
|
19
19
|
**config)
|
20
20
|
|
21
|
-
|
22
|
-
key =
|
21
|
+
Keystore.migrate_config!(config)
|
22
|
+
key = Keystore.read_key(cipher_name: cipher_name, **config)
|
23
23
|
|
24
24
|
Cipher.new(
|
25
25
|
key: key.key,
|
@@ -133,8 +133,10 @@ module SymmetricEncryption
|
|
133
133
|
# compression
|
134
134
|
def encrypt(str, random_iv: false, compress: false, header: always_add_header)
|
135
135
|
return if str.nil?
|
136
|
+
|
136
137
|
str = str.to_s
|
137
138
|
return str if str.empty?
|
139
|
+
|
138
140
|
encrypted = binary_encrypt(str, random_iv: random_iv, compress: compress, header: header)
|
139
141
|
encode(encrypted)
|
140
142
|
end
|
@@ -161,6 +163,7 @@ module SymmetricEncryption
|
|
161
163
|
return unless decoded
|
162
164
|
|
163
165
|
return decoded if decoded.empty?
|
166
|
+
|
164
167
|
decrypted = binary_decrypt(decoded)
|
165
168
|
|
166
169
|
# Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary
|
@@ -178,6 +181,7 @@ module SymmetricEncryption
|
|
178
181
|
# Returned string is UTF8 encoded except for encoding :none
|
179
182
|
def encode(binary_string)
|
180
183
|
return binary_string if binary_string.nil? || (binary_string == '')
|
184
|
+
|
181
185
|
encoder.encode(binary_string)
|
182
186
|
end
|
183
187
|
|
@@ -187,6 +191,7 @@ module SymmetricEncryption
|
|
187
191
|
# Returned string is Binary encoded
|
188
192
|
def decode(encoded_string)
|
189
193
|
return encoded_string if encoded_string.nil? || (encoded_string == '')
|
194
|
+
|
190
195
|
encoder.decode(encoded_string)
|
191
196
|
end
|
192
197
|
|
@@ -243,6 +248,7 @@ module SymmetricEncryption
|
|
243
248
|
# See #encrypt to encrypt and encode the result as a string.
|
244
249
|
def binary_encrypt(str, random_iv: false, compress: false, header: always_add_header)
|
245
250
|
return if str.nil?
|
251
|
+
|
246
252
|
string = str.to_s
|
247
253
|
return string if string.empty?
|
248
254
|
|
@@ -300,6 +306,7 @@ module SymmetricEncryption
|
|
300
306
|
# is automatically set to the same UTF-8 or Binary encoding
|
301
307
|
def binary_decrypt(encrypted_string, header: Header.new)
|
302
308
|
return if encrypted_string.nil?
|
309
|
+
|
303
310
|
str = encrypted_string.to_s
|
304
311
|
str.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
305
312
|
return str if str.empty?
|
@@ -309,8 +316,8 @@ module SymmetricEncryption
|
|
309
316
|
|
310
317
|
openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || cipher_name)
|
311
318
|
openssl_cipher.decrypt
|
312
|
-
openssl_cipher.key
|
313
|
-
if (iv
|
319
|
+
openssl_cipher.key = header.key || @key
|
320
|
+
if (iv = header.iv || @iv)
|
314
321
|
openssl_cipher.iv = iv
|
315
322
|
end
|
316
323
|
result = openssl_cipher.update(data)
|
@@ -6,7 +6,7 @@ module SymmetricEncryption
|
|
6
6
|
:decrypt, :random_password, :new_keys, :generate, :environment,
|
7
7
|
:keystore, :re_encrypt, :version, :output_file_name, :compress,
|
8
8
|
:environments, :cipher_name, :rolling_deploy, :rotate_keys, :rotate_kek, :prompt, :show_version,
|
9
|
-
:cleanup_keys, :activate_key, :migrate
|
9
|
+
:cleanup_keys, :activate_key, :migrate, :regions
|
10
10
|
|
11
11
|
KEYSTORES = %i[heroku environment file].freeze
|
12
12
|
|
@@ -19,7 +19,7 @@ module SymmetricEncryption
|
|
19
19
|
@environment = ENV['SYMMETRIC_ENCRYPTION_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
20
20
|
@config_file_path = File.expand_path(ENV['SYMMETRIC_ENCRYPTION_CONFIG'] || 'config/symmetric-encryption.yml')
|
21
21
|
@app_name = 'symmetric-encryption'
|
22
|
-
@key_path = '
|
22
|
+
@key_path = '~/.symmetric-encryption'
|
23
23
|
@cipher_name = 'aes-256-cbc'
|
24
24
|
@rolling_deploy = false
|
25
25
|
@prompt = false
|
@@ -70,7 +70,7 @@ module SymmetricEncryption
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def parser
|
73
|
-
@parser
|
73
|
+
@parser ||= OptionParser.new do |opts|
|
74
74
|
opts.banner = <<~BANNER
|
75
75
|
Symmetric Encryption v#{VERSION}
|
76
76
|
|
@@ -99,10 +99,14 @@ module SymmetricEncryption
|
|
99
99
|
@prompt = true
|
100
100
|
end
|
101
101
|
|
102
|
-
opts.on '-z', '--compress', 'Compress encrypted output file.' do
|
102
|
+
opts.on '-z', '--compress', 'Compress encrypted output file. [Default for encrypting files]' do
|
103
103
|
@compress = true
|
104
104
|
end
|
105
105
|
|
106
|
+
opts.on '-Z', '--no-compress', 'Does not compress the output file. [Default for encrypting strings]' do
|
107
|
+
@compress = false
|
108
|
+
end
|
109
|
+
|
106
110
|
opts.on '-E', '--env ENVIRONMENT', "Environment to use in the config file. Default: SYMMETRIC_ENCRYPTION_ENV || RACK_ENV || RAILS_ENV || 'development'" do |environment|
|
107
111
|
@environment = environment
|
108
112
|
end
|
@@ -127,11 +131,15 @@ module SymmetricEncryption
|
|
127
131
|
@generate = config
|
128
132
|
end
|
129
133
|
|
130
|
-
opts.on '-s', '--keystore heroku|environment|file', '
|
134
|
+
opts.on '-s', '--keystore heroku|environment|file|aws', 'Which keystore to use during generation or re-encryption.' do |keystore|
|
131
135
|
@keystore = (keystore || 'file').downcase.to_sym
|
132
136
|
end
|
133
137
|
|
134
|
-
opts.on '-
|
138
|
+
opts.on '-B', '--regions [us-east-1,us-east-2,us-west-1,us-west-2]', 'AWS KMS Regions to encrypt data key with.' do |regions|
|
139
|
+
@regions = regions.to_s.split(',').collect(&:strip) if regions
|
140
|
+
end
|
141
|
+
|
142
|
+
opts.on '-K', '--key-path KEY_PATH', 'Output path in which to write generated key files. Default: ~/.symmetric-encryption' do |path|
|
135
143
|
@key_path = path
|
136
144
|
end
|
137
145
|
|
@@ -197,26 +205,21 @@ module SymmetricEncryption
|
|
197
205
|
end
|
198
206
|
|
199
207
|
def generate_new_config
|
208
|
+
unless KEYSTORES.include?(keystore)
|
209
|
+
puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
|
210
|
+
exit(-3)
|
211
|
+
end
|
212
|
+
|
200
213
|
config_file_does_not_exist!
|
201
214
|
self.environments ||= %i[development test release production]
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
elsif %i[heroku environment].include?(keystore)
|
211
|
-
SymmetricEncryption::Keystore::Environment.new_config(
|
212
|
-
app_name: app_name,
|
213
|
-
environments: environments,
|
214
|
-
cipher_name: cipher_name
|
215
|
-
)
|
216
|
-
else
|
217
|
-
puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
|
218
|
-
exit(-3)
|
219
|
-
end
|
215
|
+
args = {
|
216
|
+
app_name: app_name,
|
217
|
+
environments: environments,
|
218
|
+
cipher_name: cipher_name
|
219
|
+
}
|
220
|
+
args[:key_path] = key_path if key_path
|
221
|
+
args[:regions] = regions if regions && !regions.empty?
|
222
|
+
cfg = Keystore.generate_data_keys(keystore, **args)
|
220
223
|
Config.write_file(config_file_path, cfg)
|
221
224
|
puts "New configuration file created at: #{config_file_path}"
|
222
225
|
end
|
@@ -228,8 +231,13 @@ module SymmetricEncryption
|
|
228
231
|
end
|
229
232
|
|
230
233
|
def run_rotate_keys
|
234
|
+
if keystore && KEYSTORES.include?(keystore)
|
235
|
+
puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
|
236
|
+
exit(-3)
|
237
|
+
end
|
238
|
+
|
231
239
|
config = Config.read_file(config_file_path)
|
232
|
-
SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name, rolling_deploy: rolling_deploy)
|
240
|
+
SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name, rolling_deploy: rolling_deploy, keystore: keystore)
|
233
241
|
Config.write_file(config_file_path, config)
|
234
242
|
puts "Existing configuration file updated with new keys: #{config_file_path}"
|
235
243
|
end
|
@@ -246,7 +254,8 @@ module SymmetricEncryption
|
|
246
254
|
config.each_pair do |env, cfg|
|
247
255
|
next if environments && !environments.include?(env.to_sym)
|
248
256
|
next unless ciphers = cfg[:ciphers]
|
249
|
-
|
257
|
+
|
258
|
+
highest = ciphers.max_by { |i| i[:version] }
|
250
259
|
ciphers.clear
|
251
260
|
ciphers << highest
|
252
261
|
end
|
@@ -260,7 +269,8 @@ module SymmetricEncryption
|
|
260
269
|
config.each_pair do |env, cfg|
|
261
270
|
next if environments && !environments.include?(env.to_sym)
|
262
271
|
next unless ciphers = cfg[:ciphers]
|
263
|
-
|
272
|
+
|
273
|
+
highest = ciphers.max_by { |i| i[:version] }
|
264
274
|
ciphers.delete(highest)
|
265
275
|
ciphers.unshift(highest)
|
266
276
|
end
|
@@ -308,7 +318,7 @@ module SymmetricEncryption
|
|
308
318
|
|
309
319
|
puts('Values do not match, please try again') if value1 != value2
|
310
320
|
end
|
311
|
-
|
321
|
+
compress = false if compress.nil?
|
312
322
|
encrypted = SymmetricEncryption.cipher(version).encrypt(value1, compress: compress)
|
313
323
|
output_file_name ? File.open(output_file_name, 'wb') { |f| f << encrypted } : puts("\n\nEncrypted: #{encrypted}\n\n")
|
314
324
|
end
|
@@ -330,6 +340,7 @@ module SymmetricEncryption
|
|
330
340
|
# Ensure that the config file does not already exist before generating a new one.
|
331
341
|
def config_file_does_not_exist!
|
332
342
|
return unless File.exist?(config_file_path)
|
343
|
+
|
333
344
|
puts "\nConfiguration file already exists, please move or rename: #{config_file_path}\n\n"
|
334
345
|
exit(-1)
|
335
346
|
end
|
@@ -53,8 +53,8 @@ module SymmetricEncryption
|
|
53
53
|
env ||= defined?(Rails) ? Rails.env : ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
54
54
|
|
55
55
|
unless file_name
|
56
|
-
root
|
57
|
-
file_name
|
56
|
+
root = defined?(Rails) ? Rails.root : '.'
|
57
|
+
file_name =
|
58
58
|
if (env_var = ENV['SYMMETRIC_ENCRYPTION_CONFIG'])
|
59
59
|
File.expand_path(env_var)
|
60
60
|
else
|
@@ -101,6 +101,7 @@ module SymmetricEncryption
|
|
101
101
|
object
|
102
102
|
end
|
103
103
|
end
|
104
|
+
|
104
105
|
private_class_method :deep_symbolize_keys
|
105
106
|
|
106
107
|
# Iterate through the Hash symbolizing all keys.
|
@@ -119,28 +120,29 @@ module SymmetricEncryption
|
|
119
120
|
object
|
120
121
|
end
|
121
122
|
end
|
123
|
+
|
122
124
|
private_class_method :deep_stringify_keys
|
123
125
|
|
124
126
|
# Migrate old configuration format for this environment
|
125
127
|
def self.migrate_old_formats!(config)
|
126
128
|
# Inline single cipher before :ciphers
|
127
129
|
unless config.key?(:ciphers)
|
128
|
-
inline_cipher
|
130
|
+
inline_cipher = {}
|
129
131
|
config.keys.each { |key| inline_cipher[key] = config.delete(key) }
|
130
|
-
config[:ciphers]
|
132
|
+
config[:ciphers] = [inline_cipher]
|
131
133
|
end
|
132
134
|
|
133
135
|
# Copy Old :private_rsa_key into each ciphers config
|
134
136
|
# Cipher.from_config replaces it with the RSA Kek
|
135
137
|
if config[:private_rsa_key]
|
136
|
-
private_rsa_key
|
138
|
+
private_rsa_key = config.delete(:private_rsa_key)
|
137
139
|
config[:ciphers].each { |cipher| cipher[:private_rsa_key] = private_rsa_key }
|
138
140
|
end
|
139
141
|
|
140
142
|
# Old :cipher_name
|
141
143
|
config[:ciphers].each do |cipher|
|
142
144
|
if (old_key_name_cipher = cipher.delete(:cipher))
|
143
|
-
cipher[:cipher_name]
|
145
|
+
cipher[:cipher_name] = old_key_name_cipher
|
144
146
|
end
|
145
147
|
|
146
148
|
# Only temporarily used during v4 Beta process
|
@@ -156,6 +158,7 @@ module SymmetricEncryption
|
|
156
158
|
end
|
157
159
|
config
|
158
160
|
end
|
161
|
+
|
159
162
|
private_class_method :migrate_old_formats!
|
160
163
|
end
|
161
164
|
end
|
@@ -36,12 +36,14 @@ module SymmetricEncryption
|
|
36
36
|
class Base64
|
37
37
|
def encode(binary_string)
|
38
38
|
return binary_string if binary_string.nil? || (binary_string == '')
|
39
|
+
|
39
40
|
encoded_string = ::Base64.encode64(binary_string)
|
40
41
|
encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
41
42
|
end
|
42
43
|
|
43
44
|
def decode(encoded_string)
|
44
45
|
return encoded_string if encoded_string.nil? || (encoded_string == '')
|
46
|
+
|
45
47
|
decoded_string = ::Base64.decode64(encoded_string)
|
46
48
|
decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
47
49
|
end
|
@@ -50,12 +52,14 @@ module SymmetricEncryption
|
|
50
52
|
class Base64Strict
|
51
53
|
def encode(binary_string)
|
52
54
|
return binary_string if binary_string.nil? || (binary_string == '')
|
55
|
+
|
53
56
|
encoded_string = ::Base64.strict_encode64(binary_string)
|
54
57
|
encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
55
58
|
end
|
56
59
|
|
57
60
|
def decode(encoded_string)
|
58
61
|
return encoded_string if encoded_string.nil? || (encoded_string == '')
|
62
|
+
|
59
63
|
decoded_string = ::Base64.decode64(encoded_string)
|
60
64
|
decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
61
65
|
end
|
@@ -64,12 +68,14 @@ module SymmetricEncryption
|
|
64
68
|
class Base16
|
65
69
|
def encode(binary_string)
|
66
70
|
return binary_string if binary_string.nil? || (binary_string == '')
|
71
|
+
|
67
72
|
encoded_string = binary_string.to_s.unpack('H*').first
|
68
73
|
encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
69
74
|
end
|
70
75
|
|
71
76
|
def decode(encoded_string)
|
72
77
|
return encoded_string if encoded_string.nil? || (encoded_string == '')
|
78
|
+
|
73
79
|
decoded_string = [encoded_string].pack('H*')
|
74
80
|
decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
75
81
|
end
|
@@ -12,7 +12,7 @@ module SymmetricEncryption
|
|
12
12
|
raise(ArgumentError, "Invalid type: #{type.inspect}. Valid types: #{SymmetricEncryption::COERCION_TYPES.inspect}") unless SymmetricEncryption::COERCION_TYPES.include?(type)
|
13
13
|
|
14
14
|
if model.const_defined?(:EncryptedAttributes, _search_ancestors = false)
|
15
|
-
mod
|
15
|
+
mod = model.const_get(:EncryptedAttributes)
|
16
16
|
else
|
17
17
|
mod = model.const_set(:EncryptedAttributes, Module.new)
|
18
18
|
model.send(:include, mod)
|
@@ -38,6 +38,7 @@ module SymmetricEncryption
|
|
38
38
|
# Note: The encoding of the supplied buffer is forced to binary if not already binary
|
39
39
|
def self.present?(buffer)
|
40
40
|
return false if buffer.nil? || (buffer == '')
|
41
|
+
|
41
42
|
buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
42
43
|
buffer.start_with?(MAGIC_HEADER)
|
43
44
|
end
|
@@ -112,6 +113,7 @@ module SymmetricEncryption
|
|
112
113
|
def parse!(buffer)
|
113
114
|
offset = parse(buffer)
|
114
115
|
return if offset.zero?
|
116
|
+
|
115
117
|
buffer.slice!(0..offset - 1)
|
116
118
|
buffer
|
117
119
|
end
|
@@ -151,7 +153,7 @@ module SymmetricEncryption
|
|
151
153
|
|
152
154
|
# Remove header and extract flags
|
153
155
|
self.version = buffer.getbyte(offset)
|
154
|
-
offset
|
156
|
+
offset += 1
|
155
157
|
|
156
158
|
unless cipher
|
157
159
|
raise(
|
@@ -160,7 +162,7 @@ module SymmetricEncryption
|
|
160
162
|
)
|
161
163
|
end
|
162
164
|
|
163
|
-
flags
|
165
|
+
flags = buffer.getbyte(offset)
|
164
166
|
offset += 1
|
165
167
|
|
166
168
|
self.compress = (flags & FLAG_COMPRESSED) != 0
|
@@ -195,7 +197,7 @@ module SymmetricEncryption
|
|
195
197
|
|
196
198
|
# Returns [String] this header as a string
|
197
199
|
def to_s
|
198
|
-
flags
|
200
|
+
flags = 0
|
199
201
|
flags |= FLAG_COMPRESSED if compressed?
|
200
202
|
flags |= FLAG_IV if iv
|
201
203
|
flags |= FLAG_KEY if key
|
@@ -256,9 +258,9 @@ module SymmetricEncryption
|
|
256
258
|
# Exception when
|
257
259
|
# - offset exceeds length of buffer
|
258
260
|
# byteslice truncates when too long, but returns nil when start is beyond end of buffer
|
259
|
-
len
|
261
|
+
len = buffer.byteslice(offset, 2).unpack('v').first
|
260
262
|
offset += 2
|
261
|
-
out
|
263
|
+
out = buffer.byteslice(offset, len)
|
262
264
|
[out, offset + len]
|
263
265
|
end
|
264
266
|
end
|
@@ -3,68 +3,6 @@ module SymmetricEncryption
|
|
3
3
|
class Key
|
4
4
|
attr_reader :key, :iv, :cipher_name
|
5
5
|
|
6
|
-
# Returns [Key] from cipher data usually extracted from the configuration file.
|
7
|
-
#
|
8
|
-
# Supports N level deep key encrypting keys.
|
9
|
-
#
|
10
|
-
# Configuration keys:
|
11
|
-
# * key
|
12
|
-
# * encrypted_key
|
13
|
-
# * key_filename
|
14
|
-
def self.from_config(key: nil, key_filename: nil, encrypted_key: nil, key_env_var: nil,
|
15
|
-
iv:, key_encrypting_key: nil, cipher_name: 'aes-256-cbc')
|
16
|
-
|
17
|
-
if key_encrypting_key.is_a?(Hash)
|
18
|
-
# Recurse up the chain returning the parent key_encrypting_key
|
19
|
-
key_encrypting_key = from_config(cipher_name: cipher_name, **key_encrypting_key)
|
20
|
-
end
|
21
|
-
|
22
|
-
key ||=
|
23
|
-
if encrypted_key
|
24
|
-
raise(ArgumentError, 'Missing mandatory :key_encrypting_key when config includes :encrypted_key') unless key_encrypting_key
|
25
|
-
Keystore::Memory.new(encrypted_key: encrypted_key, key_encrypting_key: key_encrypting_key).read
|
26
|
-
elsif key_filename
|
27
|
-
Keystore::File.new(file_name: key_filename, key_encrypting_key: key_encrypting_key).read
|
28
|
-
elsif key_env_var
|
29
|
-
raise(ArgumentError, 'Missing mandatory :key_encrypting_key when config includes :key_env_var') unless key_encrypting_key
|
30
|
-
Keystore::Environment.new(key_env_var: key_env_var, key_encrypting_key: key_encrypting_key).read
|
31
|
-
end
|
32
|
-
|
33
|
-
new(key: key, iv: iv, cipher_name: cipher_name)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Migrate a prior config.
|
37
|
-
#
|
38
|
-
# Note:
|
39
|
-
# * The config cannot be saved back to the config file once
|
40
|
-
# migrated, without generating new Key Encrypting Keys.
|
41
|
-
# * Only run this migration in the target environment so that the
|
42
|
-
# current key encrypting files are present.
|
43
|
-
def self.migrate_config!(config)
|
44
|
-
# Backward compatibility - Deprecated
|
45
|
-
private_rsa_key = config.delete(:private_rsa_key)
|
46
|
-
|
47
|
-
# Migrate old encrypted_iv
|
48
|
-
if (encrypted_iv = config.delete(:encrypted_iv)) && private_rsa_key
|
49
|
-
encrypted_iv = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
|
50
|
-
config[:iv] = ::Base64.decode64(encrypted_iv)
|
51
|
-
end
|
52
|
-
|
53
|
-
# Migrate old iv_filename
|
54
|
-
if (file_name = config.delete(:iv_filename)) && private_rsa_key
|
55
|
-
encrypted_iv = File.read(file_name)
|
56
|
-
config[:iv] = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
|
57
|
-
end
|
58
|
-
|
59
|
-
# Backward compatibility - Deprecated
|
60
|
-
config[:key_encrypting_key] = RSAKey.new(private_rsa_key) if private_rsa_key
|
61
|
-
|
62
|
-
# Migrate old encrypted_key to new binary format
|
63
|
-
if (encrypted_key = config[:encrypted_key]) && private_rsa_key
|
64
|
-
config[:encrypted_key] = ::Base64.decode64(encrypted_key)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
6
|
def initialize(key: :random, iv: :random, cipher_name: 'aes-256-cbc')
|
69
7
|
@key = key == :random ? ::OpenSSL::Cipher.new(cipher_name).random_key : key
|
70
8
|
@iv = iv == :random ? ::OpenSSL::Cipher.new(cipher_name).random_iv : iv
|
@@ -73,6 +11,7 @@ module SymmetricEncryption
|
|
73
11
|
|
74
12
|
def encrypt(string)
|
75
13
|
return if string.nil?
|
14
|
+
|
76
15
|
string = string.to_s
|
77
16
|
return string if string.empty?
|
78
17
|
|
@@ -88,6 +27,7 @@ module SymmetricEncryption
|
|
88
27
|
|
89
28
|
def decrypt(encrypted_string)
|
90
29
|
return if encrypted_string.nil?
|
30
|
+
|
91
31
|
encrypted_string = encrypted_string.to_s
|
92
32
|
encrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
93
33
|
return encrypted_string if encrypted_string.empty?
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'aws-sdk-kms'
|
3
|
+
module SymmetricEncryption
|
4
|
+
module Keystore
|
5
|
+
# Support AWS Key Management Service (KMS)
|
6
|
+
#
|
7
|
+
# Terms:
|
8
|
+
# Aws
|
9
|
+
# Amazon Web Services.
|
10
|
+
#
|
11
|
+
# CMK
|
12
|
+
# Customer Master Key.
|
13
|
+
# Master key to encrypt and decrypt data, specifically the DEK in this case.
|
14
|
+
# Stored in AWS, cannot be exported.
|
15
|
+
#
|
16
|
+
# DEK
|
17
|
+
# Data Encryption Key.
|
18
|
+
# Key used to encrypt local data.
|
19
|
+
# Encrypted with the CMK and stored locally.
|
20
|
+
#
|
21
|
+
# KMS
|
22
|
+
# Key Management Service.
|
23
|
+
# For generating and storing the CMK.
|
24
|
+
# Used to encrypt and decrypt the DEK.
|
25
|
+
#
|
26
|
+
# Recommended reading:
|
27
|
+
#
|
28
|
+
# Concepts:
|
29
|
+
# https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html
|
30
|
+
#
|
31
|
+
# Overview:
|
32
|
+
# https://docs.aws.amazon.com/kms/latest/developerguide/overview.html
|
33
|
+
#
|
34
|
+
# Process:
|
35
|
+
# 1. Create a customer master key (CMK) along with an alias for use by Symmetric Encryption.
|
36
|
+
# - Note: CMK is region specific.
|
37
|
+
# - Stored exclusively in AWS KMS, cannot be exported.
|
38
|
+
#
|
39
|
+
# 2. Generate and encrypt a data encryption key (DEK).
|
40
|
+
# - CMK is used to encrypt the DEK.
|
41
|
+
# - Encrypted DEK is stored locally.
|
42
|
+
# - Encrypted DEK is region specific.
|
43
|
+
# - DEK can be shared, but then must be re-encrypted in each region.
|
44
|
+
# - Shared DEK across regions for database replication.
|
45
|
+
# - List of regions to publish DEK to during generation / key-rotation.
|
46
|
+
# - DEK must be encrypted with CMK in each region consecutively.
|
47
|
+
#
|
48
|
+
# Warning:
|
49
|
+
# If access to the AWS KMS is ever lost, then it is not possible to decrypt any encrypted data.
|
50
|
+
# Examples:
|
51
|
+
# - Loss of access to AWS accounts.
|
52
|
+
# - Loss of region(s) in which master keys are stored.
|
53
|
+
class Aws
|
54
|
+
attr_reader :region, :key_files, :master_key_alias
|
55
|
+
|
56
|
+
# Returns [Hash] a new keystore configuration after generating the data key.
|
57
|
+
#
|
58
|
+
# Increments the supplied version number by 1.
|
59
|
+
#
|
60
|
+
# Sample Hash layout returned:
|
61
|
+
# {
|
62
|
+
# cipher_name: aes-256-cbc,
|
63
|
+
# version: 8,
|
64
|
+
# keystore: :aws,
|
65
|
+
# master_key_alias: 'alias/symmetric-encryption/application/production',
|
66
|
+
# key_files: [
|
67
|
+
# {region: blah1, file_name: "~/symmetric-encryption/application_production_blah1_v6.encrypted_key"},
|
68
|
+
# {region: blah2, file_name: "~/symmetric-encryption/application_production_blah2_v6.encrypted_key"},
|
69
|
+
# ],
|
70
|
+
# iv: 'T80pYzD0E6e/bJCdjZ6TiQ=='
|
71
|
+
# }
|
72
|
+
def self.generate_data_key(version: 0,
|
73
|
+
regions: Utils::Aws::AWS_US_REGIONS,
|
74
|
+
dek: nil,
|
75
|
+
cipher_name:,
|
76
|
+
app_name:,
|
77
|
+
environment:,
|
78
|
+
key_path:)
|
79
|
+
|
80
|
+
# TODO: Also support generating environment variables instead of files.
|
81
|
+
|
82
|
+
version >= 255 ? (version = 1) : (version += 1)
|
83
|
+
regions = Array(regions).dup
|
84
|
+
|
85
|
+
master_key_alias = master_key_alias(app_name, environment)
|
86
|
+
|
87
|
+
# File per region for holding the encrypted data key
|
88
|
+
key_files = regions.collect do |region|
|
89
|
+
file_name = "#{app_name}_#{environment}_#{region}_v#{version}.encrypted_key"
|
90
|
+
{region: region, file_name: ::File.join(key_path, file_name)}
|
91
|
+
end
|
92
|
+
|
93
|
+
keystore = new(key_files: key_files, master_key_alias: master_key_alias)
|
94
|
+
unless dek
|
95
|
+
data_key = keystore.aws(regions.first).generate_data_key(cipher_name)
|
96
|
+
dek = Key.new(key: data_key, cipher_name: cipher_name)
|
97
|
+
end
|
98
|
+
keystore.write(dek.key)
|
99
|
+
|
100
|
+
{
|
101
|
+
keystore: :aws,
|
102
|
+
cipher_name: dek.cipher_name,
|
103
|
+
version: version,
|
104
|
+
master_key_alias: master_key_alias,
|
105
|
+
key_files: key_files,
|
106
|
+
iv: dek.iv
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Alias pointing to the active version of the master key for that region.
|
111
|
+
def self.master_key_alias(app_name, environment)
|
112
|
+
@master_key_alias ||= "alias/symmetric-encryption/#{app_name}/#{environment}"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Stores the Encryption key in a file.
|
116
|
+
# Secures the Encryption key by encrypting it with a key encryption key.
|
117
|
+
def initialize(region: nil, key_files:, master_key_alias:, key_encrypting_key: nil)
|
118
|
+
@key_files = key_files
|
119
|
+
@master_key_alias = master_key_alias
|
120
|
+
@region = region || ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'] || ::Aws.config[:region]
|
121
|
+
if key_encrypting_key
|
122
|
+
raise(SymmetricEncryption::ConfigError, 'AWS KMS keystore encrypts the key itself, so does not support supplying a key_encrypting_key')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Reads the data key environment variable, if present, otherwise a file.
|
127
|
+
# Decrypts the key using the master key for this region.
|
128
|
+
def read
|
129
|
+
key_file = key_files.find { |i| i[:region] == region }
|
130
|
+
raise(SymmetricEncryption::ConfigError, "region: #{region} not available in the supplied key_files") unless key_file
|
131
|
+
|
132
|
+
file_name = key_file[:file_name]
|
133
|
+
raise(SymmetricEncryption::ConfigError, 'file_name is mandatory for each key_file entry') unless file_name
|
134
|
+
|
135
|
+
raise(SymmetricEncryption::ConfigError, "File #{file_name} could not be found") unless ::File.exist?(file_name)
|
136
|
+
|
137
|
+
# TODO: Validate that file is not globally readable.
|
138
|
+
encoded_dek = ::File.open(file_name, 'rb', &:read)
|
139
|
+
encrypted_data_key = Base64.strict_decode64(encoded_dek)
|
140
|
+
aws(region).decrypt(encrypted_data_key)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Encrypt and write the data key to the file for each region.
|
144
|
+
def write(data_key)
|
145
|
+
key_files.each do |key_file|
|
146
|
+
region = key_file[:region]
|
147
|
+
file_name = key_file[:file_name]
|
148
|
+
|
149
|
+
raise(ArgumentError, 'region and file_name are mandatory for each key_file entry') unless region && file_name
|
150
|
+
|
151
|
+
encrypted_data_key = aws(region).encrypt(data_key)
|
152
|
+
encoded_dek = Base64.strict_encode64(encrypted_data_key)
|
153
|
+
write_to_file(file_name, encoded_dek)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def aws(region)
|
158
|
+
Utils::Aws.new(region: region, master_key_alias: master_key_alias)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
# Write to the supplied file_name, backing up the existing file if present
|
164
|
+
def write_to_file(file_name, data)
|
165
|
+
path = ::File.dirname(file_name)
|
166
|
+
::FileUtils.mkdir_p(path) unless ::File.directory?(path)
|
167
|
+
::File.rename(file_name, "#{file_name}.#{Time.now.to_i}") if ::File.exist?(file_name)
|
168
|
+
::File.open(file_name, 'wb') { |file| file.write(data) }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|