symmetric-encryption 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -7
  3. data/lib/symmetric_encryption/cipher.rb +11 -4
  4. data/lib/symmetric_encryption/cli.rb +39 -28
  5. data/lib/symmetric_encryption/config.rb +9 -6
  6. data/lib/symmetric_encryption/encoder.rb +6 -0
  7. data/lib/symmetric_encryption/extensions/mongoid/encrypted.rb +1 -0
  8. data/lib/symmetric_encryption/generator.rb +1 -1
  9. data/lib/symmetric_encryption/header.rb +7 -5
  10. data/lib/symmetric_encryption/key.rb +2 -62
  11. data/lib/symmetric_encryption/keystore/aws.rb +172 -0
  12. data/lib/symmetric_encryption/keystore/environment.rb +7 -30
  13. data/lib/symmetric_encryption/keystore/file.rb +8 -30
  14. data/lib/symmetric_encryption/keystore/heroku.rb +22 -0
  15. data/lib/symmetric_encryption/keystore/memory.rb +4 -3
  16. data/lib/symmetric_encryption/keystore.rb +151 -36
  17. data/lib/symmetric_encryption/railtie.rb +9 -4
  18. data/lib/symmetric_encryption/railties/symmetric_encryption_validator.rb +1 -0
  19. data/lib/symmetric_encryption/reader.rb +50 -58
  20. data/lib/symmetric_encryption/symmetric_encryption.rb +2 -1
  21. data/lib/symmetric_encryption/utils/aws.rb +141 -0
  22. data/lib/symmetric_encryption/utils/re_encrypt_files.rb +12 -5
  23. data/lib/symmetric_encryption/version.rb +1 -1
  24. data/lib/symmetric_encryption/writer.rb +33 -27
  25. data/lib/symmetric_encryption.rb +27 -6
  26. data/test/active_record_test.rb +25 -25
  27. data/test/cipher_test.rb +3 -3
  28. data/test/header_test.rb +1 -1
  29. data/test/key_test.rb +0 -157
  30. data/test/keystore/aws_test.rb +133 -0
  31. data/test/keystore/environment_test.rb +3 -51
  32. data/test/keystore/file_test.rb +13 -52
  33. data/test/keystore/heroku_test.rb +70 -0
  34. data/test/keystore_test.rb +200 -4
  35. data/test/mongoid_test.rb +15 -15
  36. data/test/reader_test.rb +28 -8
  37. data/test/symmetric_encryption_test.rb +2 -2
  38. data/test/test_db.sqlite3 +0 -0
  39. data/test/test_helper.rb +1 -0
  40. data/test/utils/aws_test.rb +74 -0
  41. data/test/writer_test.rb +48 -46
  42. metadata +29 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8b4f45cc7b6dca91b1eb5d8eb5df044485d0a484f93472ce38fee62559453e8
4
- data.tar.gz: 973376b8363032b2a71aaf840a3012cf7485d7f6b16f2ea1ebf20f622eaf56f0
3
+ metadata.gz: 825c28cf5b38d4cf22d26f4ed8196bbf1085ee0e09b372ab3c30aa055238902f
4
+ data.tar.gz: de736c34beb30c50e9316f0f85cade71b022caac4fe4ca94cc61c94d0c8fe1aa
5
5
  SHA512:
6
- metadata.gz: ae3695e636ea98bcbfe489187e26244dee6116257afdf4383a234359c201974024d3180d1ea1851edbc1798343ce1ab862fea20691a01f8eb7993b58a7206921
7
- data.tar.gz: cbe308f3287c77c32996551b8f4ace32fd803e123e32f906ed21d65bf6d3823b19ce5459623a445a4ffd4bb1b33a0377558ad6604e98d61ae17b206d4cef1892
6
+ metadata.gz: 711727ec509464e8807f798f82c4944c25f37a2cd48ec190c93f4f0134437887c8355fedf199157727ffd7faaa1023146fa95946ee6f2f9ec8a648426ec68f60
7
+ data.tar.gz: 834ae0b58bd0ca3011b846a94709203ac648a6cd341c5f86f18c82fa4b4c9b16abfc400af9d3c7c0f0348b4127db7c425de56345738bfde0de5dae97666a4031
data/README.md CHANGED
@@ -29,13 +29,6 @@ Checkout the sister project [Rocket Job](http://rocketjob.io): Ruby's missing ba
29
29
 
30
30
  Fully supports Symmetric Encryption to encrypt data in flight and at rest while running jobs in the background.
31
31
 
32
- ## Supports
33
-
34
- Symmetric Encryption works with the following Ruby VMs:
35
-
36
- - Ruby 2.1 and higher.
37
- - JRuby 9.1 and higher.
38
-
39
32
  ## Upgrading to SymmetricEncryption V4
40
33
 
41
34
  Version 4 of Symmetric Encryption has completely adopted the Ruby keyword arguments on most API's where
@@ -18,8 +18,8 @@ module SymmetricEncryption
18
18
  encoding: :base64strict,
19
19
  **config)
