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.
@@ -4,32 +4,10 @@ module SymmetricEncryption
4
4
  class Environment < Memory
5
5
  attr_accessor :key_env_var, :encoding
6
6
 
7
- # Returns [Hash] initial configuration for heroku.
8
- # Displays the keys that need to be added to the heroku environment.
9
- def self.new_config(app_name: 'symmetric-encryption',
10
- environments: %i[development test release production],
11
- cipher_name: 'aes-256-cbc')
12
-
13
- configs = {}
14
- environments.each do |environment|
15
- environment = environment.to_sym
16
- configs[environment] =
17
- if %i[development test].include?(environment)
18
- Keystore.dev_config
19
- else
20
- cfg = new_key_config(cipher_name: cipher_name, app_name: app_name, environment: environment)
21
- {
22
- ciphers: [cfg]
23
- }
24
- end
25
- end
26
- configs
27
- end
28
-
29
- # Returns [Hash] a new cipher, and writes its encrypted key file.
7
+ # Returns [Hash] a new keystore configuration after generating the data key.
30
8
  #
31
9
  # Increments the supplied version number by 1.
32
- def self.new_key_config(cipher_name:, app_name:, environment:, version: 0, dek: nil)
10
+ def self.generate_data_key(cipher_name:, app_name:, environment:, version: 0, dek: nil)
33
11
  version >= 255 ? (version = 1) : (version += 1)
34
12
 
35
13
  kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
@@ -39,6 +17,7 @@ module SymmetricEncryption
39
17
  new(key_env_var: key_env_var, key_encrypting_key: kek).write(dek.key)
40
18
 
41
19
  {
20
+ keystore: :environment,
42
21
  cipher_name: dek.cipher_name,
43
22
  version: version,
44
23
  key_env_var: key_env_var,
@@ -70,11 +49,8 @@ module SymmetricEncryption
70
49
  def write(key)
71
50
  encrypted_key = key_encrypting_key.encrypt(key)
72
51
  puts "\n\n********************************************************************************"
73
- puts "Add the environment key to Heroku:\n\n"
74
- puts " heroku config:add #{key_env_var}=#{encoder.encode(encrypted_key)}"
75
- puts
76
- puts "Or, if using environment variables on another system set the environment variable as follows:\n\n"
77
- puts " export #{key_env_var}=\"#{encoder.encode(encrypted_key)}\"\n\n"
52
+ puts "Set the environment variable as follows:"
53
+ puts " export #{key_env_var}=\"#{encoder.encode(encrypted_key)}\""
78
54
  puts '********************************************************************************'
79
55
  end
80
56
 
@@ -3,33 +3,10 @@ module SymmetricEncryption
3
3
  class File
4
4
  attr_accessor :file_name, :key_encrypting_key
5
5
 
6
- # Returns [Hash] initial configuration.
7
- # Generates the encrypted key file for every environment except development and test.
8
- def self.new_config(key_path: '/etc/symmetric-encryption',
9
- app_name: 'symmetric-encryption',
10
- environments: %i[development test release production],
11
- cipher_name: 'aes-256-cbc')
12
-
13
- configs = {}
14
- environments.each do |environment|
15
- environment = environment.to_sym
16
- configs[environment] =
17
- if %i[development test].include?(environment)
18
- Keystore.dev_config
19
- else
20
- cfg = new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, environment: environment)
21
- {
22
- ciphers: [cfg]
23
- }
24
- end
25
- end
26
- configs
27
- end
28
-
29
- # Returns [Hash] a new cipher, and writes its encrypted key file.
6
+ # Returns [Hash] a new keystore configuration after generating the data key.
30
7
  #
31
8
  # Increments the supplied version number by 1.
32
- def self.new_key_config(key_path:, cipher_name:, app_name:, environment:, version: 0, dek: nil)
9
+ def self.generate_data_key(key_path:, cipher_name:, app_name:, environment:, version: 0, dek: nil)
33
10
  version >= 255 ? (version = 1) : (version += 1)
34
11
 
35
12
  dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
@@ -37,12 +14,13 @@ module SymmetricEncryption
37
14
  kekek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
38
15
 
39
16
  dek_file_name = ::File.join(key_path, "#{app_name}_#{environment}_v#{version}.encrypted_key")
