symmetric-encryption 4.0.1 → 4.1.0.beta1
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.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
|