20
20
 
21
- 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,
@@ -133,8 +133,10 @@ module SymmetricEncryption
133
133
  # compression
134
134
  def encrypt(str, random_iv: false, compress: false, header: always_add_header)
135
135
  return if str.nil?
136
+
136
137
  str = str.to_s
137
138
  return str if str.empty?
139
+
138
140
  encrypted = binary_encrypt(str, random_iv: random_iv, compress: compress, header: header)
139
141
  encode(encrypted)
140
142
  end
@@ -161,6 +163,7 @@ module SymmetricEncryption
161
163
  return unless decoded
162
164
 
163
165
  return decoded if decoded.empty?
166
+
164
167
  decrypted = binary_decrypt(decoded)
165
168
 
166
169
  # Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary
@@ -178,6 +181,7 @@ module SymmetricEncryption
178
181
  # Returned string is UTF8 encoded except for encoding :none
179
182
  def encode(binary_string)
180
183
  return binary_string if binary_string.nil? || (binary_string == '')
184
+
181
185
  encoder.encode(binary_string)
182
186
  end
183
187
 
@@ -187,6 +191,7 @@ module SymmetricEncryption
187
191
  # Returned string is Binary encoded
188
192
  def decode(encoded_string)
189
193
  return encoded_string if encoded_string.nil? || (encoded_string == '')
194
+
190
195
  encoder.decode(encoded_string)
191
196
  end
192
197
 
@@ -243,6 +248,7 @@ module SymmetricEncryption
243
248
  # See #encrypt to encrypt and encode the result as a string.
244
249
  def binary_encrypt(str, random_iv: false, compress: false, header: always_add_header)
245
250
  return if str.nil?
251
+
246
252
  string = str.to_s
247
253
  return string if string.empty?
248
254
 
@@ -300,6 +306,7 @@ module SymmetricEncryption
300
306
  # is automatically set to the same UTF-8 or Binary encoding
301
307
  def binary_decrypt(encrypted_string, header: Header.new)
302
308
  return if encrypted_string.nil?
309
+
303
310
  str = encrypted_string.to_s
304
311
  str.force_encoding(SymmetricEncryption::BINARY_ENCODING)
305
312
  return str if str.empty?
@@ -309,8 +316,8 @@ module SymmetricEncryption
309
316
 
310
317
  openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || cipher_name)
311
318
  openssl_cipher.decrypt
312
- openssl_cipher.key = header.key || @key
313
- if (iv = header.iv || @iv)
319
+ openssl_cipher.key = header.key || @key
320
+ if (iv = header.iv || @iv)
314
321
  openssl_cipher.iv = iv
315
322
  end
316
323
  result = openssl_cipher.update(data)
@@ -6,7 +6,7 @@ module SymmetricEncryption
6
6
  :decrypt, :random_password, :new_keys, :generate, :environment,
7
7
  :keystore, :re_encrypt, :version, :output_file_name, :compress,
8
8
  :environments, :cipher_name, :rolling_deploy, :rotate_keys, :rotate_kek, :prompt, :show_version,
9
- :cleanup_keys, :activate_key, :migrate
9
+ :cleanup_keys, :activate_key, :migrate, :regions
10
10
 
11
11
  KEYSTORES = %i[heroku environment file].freeze
12
12
 
