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