symmetric-encryption 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|