@@ -19,7 +19,7 @@ module SymmetricEncryption
19
19
  @environment = ENV['SYMMETRIC_ENCRYPTION_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
20
20
  @config_file_path = File.expand_path(ENV['SYMMETRIC_ENCRYPTION_CONFIG'] || 'config/symmetric-encryption.yml')
21
21
  @app_name = 'symmetric-encryption'
22
- @key_path = '/etc/symmetric-encryption'
22
+ @key_path = '~/.symmetric-encryption'
23
23
  @cipher_name = 'aes-256-cbc'
24
24
  @rolling_deploy = false
25
25
  @prompt = false
@@ -70,7 +70,7 @@ module SymmetricEncryption
70
70
  end
71
71
 
72
72
  def parser
73
- @parser ||= OptionParser.new do |opts|
73
+ @parser ||= OptionParser.new do |opts|
74
74
  opts.banner = <<~BANNER
75
75
  Symmetric Encryption v#{VERSION}
76
76
 
@@ -99,10 +99,14 @@ module SymmetricEncryption
99
99
  @prompt = true
100
100
  end
101
101
 
102
- opts.on '-z', '--compress', 'Compress encrypted output file.' do
102
+ opts.on '-z', '--compress', 'Compress encrypted output file. [Default for encrypting files]' do
103
103
  @compress = true
104
104
  end
105
105
 
106
+ opts.on '-Z', '--no-compress', 'Does not compress the output file. [Default for encrypting strings]' do
107
+ @compress = false
108
+ end
109
+
106
110
  opts.on '-E', '--env ENVIRONMENT', "Environment to use in the config file. Default: SYMMETRIC_ENCRYPTION_ENV || RACK_ENV || RAILS_ENV || 'development'" do |environment|
107
111
  @environment = environment
108
112
  end
@@ -127,11 +131,15 @@ module SymmetricEncryption
127
131
  @generate = config
128
132
  end
129
133
 
130
- opts.on '-s', '--keystore heroku|environment|file', 'Generate a new configuration file and encryption keys for every environment.' do |keystore|
134
+ opts.on '-s', '--keystore heroku|environment|file|aws', 'Which keystore to use during generation or re-encryption.' do |keystore|
131
135
  @keystore = (keystore || 'file').downcase.to_sym
132
136
  end
133
137
 
134
- opts.on '-K', '--key-path KEY_PATH', 'Output path in which to write generated key files. Default: /etc/symmetric-encryption' do |path|
138
+ opts.on '-B', '--regions [us-east-1,us-east-2,us-west-1,us-west-2]', 'AWS KMS Regions to encrypt data key with.' do |regions|
139
+ @regions = regions.to_s.split(',').collect(&:strip) if regions
140
+ end
141
+
142
+ opts.on '-K', '--key-path KEY_PATH', 'Output path in which to write generated key files. Default: ~/.symmetric-encryption' do |path|
135
143
  @key_path = path
136
144
  end
137
145
 
@@ -197,26 +205,21 @@ module SymmetricEncryption
197
205
  end
198
206
 
199
207
  def generate_new_config
208
+ unless KEYSTORES.include?(keystore)
209
+ puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
210
+ exit(-3)
211
+ end
212
+
200
213
  config_file_does_not_exist!
201
214
  self.environments ||= %i[development test release production]
202
- 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
215
+ args = {
216
+ app_name: app_name,
217
+ environments: environments,
218
+ cipher_name: cipher_name
219
+ }
220
+ args[:key_path] = key_path if key_path
221
+ args[:regions] = regions if regions && !regions.empty?
222
+ cfg = Keystore.generate_data_keys(keystore, **args)
220
223
  Config.write_file(config_file_path, cfg)
221
224
  puts "New configuration file created at: #{config_file_path}"
222
225
  end
@@ -228,8 +231,13 @@ module SymmetricEncryption
228
231
  end
229
232
 
230
233
  def run_rotate_keys
234
+ if keystore && KEYSTORES.include?(keystore)
235
+ puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
236
+ exit(-3)
237
+ end
238
+
231
239
  config = Config.read_file(config_file_path)
232
- SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name, rolling_deploy: rolling_deploy)
240
+ SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name, rolling_deploy: rolling_deploy, keystore: keystore)
233
241
  Config.write_file(config_file_path, config)
