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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8b4f45cc7b6dca91b1eb5d8eb5df044485d0a484f93472ce38fee62559453e8
4
- data.tar.gz: 973376b8363032b2a71aaf840a3012cf7485d7f6b16f2ea1ebf20f622eaf56f0
3
+ metadata.gz: 674089b02b1620226cd6282347185623f2e94584d31759a42200fed1288f4bc2
4
+ data.tar.gz: 35d96710285ed9190f5d75e36471489f137ae19ba64bc3bcebf3224020d75b30
5
5
  SHA512:
6
- metadata.gz: ae3695e636ea98bcbfe489187e26244dee6116257afdf4383a234359c201974024d3180d1ea1851edbc1798343ce1ab862fea20691a01f8eb7993b58a7206921
7
- data.tar.gz: cbe308f3287c77c32996551b8f4ace32fd803e123e32f906ed21d65bf6d3823b19ce5459623a445a4ffd4bb1b33a0377558ad6604e98d61ae17b206d4cef1892
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
@@ -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 :Generate, 'symmetric_encryption/utils/generate'
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
- Key.migrate_config!(config)
22
- key = Key.from_config(cipher_name: cipher_name, **config)
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 = '/etc/symmetric-encryption'
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', 'Generate a new configuration file and encryption keys for every environment.' do |keystore|
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 '-K', '--key-path KEY_PATH', 'Output path in which to write generated key files. Default: /etc/symmetric-encryption' do |path|
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
- cfg =
203
- if keystore == :file
204
- SymmetricEncryption::Keystore::File.new_config(
205
- key_path: key_path,
206
- app_name: app_name,
207
- environments: environments,
208
- cipher_name: cipher_name
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 = config[:cipher_name] || 'aes-256-cbc'
47
- new_key_config =
48
- if config.key?(:key_filename)
49
- key_path = ::File.dirname(config[:key_filename])
50
- Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
51
- elsif config.key?(:key_env_var)
52
- Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
53
- elsif config.key?(:encrypted_key)
54
- Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
55
- end
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, new_key_config)
86
+ cfg[:ciphers].insert(1, new_data_key)
60
87
  else
61
- cfg[:ciphers].unshift(new_key_config)
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
- Key.migrate_config!(config)
113
+ migrate_config!(config)
87
114
 
88
115
  # The current data encrypting key without any of the key encrypting keys.
89
- key = Key.from_config(config)
116
+ key = Keystore.read_key(config)
90
117
  cipher_name = key.cipher_name
91
- new_key_config =
92
- if config.key?(:key_filename)
93
- key_path = ::File.dirname(config[:key_filename])
94
- Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
95
- elsif config.key?(:key_env_var)
96
- Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
97
- elsif config.key?(:encrypted_key)
98
- Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
99
- end
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
- new_key_config[:always_add_header] = always_add_header
102
- new_key_config[:encoding] = encoding
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(new_key_config)
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