symmetric-encryption 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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