234
242
  puts "Existing configuration file updated with new keys: #{config_file_path}"
235
243
  end
@@ -246,7 +254,8 @@ module SymmetricEncryption
246
254
  config.each_pair do |env, cfg|
247
255
  next if environments && !environments.include?(env.to_sym)
248
256
  next unless ciphers = cfg[:ciphers]
249
- highest = ciphers.max_by { |i| i[:version] }
257
+
258
+ highest = ciphers.max_by { |i| i[:version] }
250
259
  ciphers.clear
251
260
  ciphers << highest
252
261
  end
@@ -260,7 +269,8 @@ module SymmetricEncryption
260
269
  config.each_pair do |env, cfg|
261
270
  next if environments && !environments.include?(env.to_sym)
262
271
  next unless ciphers = cfg[:ciphers]
263
- highest = ciphers.max_by { |i| i[:version] }
272
+
273
+ highest = ciphers.max_by { |i| i[:version] }
264
274
  ciphers.delete(highest)
265
275
  ciphers.unshift(highest)
266
276
  end
@@ -308,7 +318,7 @@ module SymmetricEncryption
308
318
 
309
319
  puts('Values do not match, please try again') if value1 != value2
310
320
  end
311
-
321
+ compress = false if compress.nil?
312
322
  encrypted = SymmetricEncryption.cipher(version).encrypt(value1, compress: compress)
313
323
  output_file_name ? File.open(output_file_name, 'wb') { |f| f << encrypted } : puts("\n\nEncrypted: #{encrypted}\n\n")
314
324
  end
@@ -330,6 +340,7 @@ module SymmetricEncryption
330
340
  # Ensure that the config file does not already exist before generating a new one.
331
341
  def config_file_does_not_exist!
332
342
  return unless File.exist?(config_file_path)
343
+
333
344
  puts "\nConfiguration file already exists, please move or rename: #{config_file_path}\n\n"
334
345
  exit(-1)
335
346
  end
@@ -53,8 +53,8 @@ module SymmetricEncryption
53
53
  env ||= defined?(Rails) ? Rails.env : ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
54
54
 
55
55
  unless file_name
56
- root = defined?(Rails) ? Rails.root : '.'
57
- file_name =
56
+ root = defined?(Rails) ? Rails.root : '.'
57
+ file_name =
58
58
  if (env_var = ENV['SYMMETRIC_ENCRYPTION_CONFIG'])
59
59
  File.expand_path(env_var)
60
60
  else
@@ -101,6 +101,7 @@ module SymmetricEncryption
101
101
  object
102
102
  end
103
103
  end
104
+
104
105
  private_class_method :deep_symbolize_keys
105
106
 
106
107
  # Iterate through the Hash symbolizing all keys.
@@ -119,28 +120,29 @@ module SymmetricEncryption
119
120
  object
120
121
  end
121
122
  end
123
+
122
124
  private_class_method :deep_stringify_keys
123
125
 
124
126
  # Migrate old configuration format for this environment
125
127
  def self.migrate_old_formats!(config)
126
128
  # Inline single cipher before :ciphers
127
129
  unless config.key?(:ciphers)
128
- inline_cipher = {}
130
+ inline_cipher = {}
129
131
  config.keys.each { |key| inline_cipher[key] = config.delete(key) }
130
- config[:ciphers] = [inline_cipher]
132
+ config[:ciphers] = [inline_cipher]
131
133
  end
132
134
 
133
135
  # Copy Old :private_rsa_key into each ciphers config
134
136
  # Cipher.from_config replaces it with the RSA Kek
135
137
  if config[:private_rsa_key]
136
- private_rsa_key = config.delete(:private_rsa_key)
138
+ private_rsa_key = config.delete(:private_rsa_key)
137
139
  config[:ciphers].each { |cipher| cipher[:private_rsa_key] = private_rsa_key }
138
140
  end
139
141
 
140
142
  # Old :cipher_name
141
143
  config[:ciphers].each do |cipher|
142
144
  if (old_key_name_cipher = cipher.delete(:cipher))
