symmetric-encryption 4.0.1 → 4.1.0.beta1
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.rb +1 -1
- data/lib/symmetric_encryption/cipher.rb +2 -2
- data/lib/symmetric_encryption/cli.rb +27 -23
- data/lib/symmetric_encryption/key.rb +0 -62
- data/lib/symmetric_encryption/keystore.rb +143 -27
- data/lib/symmetric_encryption/keystore/aws.rb +172 -0
- data/lib/symmetric_encryption/keystore/environment.rb +5 -29
- data/lib/symmetric_encryption/keystore/file.rb +7 -29
- data/lib/symmetric_encryption/keystore/heroku.rb +22 -0
- data/lib/symmetric_encryption/keystore/memory.rb +3 -2
- data/lib/symmetric_encryption/railtie.rb +2 -2
- data/lib/symmetric_encryption/utils/aws.rb +139 -0
- data/lib/symmetric_encryption/utils/re_encrypt_files.rb +10 -3
- data/lib/symmetric_encryption/version.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 +199 -3
- data/test/test_db.sqlite3 +0 -0
- data/test/test_helper.rb +1 -0
- data/test/utils/aws_test.rb +75 -0
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 674089b02b1620226cd6282347185623f2e94584d31759a42200fed1288f4bc2
|
4
|
+
data.tar.gz: 35d96710285ed9190f5d75e36471489f137ae19ba64bc3bcebf3224020d75b30
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57a4050574792eaeca82e4c0174ed4676491e30c09186380f4f72c5d39c4fe6cd430ba025ae1b9ff6c4cfec7101b181787f6f295bb1b8c44269dddd0145cfb26
|
7
|
+
data.tar.gz: 9837704656992c51e9771e962331e04f0be6b0b2ba3b577f1196cb091cd426a800147ebbe424b9e6f9ebabaf012b635f0f480438112436b7f1eba55c04603f58
|
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
|
data/lib/symmetric_encryption.rb
CHANGED
@@ -22,7 +22,7 @@ module SymmetricEncryption
|
|
22
22
|
autoload :CLI, 'symmetric_encryption/cli'
|
23
23
|
autoload :Keystore, 'symmetric_encryption/keystore'
|
24
24
|
module Utils
|
25
|
-
autoload :
|
25
|
+
autoload :Aws, 'symmetric_encryption/utils/aws'
|
26
26
|
autoload :ReEncryptFiles, 'symmetric_encryption/utils/re_encrypt_files'
|
27
27
|
end
|
28
28
|
end
|
@@ -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,
|
@@ -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
|
@@ -127,11 +127,15 @@ module SymmetricEncryption
|
|
127
127
|
@generate = config
|
128
128
|
end
|
129
129
|
|
130
|
-
opts.on '-s', '--keystore heroku|environment|file', '
|
130
|
+
opts.on '-s', '--keystore heroku|environment|file|aws', 'Which keystore to use during generation or re-encryption.' do |keystore|
|
131
131
|
@keystore = (keystore || 'file').downcase.to_sym
|
132
132
|
end
|
133
133
|
|
134
|
-
opts.on '-
|
134
|
+
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|
|
135
|
+
@regions = regions.to_s.split(',').collect(&:strip) if regions
|
136
|
+
end
|
137
|
+
|
138
|
+
opts.on '-K', '--key-path KEY_PATH', 'Output path in which to write generated key files. Default: ~/.symmetric-encryption' do |path|
|
135
139
|
@key_path = path
|
136
140
|
end
|
137
141
|
|
@@ -197,26 +201,21 @@ module SymmetricEncryption
|
|
197
201
|
end
|
198
202
|
|
199
203
|
def generate_new_config
|
204
|
+
unless KEYSTORES.include?(keystore)
|
205
|
+
puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
|
206
|
+
exit(-3)
|
207
|
+
end
|
208
|
+
|
200
209
|
config_file_does_not_exist!
|
201
210
|
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
|
211
|
+
args = {
|
212
|
+
app_name: app_name,
|
213
|
+
environments: environments,
|
214
|
+
cipher_name: cipher_name
|
215
|
+
}
|
216
|
+
args[:key_path] = key_path if key_path
|
217
|
+
args[:regions] = regions if regions && !regions.empty?
|
218
|
+
cfg = Keystore.generate_data_keys(keystore, **args)
|
220
219
|
Config.write_file(config_file_path, cfg)
|
221
220
|
puts "New configuration file created at: #{config_file_path}"
|
222
221
|
end
|
@@ -228,8 +227,13 @@ module SymmetricEncryption
|
|
228
227
|
end
|
229
228
|
|
230
229
|
def run_rotate_keys
|
230
|
+
if keystore && KEYSTORES.include?(keystore)
|
231
|
+
puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
|
232
|
+
exit(-3)
|
233
|
+
end
|
234
|
+
|
231
235
|
config = Config.read_file(config_file_path)
|
232
|
-
SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name, rolling_deploy: rolling_deploy)
|
236
|
+
SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name, rolling_deploy: rolling_deploy, keystore: keystore)
|
233
237
|
Config.write_file(config_file_path, config)
|
234
238
|
puts "Existing configuration file updated with new keys: #{config_file_path}"
|
235
239
|
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
|
@@ -2,11 +2,33 @@ module SymmetricEncryption
|
|
2
2
|
# Encryption keys are secured in Keystores
|
3
3
|
module Keystore
|
4
4
|
# @formatter:off
|
5
|
+
autoload :Aws, 'symmetric_encryption/keystore/aws'
|
5
6
|
autoload :Environment, 'symmetric_encryption/keystore/environment'
|
6
7
|
autoload :File, 'symmetric_encryption/keystore/file'
|
8
|
+
autoload :Heroku, 'symmetric_encryption/keystore/heroku'
|
7
9
|
autoload :Memory, 'symmetric_encryption/keystore/memory'
|
8
10
|
# @formatter:on
|
9
11
|
|
12
|
+
# Returns [Hash] a new keystore configuration after generating data keys for each environment.
|
13
|
+
def self.generate_data_keys(keystore:, environments: %i[development test release production], **args)
|
14
|
+
keystore_class = keystore.is_a?(Symbol) || keystore.is_a?(String) ? constantize_symbol(keystore) : keystore
|
15
|
+
|
16
|
+
configs = {}
|
17
|
+
environments.each do |environment|
|
18
|
+
environment = environment.to_sym
|
19
|
+
configs[environment] =
|
20
|
+
if %i[development test].include?(environment)
|
21
|
+
dev_config
|
22
|
+
else
|
23
|
+
cfg = keystore_class.generate_data_key(environment: environment, **args)
|
24
|
+
{
|
25
|
+
ciphers: [cfg]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
configs
|
30
|
+
end
|
31
|
+
|
10
32
|
# Returns [Hash] a new configuration file after performing key rotation.
|
11
33
|
#
|
12
34
|
# Perform key rotation for each of the environments in the configuration file, by
|
@@ -27,10 +49,13 @@ module SymmetricEncryption
|
|
27
49
|
# by the servers that have not been updated yet.
|
28
50
|
# Default: false
|
29
51
|
#
|
52
|
+
# keystore: [Symbol]
|
53
|
+
# If supplied, changes the keystore during key rotation.
|
54
|
+
#
|
30
55
|
# Notes:
|
31
56
|
# * iv_filename is no longer supported and is removed when creating a new random cipher.
|
32
57
|
# * `iv` does not need to be encrypted and is included in the clear.
|
33
|
-
def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false)
|
58
|
+
def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false, keystore: nil)
|
34
59
|
full_config.each_pair do |environment, cfg|
|
35
60
|
# Only rotate keys for specified environments. Default, all
|
36
61
|
next if !environments.empty? && !environments.include?(environment.to_sym)
|
@@ -43,22 +68,24 @@ module SymmetricEncryption
|
|
43
68
|
# Only generate new keys for keystore's that have a key encrypting key
|
44
69
|
next unless config[:key_encrypting_key] || config[:private_rsa_key]
|
45
70
|
|
46
|
-
cipher_name
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
71
|
+
cipher_name = config[:cipher_name] || 'aes-256-cbc'
|
72
|
+
|
73
|
+
keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(config)
|
74
|
+
|
75
|
+
args = {
|
76
|
+
cipher_name: cipher_name,
|
77
|
+
app_name: app_name,
|
78
|
+
version: version,
|
79
|
+
environment: environment
|
80
|
+
}
|
81
|
+
args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename)
|
82
|
+
new_data_key = keystore_class.generate_data_key(args)
|
56
83
|
|
57
84
|
# Add as second key so that key can be published now and only used in a later deploy.
|
58
85
|
if rolling_deploy
|
59
|
-
cfg[:ciphers].insert(1,
|
86
|
+
cfg[:ciphers].insert(1, new_data_key)
|
60
87
|
else
|
61
|
-
cfg[:ciphers].unshift(
|
88
|
+
cfg[:ciphers].unshift(new_data_key)
|
62
89
|
end
|
63
90
|
end
|
64
91
|
full_config
|
@@ -83,27 +110,29 @@ module SymmetricEncryption
|
|
83
110
|
always_add_header = config.delete(:always_add_header)
|
84
111
|
encoding = config.delete(:encoding)
|
85
112
|
|
86
|
-
|
113
|
+
migrate_config!(config)
|
87
114
|
|
88
115
|
# The current data encrypting key without any of the key encrypting keys.
|
89
|
-
key =
|
116
|
+
key = Keystore.read_key(config)
|
90
117
|
cipher_name = key.cipher_name
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
118
|
+
keystore_class = keystore_for(config)
|
119
|
+
|
120
|
+
args = {
|
121
|
+
cipher_name: cipher_name,
|
122
|
+
app_name: app_name,
|
123
|
+
version: version,
|
124
|
+
environment: environment,
|
125
|
+
dek: key
|
126
|
+
}
|
127
|
+
args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename)
|
100
128
|
|
101
|
-
|
102
|
-
|
129
|
+
new_config = keystore_class.generate_data_key(args)
|
130
|
+
new_config[:always_add_header] = always_add_header
|
131
|
+
new_config[:encoding] = encoding
|
103
132
|
|
104
133
|
# Replace existing config entry
|
105
134
|
cfg[:ciphers].shift
|
106
|
-
cfg[:ciphers].unshift(
|
135
|
+
cfg[:ciphers].unshift(new_config)
|
107
136
|
end
|
108
137
|
full_config
|
109
138
|
end
|
@@ -122,5 +151,92 @@ module SymmetricEncryption
|
|
122
151
|
]
|
123
152
|
}
|
124
153
|
end
|
154
|
+
|
155
|
+
# Returns [Key] by recursively navigating the config tree.
|
156
|
+
#
|
157
|
+
# Supports N level deep key encrypting keys.
|
158
|
+
def self.read_key(key: nil, iv:, key_encrypting_key: nil, cipher_name: 'aes-256-cbc', keystore: nil, version: 0, **args)
|
159
|
+
if key_encrypting_key.is_a?(Hash)
|
160
|
+
# Recurse up the chain returning the parent key_encrypting_key
|
161
|
+
key_encrypting_key = read_key(cipher_name: cipher_name, **key_encrypting_key)
|
162
|
+
end
|
163
|
+
|
164
|
+
unless key
|
165
|
+
keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(args)
|
166
|
+
store = keystore_class.new(key_encrypting_key: key_encrypting_key, **args)
|
167
|
+
key = store.read
|
168
|
+
end
|
169
|
+
|
170
|
+
Key.new(key: key, iv: iv, cipher_name: cipher_name)
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Internal use only methods
|
175
|
+
#
|
176
|
+
|
177
|
+
def self.keystore_for(config)
|
178
|
+
if config[:keystore]
|
179
|
+
constantize_symbol(config[:keystore])
|
180
|
+
elsif config[:encrypted_key]
|
181
|
+
Keystore::Memory
|
182
|
+
elsif config[:key_filename]
|
183
|
+
Keystore::File
|
184
|
+
elsif config[:key_env_var]
|
185
|
+
Keystore::Environment
|
186
|
+
else
|
187
|
+
raise(ArgumentError, 'Unknown keystore supplied in config')
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.constantize_symbol(symbol, namespace = 'SymmetricEncryption::Keystore')
|
192
|
+
klass = "#{namespace}::#{camelize(symbol.to_s)}"
|
193
|
+
begin
|
194
|
+
Object.const_get(klass)
|
195
|
+
rescue NameError
|
196
|
+
raise(ArgumentError, "Keystore: #{symbol.inspect} not found. Looking for: #{klass}")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Borrow from Rails, when not running Rails
|
201
|
+
def self.camelize(term)
|
202
|
+
string = term.to_s
|
203
|
+
string = string.sub(/^[a-z\d]*/, &:capitalize)
|
204
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
|
205
|
+
string.gsub!('/'.freeze, '::'.freeze)
|
206
|
+
string
|
207
|
+
end
|
208
|
+
|
209
|
+
# Migrate a prior config.
|
210
|
+
#
|
211
|
+
# Note:
|
212
|
+
# * The config cannot be saved back to the config file once
|
213
|
+
# migrated, without generating new Key Encrypting Keys.
|
214
|
+
# * Only run this migration in the target environment so that the
|
215
|
+
# current key encrypting files are present.
|
216
|
+
def self.migrate_config!(config)
|
217
|
+
# Backward compatibility - Deprecated
|
218
|
+
private_rsa_key = config.delete(:private_rsa_key)
|
219
|
+
|
220
|
+
# Migrate old encrypted_iv
|
221
|
+
if (encrypted_iv = config.delete(:encrypted_iv)) && private_rsa_key
|
222
|
+
encrypted_iv = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
|
223
|
+
config[:iv] = ::Base64.decode64(encrypted_iv)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Migrate old iv_filename
|
227
|
+
if (file_name = config.delete(:iv_filename)) && private_rsa_key
|
228
|
+
encrypted_iv = ::File.read(file_name)
|
229
|
+
config[:iv] = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Backward compatibility - Deprecated
|
233
|
+
config[:key_encrypting_key] = RSAKey.new(private_rsa_key) if private_rsa_key
|
234
|
+
|
235
|
+
# Migrate old encrypted_key to new binary format
|
236
|
+
if (encrypted_key = config[:encrypted_key]) && private_rsa_key
|
237
|
+
config[:encrypted_key] = ::Base64.decode64(encrypted_key)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
125
241
|
end
|
126
242
|
end
|
@@ -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
|