40
- new(file_name: dek_file_name, key_encrypting_key: kek).write(dek.key)
17
+ new(key_filename: dek_file_name, key_encrypting_key: kek).write(dek.key)
41
18
 
42
19
  kekek_file_name = ::File.join(key_path, "#{app_name}_#{environment}_v#{version}.kekek")
43
- new(file_name: kekek_file_name).write(kekek.key)
20
+ new(key_filename: kekek_file_name).write(kekek.key)
44
21
 
45
22
  {
23
+ keystore: :file,
46
24
  cipher_name: dek.cipher_name,
47
25
  version: version,
48
26
  key_filename: dek_file_name,
@@ -60,8 +38,8 @@ module SymmetricEncryption
60
38
 
61
39
  # Stores the Encryption key in a file.
62
40
  # Secures the Encryption key by encrypting it with a key encryption key.
63
- def initialize(file_name:, key_encrypting_key: nil)
64
- @file_name = file_name
41
+ def initialize(key_filename:, key_encrypting_key: nil)
42
+ @file_name = key_filename
65
43
  @key_encrypting_key = key_encrypting_key
66
44
  end
67
45
 
@@ -0,0 +1,22 @@
1
+ module SymmetricEncryption
2
+ module Keystore
3
+ # Heroku uses environment variables too.
4
+ class Heroku < Environment
5
+ # Returns [Hash] a new keystore configuration after generating the data key.
6
+ def self.generate_data_key(**args)
7
+ config = super(**args)
8
+ config[:keystore] = :heroku
9
+ config
10
+ end
11
+
12
+ # Write the encrypted Encryption key to `encrypted_key` attribute.
13
+ def write(key)
14
+ encrypted_key = key_encrypting_key.encrypt(key)
15
+ puts "\n\n********************************************************************************"
16
+ puts "Add the environment key to Heroku:\n\n"
17
+ puts " heroku config:add #{key_env_var}=#{encoder.encode(encrypted_key)}"
18
+ puts '********************************************************************************'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -5,14 +5,14 @@ module SymmetricEncryption
5
5
  attr_accessor :key_encrypting_key
6
6
  attr_reader :encrypted_key
7
7
 
8
- # Returns [Hash] a new cipher, and writes its encrypted key file.
8
+ # Returns [Hash] a new keystore configuration after generating the data key.
9
9
  #
10
10
  # Increments the supplied version number by 1.
11
11
  #
12
12
  # Notes:
13
13
  # * For development and testing purposes only!!
14
14
  # * Never store the encrypted encryption key in the source code / config file.
15
- def self.new_key_config(cipher_name:, app_name:, environment:, version: 0, dek: nil)
15
+ def self.generate_data_key(cipher_name:, app_name:, environment:, version: 0, dek: nil)
16
16
  version >= 255 ? (version = 1) : (version += 1)
17
17
 
18
18
  kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
@@ -21,6 +21,7 @@ module SymmetricEncryption
21
21
  encrypted_key = new(key_encrypting_key: kek).write(dek.key)
22
22
 
23
23
  {
24
+ keystore: :memory,
24
25
  cipher_name: cipher_name,
25
26
  version: version,
26
27
  encrypted_key: encrypted_key,
@@ -37,12 +37,12 @@ module SymmetricEncryption #:nodoc:
37
37
  rescue ArgumentError => exc
38
38
  puts "\nSymmetric Encryption not able to read keys."
39
39
  puts "#{exc.class.name} #{exc.message}"
40
- puts "To generate a new config file and key files: symmetric-encryption --generate --key-path /etc/#{app_name} --app-name #{app_name}\n\n"
40
+ puts "To generate a new config file and key files: symmetric-encryption --generate --app-name #{app_name}\n\n"
41
41
  raise(exc)
42
42
  end
43
43
  else
44
44
  puts "\nSymmetric Encryption config not found."
45
- puts "To generate a new config file and key files: symmetric-encryption --generate --key-path /etc/#{app_name} --app-name #{app_name}\n\n"
45
+ puts "To generate a new config file and key files: symmetric-encryption --generate --app-name #{app_name}\n\n"
46
46
  end
47
47
  end
48
48
  end
@@ -0,0 +1,139 @@
1
+ require 'base64'
2
+ require 'aws-sdk-kms'
3
+ module SymmetricEncryption
4
+ module Utils
5
+ # Wrap the AWS KMS client so that it automatically creates the Customer Master Key,
6
+ # if one does not already exist.
7
+ #
8
+ # Map OpenSSL cipher names to AWS KMS key specs.
9
+ class Aws
10
+ attr_reader :master_key_alias, :client
11
+
12
+ AWS_US_REGIONS = %w[us-east-1 us-east-2 us-west-1 us-west-2].freeze
13
+
14
+ # TODO: Map to OpenSSL ciphers
15
+ AWS_KEY_SPEC_MAP = {
16
+ 'aes-256-cbc' => 'AES_256',
17
+ 'aes-128-cbc' => 'AES_128'
18
+ }
19
+
20
+ # TODO: Move to Keystore::Aws
21
+ # Rotate the Customer Master key in each of the supplied regions.
22
+ # After the master key has been rotated, use `.write_key_files` to generate
23
+ # a new DEK and re-encrypt with the new CMK in each region.
24
+ # def self.rotate_master_key(master_key_alias:, cipher_name:, regions: AWS_US_REGIONS)
25
+ # Array(regions).collect do |region|
26
+ # key_manager = new(region: region, master_key_alias: master_key_alias, cipher_name: cipher_name)
27
+ # key_id = key_manager.create_master_key
28
+ # key_manager.create_alias(key_id)
29
+ # end
30
+ # end
31
+
32
+ def initialize(region:, master_key_alias:)
33
+ # Can region be read from environment?
34
+ # Region is required for filename / env var name
35
+ @client = ::Aws::KMS::Client.new(region: region)
36
+ @master_key_alias = master_key_alias
37
+ end
38
+
39
+ # Returns a new DEK encrypted using the CMK
40
+ def generate_encrypted_data_key(cipher_name)
41
+ auto_create_master_key do
42
+ client.generate_data_key_without_plaintext(key_id: master_key_alias, key_spec: key_spec(cipher_name)).ciphertext_blob
43
+ end
44
+ end
45
+
46
+ # Returns a new DEK in the clear
47
+ def generate_data_key(cipher_name)
48
+ auto_create_master_key do
49
+ client.generate_data_key(key_id: master_key_alias, key_spec: key_spec(cipher_name)).plaintext
50
+ end
51
+ end
52
+
53
+ # Decrypt data previously encrypted using the cmk
54
+ def decrypt(encrypted_data)
55
+ auto_create_master_key do
56
+ client.decrypt(ciphertext_blob: encrypted_data).plaintext
57
+ end
58
+ end
59
+
60
+ # Decrypt data previously encrypted using the cmk
61
+ def encrypt(data)
62
+ auto_create_master_key do
63
+ client.encrypt(key_id: master_key_alias, plaintext: data).ciphertext_blob
64
+ end
65
+ end
66
+
67
+ # Returns the AWS KMS key spec that matches the supplied OpenSSL cipher name
68
+ def key_spec(cipher_name)
69
+ key_spec = AWS_KEY_SPEC_MAP[cipher_name]
70
+ raise("OpenSSL Cipher: #{cipher_name} has not yet been mapped to an AWS key spec.") unless key_spec
71
+ key_spec
72
+ end
73
+
74
+ # Creates a new master key along with an alias that points to it.
75
+ # Returns [String] the new master key id that was created.
76
+ def create_master_key
77
+ key_id = create_new_master_key
78
+ create_alias(key_id)
79
+ key_id
80
+ end
81
+
82
+ # Deletes the current master key and its alias.
83
+ #
84
+ # retention_days: Number of days to keep the CMK before completely destroying it.
85
+ #
86
+ # NOTE:
87
+ # Use with caution, only intended for testing purposes !!!
88
+ def delete_master_key(retention_days: 30)
89
+ key_info = client.describe_key(key_id: master_key_alias)
90
+ ap key_info
91
+ resp = client.schedule_key_deletion(key_id: key_info.key_metadata.key_id, pending_window_in_days: retention_days)
92
+ ap client.delete_alias(alias_name: master_key_alias)
93
+ resp.deletion_date
94
+ rescue ::Aws::KMS::Errors::NotFoundException
95
+ nil
96
+ end
97
+
98
+ private
99
+
100
+ attr_reader :client
101
+
102
+ def whoami
103
+ @whoami ||= `whoami`.strip
104
+ rescue StandardError
105
+ @whoami = 'unknown'
106
+ end
107
+
108
+ # Creates a new Customer Master Key for Symmetric Encryption use.
109
+ def create_new_master_key
110
+ # TODO: Add error handling and retry
111
+
112
+ resp = client.create_key(
113
+ description: 'Symmetric Encryption for Ruby Customer Masker Key',
114
+ tags: [
115
+ {tag_key: 'CreatedAt', tag_value: Time.now.to_s},
116
+ {tag_key: 'CreatedBy', tag_value: whoami}
117
+ ]
118
+ )
119
+ resp.key_metadata.key_id
120
+ end
121
+
122
+ def create_alias(key_id)
123
+ # TODO: Add error handling and retry
124
+ # TODO: Move existing alias if any
125
+ client.create_alias(alias_name: master_key_alias, target_key_id: key_id)
126
+ end
127
+
128
+ def auto_create_master_key
129
+ attempt = 1
130
+ yield
131
+ rescue ::Aws::KMS::Errors::NotFoundException
132
+ raise if attempt >= 2
133
+ create_master_key
134
+ attempt += 1
135
+ retry
136
+ end
137
+ end
138
+ end
139
+ end
@@ -52,8 +52,16 @@ module SymmetricEncryption
52
52
  def re_encrypt_contents(file_name)
53
53
  return 0 if File.size(file_name) > 256 * 1024
54
54
 
55
+ lines = File.read(file_name)
56
+ hits, output_lines = re_encrypt_lines(lines)
57
+
58
+ File.open(file_name, 'wb') { |file| file.write(output_lines) } if hits.positive?
59
+ hits
60
+ end
61
+
62
+ # Replaces instances of encrypted data within lines of text with re-encrypted values
63
+ def re_encrypt_lines(lines)
55
64
  hits = 0
56
- lines = File.read(file_name)
57
65
  output_lines = ''
58
66
  r = regexp
59
67
  lines.each_line do |line|
@@ -72,8 +80,7 @@ module SymmetricEncryption
72
80
  line
73
81
  end
74
82
  end
75
- File.open(file_name, 'wb') { |file| file.write(output_lines) } if hits.positive?
76
- hits
83
+ [hits, output_lines]
77
84
  end
78
85
 
79
86
  # Re Encrypt an entire file
@@ -1,3 +1,3 @@
1
1
  module SymmetricEncryption
2
- VERSION = '4.0.1'.freeze
2
+ VERSION = '4.1.0.beta1'.freeze
3
3
  end
@@ -2,15 +2,6 @@ require_relative 'test_helper'
2
2
 
3
3
  class KeyTest < Minitest::Test
4
4
  describe SymmetricEncryption::Key do
5
- before do
6
- Dir.mkdir('tmp') unless Dir.exist?('tmp')
7
- end
8
-
9
- after do
10
- # Cleanup generated encryption key files.
11
- `rm tmp/dek_tester* 2> /dev/null`
12
- end
13
-
14
5
  let :random_key do
15
6
  SymmetricEncryption::Key.new
16
7
  end
@@ -27,30 +18,6 @@ class KeyTest < Minitest::Test
27
18
  SymmetricEncryption::Key.new(key: stored_key, iv: stored_iv)
28
19
  end
29
20
 
30
- let :stored_key2 do
31
- 'ABCDEF1234567890ABCDEF1234567890'
32
- end
33
-
34
- let :stored_iv2 do
35
- '1234567890ABCDEF'
36
- end
37
-
38
- let :key2 do
39
- SymmetricEncryption::Key.new(key: stored_key2, iv: stored_iv2)
40
- end
41
-
42
- let :stored_key3 do
43
- 'ABCDEF0123456789ABCDEF0123456789'
44
- end
45
-
46
- let :stored_iv3 do
47
- '0123456789ABCDEF'
48
- end
49
-
50
- let :key3 do
51
- SymmetricEncryption::Key.new(key: stored_key3, iv: stored_iv3)
52
- end
53
-
54
21
  let :ssn do
55
22
  '987654321'
56
23
  end
@@ -110,129 +77,5 @@ class KeyTest < Minitest::Test
110
77
  assert_equal stored_iv, key.iv
111
78
  end
112
79
  end
113
-
114
- describe '.from_config' do
115
- let :config do
116
- {key: stored_key, iv: stored_iv}
117
- end
118
-
119
- let :config_key do
120
- SymmetricEncryption::Key.from_config(config)
121
- end
122
-
123
- let :dek_file_name do
124
- 'tmp/dek_tester_dek.encrypted_key'
125
- end
126
-
127
- describe 'key' do
128
- it 'key' do
129
- assert_equal stored_key, config_key.key
130
- end
131
-
132
- it 'iv' do
133
- assert_equal stored_iv, config_key.iv
134
- end
135
-
136
- it 'cipher_name' do
137
- assert_equal 'aes-256-cbc', config_key.cipher_name
138
- end
139
- end
140
-
141
- describe 'encrypted_key' do
142
- let :config do
143
- {encrypted_key: key2.encrypt(stored_key), iv: stored_iv, key_encrypting_key: {key: stored_key2, iv: stored_iv2}}
144
- end
145
-
146
- it 'key' do
147
- assert_equal stored_key, config_key.key
148
- end
149
-
150
- it 'iv' do
151
- assert_equal stored_iv, config_key.iv
152
- end
153
-
154
- it 'cipher_name' do
155
- assert_equal 'aes-256-cbc', config_key.cipher_name
156
- end
157
- end
158
-
159
- describe 'key_filename' do
160
- let :config do
161
- File.open(dek_file_name, 'wb') { |f| f.write(key2.encrypt(stored_key)) }
162
- {key_filename: dek_file_name, iv: stored_iv, key_encrypting_key: {key: stored_key2, iv: stored_iv2}}
163
- end
164
-
165
- it 'key' do
166
- assert_equal stored_key, config_key.key
167
- end
168
-
169
- it 'iv' do
170
- assert_equal stored_iv, config_key.iv
171
- end
172
-
173
- it 'cipher_name' do
174
- assert_equal 'aes-256-cbc', config_key.cipher_name
175
- end
176
- end
177
-
178
- describe 'key_env_var' do
179
- let :env_var do
180
- 'TEST_KEY'
181
- end
182
-
183
- let :config do
184
- ENV[env_var] = ::Base64.encode64(key2.encrypt(stored_key))
185
- {key_env_var: env_var, iv: stored_iv, key_encrypting_key: {key: stored_key2, iv: stored_iv2}}
186
- end
187
-
188
- it 'key' do
189
- assert_equal stored_key, config_key.key
190
- end
191
-
192
- it 'iv' do
193
- assert_equal stored_iv, config_key.iv
194
- end
195
-
196
- it 'cipher_name' do
197
- assert_equal 'aes-256-cbc', config_key.cipher_name
198
- end
199
- end
200
-
201
- describe 'file store with kekek' do
202
- let :kekek_file_name do
203
- 'tmp/tester_kekek.key'
204
- end
205
-
206
- let :config do
207
- File.open(dek_file_name, 'wb') { |f| f.write(key2.encrypt(stored_key)) }
208
- encrypted_key = key3.encrypt(stored_key2)
209
- File.open(kekek_file_name, 'wb') { |f| f.write(stored_key3) }
210
- {
211
- key_filename: dek_file_name,
212
- iv: stored_iv,
213
- key_encrypting_key: {
214
- encrypted_key: encrypted_key,
215
- iv: stored_iv2,
216
- key_encrypting_key: {
217
- key_filename: kekek_file_name,
218
- iv: stored_iv3
219
- }
220
- }
221
- }
222
- end
223
-
224
- it 'key' do
225
- assert_equal stored_key, config_key.key
226
- end
227
-
228
- it 'iv' do
229
- assert_equal stored_iv, config_key.iv
230
- end
231
-
232
- it 'cipher_name' do
233
- assert_equal 'aes-256-cbc', config_key.cipher_name
234
- end
235
- end
236
- end
237
80
  end
238
81
  end