143
- cipher[:cipher_name] = old_key_name_cipher
145
+ cipher[:cipher_name] = old_key_name_cipher
144
146
  end
145
147
 
146
148
  # Only temporarily used during v4 Beta process
@@ -156,6 +158,7 @@ module SymmetricEncryption
156
158
  end
157
159
  config
158
160
  end
161
+
159
162
  private_class_method :migrate_old_formats!
160
163
  end
161
164
  end
@@ -36,12 +36,14 @@ module SymmetricEncryption
36
36
  class Base64
37
37
  def encode(binary_string)
38
38
  return binary_string if binary_string.nil? || (binary_string == '')
39
+
39
40
  encoded_string = ::Base64.encode64(binary_string)
40
41
  encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
41
42
  end
42
43
 
43
44
  def decode(encoded_string)
44
45
  return encoded_string if encoded_string.nil? || (encoded_string == '')
46
+
45
47
  decoded_string = ::Base64.decode64(encoded_string)
46
48
  decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
47
49
  end
@@ -50,12 +52,14 @@ module SymmetricEncryption
50
52
  class Base64Strict
51
53
  def encode(binary_string)
52
54
  return binary_string if binary_string.nil? || (binary_string == '')
55
+
53
56
  encoded_string = ::Base64.strict_encode64(binary_string)
54
57
  encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
55
58
  end
56
59
 
57
60
  def decode(encoded_string)
58
61
  return encoded_string if encoded_string.nil? || (encoded_string == '')
62
+
59
63
  decoded_string = ::Base64.decode64(encoded_string)
60
64
  decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
61
65
  end
@@ -64,12 +68,14 @@ module SymmetricEncryption
64
68
  class Base16
65
69
  def encode(binary_string)
66
70
  return binary_string if binary_string.nil? || (binary_string == '')
71
+
67
72
  encoded_string = binary_string.to_s.unpack('H*').first
68
73
  encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
69
74
  end
70
75
 
71
76
  def decode(encoded_string)
72
77
  return encoded_string if encoded_string.nil? || (encoded_string == '')
78
+
73
79
  decoded_string = [encoded_string].pack('H*')
74
80
  decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
75
81
  end
@@ -1,3 +1,4 @@
1
+ require 'mongoid'
1
2
  # Add :encrypted option for Mongoid models
2
3
  #
3
4
  # Example:
@@ -12,7 +12,7 @@ module SymmetricEncryption
12
12
  raise(ArgumentError, "Invalid type: #{type.inspect}. Valid types: #{SymmetricEncryption::COERCION_TYPES.inspect}") unless SymmetricEncryption::COERCION_TYPES.include?(type)
13
13
 
14
14
  if model.const_defined?(:EncryptedAttributes, _search_ancestors = false)
15
- mod = model.const_get(:EncryptedAttributes)
15
+ mod = model.const_get(:EncryptedAttributes)
16
16
  else
17
17
  mod = model.const_set(:EncryptedAttributes, Module.new)
18
18
  model.send(:include, mod)
@@ -38,6 +38,7 @@ module SymmetricEncryption
38
38
  # Note: The encoding of the supplied buffer is forced to binary if not already binary
39
39
  def self.present?(buffer)
40
40
  return false if buffer.nil? || (buffer == '')
41
+
41
42
  buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING)
42
43
  buffer.start_with?(MAGIC_HEADER)
43
44
  end
@@ -112,6 +113,7 @@ module SymmetricEncryption
112
113
  def parse!(buffer)
113
114
  offset = parse(buffer)
114
115
  return if offset.zero?
116
+
115
117
  buffer.slice!(0..offset - 1)
116
118
  buffer
117
119
  end
@@ -151,7 +153,7 @@ module SymmetricEncryption
151
153
 
152
154
  # Remove header and extract flags
153
155
  self.version = buffer.getbyte(offset)
154
- offset += 1
156
+ offset += 1
155
157
 
156
158
  unless cipher
157
159
  raise(
@@ -160,7 +162,7 @@ module SymmetricEncryption
160
162
  )
161
163
  end
162
164
 
163
- flags = buffer.getbyte(offset)
165
+ flags = buffer.getbyte(offset)
164
166
  offset += 1
