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 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