165
167
 
166
168
  self.compress = (flags & FLAG_COMPRESSED) != 0
@@ -195,7 +197,7 @@ module SymmetricEncryption
195
197
 
196
198
  # Returns [String] this header as a string
197
199
  def to_s
198
- flags = 0
200
+ flags = 0
199
201
  flags |= FLAG_COMPRESSED if compressed?
200
202
  flags |= FLAG_IV if iv
201
203
  flags |= FLAG_KEY if key
@@ -256,9 +258,9 @@ module SymmetricEncryption
256
258
  # Exception when
257
259
  # - offset exceeds length of buffer
258
260
  # byteslice truncates when too long, but returns nil when start is beyond end of buffer
259
- len = buffer.byteslice(offset, 2).unpack('v').first
261
+ len = buffer.byteslice(offset, 2).unpack('v').first
260
262
  offset += 2
261
- out = buffer.byteslice(offset, len)
263
+ out = buffer.byteslice(offset, len)
262
264
  [out, offset + len]
263
265
  end
264
266
  end
@@ -3,68 +3,6 @@ module SymmetricEncryption
3
3
  class Key
4
4
  attr_reader :key, :iv, :cipher_name
5
5
 
6
- # Returns [Key] from cipher data usually extracted from the configuration file.
7
- #
8
- # Supports N level deep key encrypting keys.
9
- #
10
- # Configuration keys:
11
- # * key
12
- # * encrypted_key
13
- # * key_filename
14
- def self.from_config(key: nil, key_filename: nil, encrypted_key: nil, key_env_var: nil,
15
- iv:, key_encrypting_key: nil, cipher_name: 'aes-256-cbc')
16
-
17
- if key_encrypting_key.is_a?(Hash)
18
- # Recurse up the chain returning the parent key_encrypting_key
19
- key_encrypting_key = from_config(cipher_name: cipher_name, **key_encrypting_key)
20
- end
21
-
22
- key ||=
23
- if encrypted_key
24
- raise(ArgumentError, 'Missing mandatory :key_encrypting_key when config includes :encrypted_key') unless key_encrypting_key
25
- Keystore::Memory.new(encrypted_key: encrypted_key, key_encrypting_key: key_encrypting_key).read
26
- elsif key_filename
27
- Keystore::File.new(file_name: key_filename, key_encrypting_key: key_encrypting_key).read
28
- elsif key_env_var
29
- raise(ArgumentError, 'Missing mandatory :key_encrypting_key when config includes :key_env_var') unless key_encrypting_key
30
- Keystore::Environment.new(key_env_var: key_env_var, key_encrypting_key: key_encrypting_key).read
31
- end
32
-
33
- new(key: key, iv: iv, cipher_name: cipher_name)
34
- end
35
-
36
- # Migrate a prior config.
37
- #
38
- # Note:
39
- # * The config cannot be saved back to the config file once
40
- # migrated, without generating new Key Encrypting Keys.
41
- # * Only run this migration in the target environment so that the
42
- # current key encrypting files are present.
43
- def self.migrate_config!(config)
44
- # Backward compatibility - Deprecated
45
- private_rsa_key = config.delete(:private_rsa_key)
46
-
47
- # Migrate old encrypted_iv
48
- if (encrypted_iv = config.delete(:encrypted_iv)) && private_rsa_key
49
- encrypted_iv = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
50
- config[:iv] = ::Base64.decode64(encrypted_iv)
51
- end
52
-
53
- # Migrate old iv_filename
54
- if (file_name = config.delete(:iv_filename)) && private_rsa_key
55
- encrypted_iv = File.read(file_name)
56
- config[:iv] = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
57
- end
58
-
59
- # Backward compatibility - Deprecated
60
- config[:key_encrypting_key] = RSAKey.new(private_rsa_key) if private_rsa_key
61
-
62
- # Migrate old encrypted_key to new binary format
63
- if (encrypted_key = config[:encrypted_key]) && private_rsa_key
64
- config[:encrypted_key] = ::Base64.decode64(encrypted_key)
65
- end
66
- end
67
-
68
6
  def initialize(key: :random, iv: :random, cipher_name: 'aes-256-cbc')
69
7
  @key = key == :random ? ::OpenSSL::Cipher.new(cipher_name).random_key : key
70
8
  @iv = iv == :random ? ::OpenSSL::Cipher.new(cipher_name).random_iv : iv
@@ -73,6 +11,7 @@ module SymmetricEncryption
73
11
 
74
12
  def encrypt(string)
75
13
  return if string.nil?
14
+
76
15
  string = string.to_s
77
16
  return string if string.empty?
78
17
 
@@ -88,6 +27,7 @@ module SymmetricEncryption
88
27
 
89
28
  def decrypt(encrypted_string)
90
29
  return if encrypted_string.nil?
30
+
91
31
  encrypted_string = encrypted_string.to_s
92
32
  encrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
93
33
  return encrypted_string if encrypted_string.empty?
@@ -0,0 +1,172 @@
1
+ require 'base64'
2
+ require 'aws-sdk-kms'
3
+ module SymmetricEncryption
4
+ module Keystore
5
+ # Support AWS Key Management Service (KMS)
6
+ #
7
+ # Terms:
8
+ # Aws
9
+ # Amazon Web Services.
10
+ #
11
+ # CMK
12
+ # Customer Master Key.
13
+ # Master key to encrypt and decrypt data, specifically the DEK in this case.
14
+ # Stored in AWS, cannot be exported.
15
+ #
16
+ # DEK
17
+ # Data Encryption Key.
18
+ # Key used to encrypt local data.
19
+ # Encrypted with the CMK and stored locally.
20
+ #
21
+ # KMS
22
+ # Key Management Service.
23
+ # For generating and storing the CMK.
24
+ # Used to encrypt and decrypt the DEK.
25
+ #
26
+ # Recommended reading:
27
+ #
28
+ # Concepts:
29
+ # https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html
30
+ #
31
+ # Overview:
32
+ # https://docs.aws.amazon.com/kms/latest/developerguide/overview.html
33
+ #
34
+ # Process:
35
+ # 1. Create a customer master key (CMK) along with an alias for use by Symmetric Encryption.
36
+ # - Note: CMK is region specific.
37
+ # - Stored exclusively in AWS KMS, cannot be exported.
38
+ #
39
+ # 2. Generate and encrypt a data encryption key (DEK).
40
+ # - CMK is used to encrypt the DEK.
41
+ # - Encrypted DEK is stored locally.
42
+ # - Encrypted DEK is region specific.
43
+ # - DEK can be shared, but then must be re-encrypted in each region.
44
+ # - Shared DEK across regions for database replication.
45
+ # - List of regions to publish DEK to during generation / key-rotation.
46
+ # - DEK must be encrypted with CMK in each region consecutively.
47
+ #
48
+ # Warning:
49
+ # If access to the AWS KMS is ever lost, then it is not possible to decrypt any encrypted data.
50
+ # Examples:
51
+ # - Loss of access to AWS accounts.
52
+ # - Loss of region(s) in which master keys are stored.
53
+ class Aws
54
+ attr_reader :region, :key_files, :master_key_alias
55
+
56
+ # Returns [Hash] a new keystore configuration after generating the data key.
57
+ #
58
+ # Increments the supplied version number by 1.
59
+ #
60
+ # Sample Hash layout returned:
61
+ # {
62
+ # cipher_name: aes-256-cbc,
63
+ # version: 8,
64
+ # keystore: :aws,
65
+ # master_key_alias: 'alias/symmetric-encryption/application/production',
66
+ # key_files: [
67
+ # {region: blah1, file_name: "~/symmetric-encryption/application_production_blah1_v6.encrypted_key"},
68
+ # {region: blah2, file_name: "~/symmetric-encryption/application_production_blah2_v6.encrypted_key"},
69
+ # ],
70
+ # iv: 'T80pYzD0E6e/bJCdjZ6TiQ=='
71
+ # }
72
+ def self.generate_data_key(version: 0,
73
+ regions: Utils::Aws::AWS_US_REGIONS,
74
+ dek: nil,
75
+ cipher_name:,
76
+ app_name:,
77
+ environment:,
78
+ key_path:)
79
+
80
+ # TODO: Also support generating environment variables instead of files.
81
+
82
+ version >= 255 ? (version = 1) : (version += 1)
83
+ regions = Array(regions).dup
84
+
85
+ master_key_alias = master_key_alias(app_name, environment)
86
+
87
+ # File per region for holding the encrypted data key
88
+ key_files = regions.collect do |region|
89
+ file_name = "#{app_name}_#{environment}_#{region}_v#{version}.encrypted_key"
90
+ {region: region, file_name: ::File.join(key_path, file_name)}
91
+ end
92
+
93
+ keystore = new(key_files: key_files, master_key_alias: master_key_alias)
94
+ unless dek
95
+ data_key = keystore.aws(regions.first).generate_data_key(cipher_name)
96
+ dek = Key.new(key: data_key, cipher_name: cipher_name)
97
+ end
98
+ keystore.write(dek.key)
99
+
100
+ {
101
+ keystore: :aws,
102
+ cipher_name: dek.cipher_name,
103
+ version: version,
104
+ master_key_alias: master_key_alias,
105
+ key_files: key_files,
106
+ iv: dek.iv
107
+ }
108
+ end
109
+
110
+ # Alias pointing to the active version of the master key for that region.
111
+ def self.master_key_alias(app_name, environment)
112
+ @master_key_alias ||= "alias/symmetric-encryption/#{app_name}/#{environment}"
113
+ end
114
+
115
+ # Stores the Encryption key in a file.
116
+ # Secures the Encryption key by encrypting it with a key encryption key.
117
+ def initialize(region: nil, key_files:, master_key_alias:, key_encrypting_key: nil)
118
+ @key_files = key_files
119
+ @master_key_alias = master_key_alias
120
+ @region = region || ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'] || ::Aws.config[:region]
121
+ if key_encrypting_key
122
+ raise(SymmetricEncryption::ConfigError, 'AWS KMS keystore encrypts the key itself, so does not support supplying a key_encrypting_key')
123
+ end
124
+ end
125
+
126
+ # Reads the data key environment variable, if present, otherwise a file.
127
+ # Decrypts the key using the master key for this region.
128
+ def read
129
+ key_file = key_files.find { |i| i[:region] == region }
130
+ raise(SymmetricEncryption::ConfigError, "region: #{region} not available in the supplied key_files") unless key_file
131
+
132
+ file_name = key_file[:file_name]
133
+ raise(SymmetricEncryption::ConfigError, 'file_name is mandatory for each key_file entry') unless file_name
134
+
135
+ raise(SymmetricEncryption::ConfigError, "File #{file_name} could not be found") unless ::File.exist?(file_name)
136
+
137
+ # TODO: Validate that file is not globally readable.
138
+ encoded_dek = ::File.open(file_name, 'rb', &:read)
139
+ encrypted_data_key = Base64.strict_decode64(encoded_dek)
140
+ aws(region).decrypt(encrypted_data_key)
141
+ end
142
+
143
+ # Encrypt and write the data key to the file for each region.
144
+ def write(data_key)
145
+ key_files.each do |key_file|
146
+ region = key_file[:region]
147
+ file_name = key_file[:file_name]
148
+
149
+ raise(ArgumentError, 'region and file_name are mandatory for each key_file entry') unless region && file_name
150
+
151
+ encrypted_data_key = aws(region).encrypt(data_key)
152
+ encoded_dek = Base64.strict_encode64(encrypted_data_key)
153
+ write_to_file(file_name, encoded_dek)
154
+ end
155
+ end
156
+
157
+ def aws(region)
158
+ Utils::Aws.new(region: region, master_key_alias: master_key_alias)
159
+ end
160
+
161
+ private
162
+
163
+ # Write to the supplied file_name, backing up the existing file if present
164
+ def write_to_file(file_name, data)
165
+ path = ::File.dirname(file_name)
166
+ ::FileUtils.mkdir_p(path) unless ::File.directory?(path)
167
+ ::File.rename(file_name, "#{file_name}.#{Time.now.to_i}") if ::File.exist?(file_name)
168
+ ::File.open(file_name, 'wb') { |file| file.write(data) }
169
+ end
170
+ end
171
+ end
172
+ end