symmetric-encryption 3.9.1 → 4.0.0.beta3

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -0
  3. data/bin/symmetric-encryption +5 -0
  4. data/lib/symmetric_encryption/cipher.rb +162 -419
  5. data/lib/symmetric_encryption/cli.rb +343 -0
  6. data/lib/symmetric_encryption/coerce.rb +5 -20
  7. data/lib/symmetric_encryption/config.rb +128 -50
  8. data/lib/symmetric_encryption/extensions/mongo_mapper/plugins/encrypted_key.rb +2 -2
  9. data/lib/symmetric_encryption/generator.rb +3 -2
  10. data/lib/symmetric_encryption/header.rb +260 -0
  11. data/lib/symmetric_encryption/key.rb +106 -0
  12. data/lib/symmetric_encryption/keystore/environment.rb +90 -0
  13. data/lib/symmetric_encryption/keystore/file.rb +102 -0
  14. data/lib/symmetric_encryption/keystore/memory.rb +53 -0
  15. data/lib/symmetric_encryption/keystore.rb +124 -0
  16. data/lib/symmetric_encryption/railtie.rb +5 -7
  17. data/lib/symmetric_encryption/reader.rb +74 -55
  18. data/lib/symmetric_encryption/rsa_key.rb +24 -0
  19. data/lib/symmetric_encryption/symmetric_encryption.rb +64 -102
  20. data/lib/symmetric_encryption/utils/re_encrypt_files.rb +140 -0
  21. data/lib/symmetric_encryption/version.rb +1 -1
  22. data/lib/symmetric_encryption/writer.rb +104 -117
  23. data/lib/symmetric_encryption.rb +9 -4
  24. data/test/active_record_test.rb +61 -40
  25. data/test/cipher_test.rb +179 -236
  26. data/test/config/symmetric-encryption.yml +140 -82
  27. data/test/header_test.rb +218 -0
  28. data/test/key_test.rb +231 -0
  29. data/test/keystore/environment_test.rb +119 -0
  30. data/test/keystore/file_test.rb +125 -0
  31. data/test/keystore_test.rb +59 -0
  32. data/test/mongoid_test.rb +13 -13
  33. data/test/reader_test.rb +52 -53
  34. data/test/symmetric_encryption_test.rb +50 -135
  35. data/test/test_db.sqlite3 +0 -0
  36. data/test/writer_test.rb +52 -31
  37. metadata +26 -14
  38. data/examples/symmetric-encryption.yml +0 -108
  39. data/lib/rails/generators/symmetric_encryption/config/config_generator.rb +0 -22
  40. data/lib/rails/generators/symmetric_encryption/config/templates/symmetric-encryption.yml +0 -50
  41. data/lib/rails/generators/symmetric_encryption/heroku_config/heroku_config_generator.rb +0 -20
  42. data/lib/rails/generators/symmetric_encryption/heroku_config/templates/symmetric-encryption.yml +0 -78
  43. data/lib/rails/generators/symmetric_encryption/new_keys/new_keys_generator.rb +0 -14
  44. data/lib/symmetric_encryption/key_encryption_key.rb +0 -32
  45. data/lib/symmetric_encryption/railties/symmetric_encryption.rake +0 -84
  46. data/lib/symmetric_encryption/utils/re_encrypt_config_files.rb +0 -82
@@ -0,0 +1,260 @@
1
+ module SymmetricEncryption
2
+ # Defines the Header Structure returned when parsing the header.
3
+ #
4
+ # Note:
5
+ # * Header only works against binary encrypted data that has not been decoded.
6
+ # * Decode data first before trying to extract its header.
7
+ # * Decoding is not required when encoding is set to `:none`.
8
+ class Header
9
+ # Encrypted data includes this header prior to encoding when
10
+ # `always_add_header` is true.
11
+ MAGIC_HEADER = '@EnC'.force_encoding(SymmetricEncryption::BINARY_ENCODING)
12
+ MAGIC_HEADER_SIZE = MAGIC_HEADER.size
13
+
14
+ # [true|false] Whether to compress the data before encryption.
15
+ # If supplied in the header.
16
+ attr_accessor :compress
17
+
18
+ # [String] IV used to encrypt the data.
19
+ # If supplied in the header.
20
+ attr_accessor :iv
21
+
22
+ # [String] Key used to encrypt the data.
23
+ # If supplied in the header.
24
+ attr_accessor :key
25
+
26
+ # [String] Name of the cipher used.
27
+ attr_accessor :cipher_name
28
+
29
+ # [Integer] Version of the cipher used.
30
+ attr_reader :version
31
+
32
+ # [String] Binary auth tag used to encrypt the data.
33
+ # Usually 16 bytes.
34
+ # Present when using an authenticated encryption mode.
35
+ attr_reader :auth_tag
36
+
37
+ # Returns whether the supplied buffer starts with a symmetric_encryption header
38
+ # Note: The encoding of the supplied buffer is forced to binary if not already binary
39
+ def self.present?(buffer)
40
+ return false if buffer.nil? || (buffer == '')
41
+ buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING)
42
+ buffer.start_with?(MAGIC_HEADER)
43
+ end
44
+
45
+ # Returns a magic header for this cipher instance that can be placed at
46
+ # the beginning of a file or stream to indicate how the data was encrypted
47
+ #
48
+ # Parameters
49
+ # compress [true|false]
50
+ # Whether the data should be compressed before encryption.
51
+ # Default: false
52
+ #
53
+ # iv [String]
54
+ # The iv to to put in the header
55
+ # Default: nil : Exclude from header
56
+ #
57
+ # key [String]
58
+ # The key to to put in the header.
59
+ # The key is encrypted using the global encryption key
60
+ # Default: nil : Exclude key from header
61
+ #
62
+ # version: [Integer (0..255)]
63
+ # Version of the global cipher used to encrypt the data,
64
+ # or the encryption key if supplied.
65
+ # default: The current global encryption cipher version.
66
+ #
67
+ # cipher_name [String]
68
+ # The cipher_name to be used for encrypting the data portion.
69
+ # For example 'aes-256-cbc'
70
+ # `key` if supplied is encrypted with the cipher name based on the cipher version in this header.
71
+ # Intended for use when encrypting large files with a different cipher to the global one.
72
+ # Default: nil : Exclude cipher_name name from header
73
+ def initialize(version: SymmetricEncryption.cipher.version,
74
+ compress: false,
75
+ iv: nil,
76
+ key: nil,
77
+ cipher_name: nil,
78
+ auth_tag: nil)
79
+
80
+ @version = version
81
+ @compress = compress
82
+ @iv = iv
83
+ @key = key
84
+ @cipher_name = cipher_name
85
+ @auth_tag = auth_tag
86
+ end
87
+
88
+ # Returns [SymmetricEncryption::Cipher] the cipher used to decrypt or encrypt the key
89
+ # specified in this header, if supplied.
90
+ def cipher
91
+ @cipher ||= SymmetricEncryption.cipher(version)
92
+ end
93
+
94
+ def version=(version)
95
+ @version = version
96
+ @cipher = nil
97
+ end
98
+
99
+ def compressed?
100
+ @compress
101
+ end
102
+
103
+ # Returns [String] the encrypted data without header
104
+ # Returns nil if no header is present
105
+ #
106
+ # The supplied buffer will be updated directly and
107
+ # its header will be stripped if present.
108
+ #
109
+ # Parameters
110
+ # buffer
111
+ # String to extract the header from
112
+ def parse!(buffer)
113
+ offset = parse(buffer)
114
+ return if offset == 0
115
+ buffer.slice!(0..offset - 1)
116
+ buffer
117
+ end
118
+
119
+ # Returns [Integer] the offset within the buffer of the data after the header has been read.
120
+ #
121
+ # Returns 0 if no header is present
122
+ def parse(buffer, offset = 0)
123
+ return 0 if buffer.nil? || (buffer == '') || (buffer.length <= MAGIC_HEADER_SIZE + 2)
124
+
125
+ # Symmetric Encryption Header
126
+ #
127
+ # Consists of:
128
+ # 4 Bytes: Magic Header Prefix: @Enc
129
+ # 1 Byte: The version of the cipher used to encrypt the header.
130
+ # 1 Byte: Flags:
131
+ # Bit 1: Whether the data is compressed
132
+ # Bit 2: Whether the IV is included
133
+ # Bit 3: Whether the Key is included
134
+ # Bit 4: Whether the Cipher Name is included
135
+ # Bit 5: Future use
136
+ # Bit 6: Future use
137
+ # Bit 7: Future use
138
+ # Bit 8: Future use
139
+ # 2 Bytes: IV Length (little endian), if included.
140
+ # IV in binary form.
141
+ # 2 Bytes: Key Length (little endian), if included.
142
+ # Key in binary form
143
+ # 2 Bytes: Cipher Name Length (little endian), if included.
144
+ # Cipher name it UTF8 text
145
+
146
+ buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING)
147
+ header = buffer.byteslice(offset, MAGIC_HEADER_SIZE)
148
+ return 0 unless header == MAGIC_HEADER
149
+
150
+ offset += MAGIC_HEADER_SIZE
151
+
152
+ # Remove header and extract flags
153
+ self.version = buffer.getbyte(offset)
154
+ offset += 1
155
+
156
+ raise(SymmetricEncryption::CipherError, "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers") unless cipher
157
+ flags = buffer.getbyte(offset)
158
+ offset += 1
159
+
160
+ self.compress = (flags & FLAG_COMPRESSED) != 0
161
+
162
+ if (flags & FLAG_IV) != 0
163
+ self.iv, offset = read_string(buffer, offset)
164
+ else
165
+ self.iv = nil
166
+ end
167
+
168
+ if (flags & FLAG_KEY) != 0
169
+ encrypted_key, offset = read_string(buffer, offset)
170
+ self.key = cipher.binary_decrypt(encrypted_key)
171
+ else
172
+ self.key = nil
173
+ end
174
+
175
+ if (flags & FLAG_CIPHER_NAME) != 0
176
+ self.cipher_name, offset = read_string(buffer, offset)
177
+ else
178
+ self.cipher_name = nil
179
+ end
180
+
181
+ if (flags & FLAG_AUTH_TAG) != 0
182
+ self.auth_tag, offset = read_string(buffer, offset)
183
+ else
184
+ self.auth_tag = nil
185
+ end
186
+
187
+ offset
188
+ end
189
+
190
+ # Returns [String] this header as a string
191
+ def to_s
192
+ flags = 0
193
+ flags |= FLAG_COMPRESSED if compressed?
194
+ flags |= FLAG_IV if iv
195
+ flags |= FLAG_KEY if key
196
+ flags |= FLAG_CIPHER_NAME if cipher_name
197
+ flags |= FLAG_AUTH_TAG if auth_tag
198
+
199
+ header = "#{MAGIC_HEADER}#{version.chr(SymmetricEncryption::BINARY_ENCODING)}#{flags.chr(SymmetricEncryption::BINARY_ENCODING)}"
200
+
201
+ if iv
202
+ header << [iv.length].pack('v')
203
+ header << iv
204
+ end
205
+
206
+ if key
207
+ encrypted = cipher.binary_encrypt(key, header: false)
208
+ header << [encrypted.length].pack('v')
209
+ header << encrypted
210
+ end
211
+
212
+ if cipher_name
213
+ header << [cipher_name.length].pack('v')
214
+ header << cipher_name
215
+ end
216
+
217
+ if auth_tag
218
+ header << [auth_tag.length].pack('v')
219
+ header << auth_tag
220
+ end
221
+
222
+ header
223
+ end
224
+
225
+ private
226
+
227
+ FLAG_COMPRESSED = 0b1000_0000
228
+ FLAG_IV = 0b0100_0000
229
+ FLAG_KEY = 0b0010_0000
230
+ FLAG_CIPHER_NAME = 0b0001_0000
231
+ FLAG_AUTH_TAG = 0b0000_1000
232
+
233
+ attr_writer :auth_tag
234
+
235
+ # Extracts a string from the supplied buffer.
236
+ # The buffer starts with a 2 byte length indicator in little endian format.
237
+ #
238
+ # Parameters
239
+ # buffer [String]
240
+ # offset [Integer]
241
+ # Start position within the buffer.
242
+ #
243
+ # Returns [string, offset]
244
+ # string [String]
245
+ # The string copied from the buffer.
246
+ # offset [Integer]
247
+ # The new offset within the buffer.
248
+ def read_string(buffer, offset)
249
+ # TODO: Length check
250
+ # Exception when
251
+ # - offset exceeds length of buffer
252
+ # byteslice truncates when too long, but returns nil when start is beyond end of buffer
253
+ len = buffer.byteslice(offset, 2).unpack('v').first
254
+ offset += 2
255
+ out = buffer.byteslice(offset, len)
256
+ [out, offset + len]
257
+ end
258
+
259
+ end
260
+ end
@@ -0,0 +1,106 @@
1
+ # The key, iv and encrypted data are handled in their raw form, with no encoding.
2
+ module SymmetricEncryption
3
+ class Key
4
+ attr_reader :key, :iv, :cipher_name
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
+ def initialize(key: :random, iv: :random, cipher_name: 'aes-256-cbc')
69
+ @key = key == :random ? ::OpenSSL::Cipher.new(cipher_name).random_key : key
70
+ @iv = iv == :random ? ::OpenSSL::Cipher.new(cipher_name).random_iv : iv
71
+ @cipher_name = cipher_name
72
+ end
73
+
74
+ def encrypt(string)
75
+ return if string.nil?
76
+ string = string.to_s
77
+ return string if string.empty?
78
+
79
+ # Creates a new OpenSSL::Cipher with every call so that this key instance is thread-safe.
80
+ openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
81
+ openssl_cipher.encrypt
82
+ openssl_cipher.key = key
83
+ openssl_cipher.iv = iv
84
+
85
+ result = openssl_cipher.update(string)
86
+ result << openssl_cipher.final
87
+ end
88
+
89
+ def decrypt(encrypted_string)
90
+ return if encrypted_string.nil?
91
+ encrypted_string = encrypted_string.to_s
92
+ encrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
93
+ return encrypted_string if encrypted_string.empty?
94
+
95
+ # Creates a new OpenSSL::Cipher with every call so that this key instance is thread-safe.
96
+ openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
97
+ openssl_cipher.decrypt
98
+ openssl_cipher.key = key
99
+ openssl_cipher.iv = iv
100
+
101
+ result = openssl_cipher.update(encrypted_string)
102
+ result << openssl_cipher.final
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,90 @@
1
+ module SymmetricEncryption
2
+ module Keystore
3
+ # Store the encrypted encryption key in an environment variable
4
+ class Environment < Memory
5
+ attr_accessor :key_env_var, :encoding
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.
30
+ #
31
+ # Increments the supplied version number by 1.
32
+ def self.new_key_config(cipher_name:, app_name:, environment:, version: 0, dek: nil)
33
+ version >= 255 ? (version = 1) : (version += 1)
34
+
35
+ kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
36
+ dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
37
+
38
+ key_env_var = "#{app_name}_#{environment}_v#{version}".upcase.gsub('-', '_')
39
+ new(key_env_var: key_env_var, key_encrypting_key: kek).write(dek.key)
40
+
41
+ {
42
+ cipher_name: dek.cipher_name,
43
+ version: version,
44
+ key_env_var: key_env_var,
45
+ iv: dek.iv,
46
+ key_encrypting_key: {
47
+ key: kek.key,
48
+ iv: kek.iv,
49
+ }
50
+ }
51
+ end
52
+
53
+ # Stores the Encryption key in an environment var.
54
+ # Secures the Encryption key by encrypting it with a key encryption key.
55
+ def initialize(key_encrypting_key:, key_env_var:, encoding: :base64strict)
56
+ @key_env_var = key_env_var
57
+ @key_encrypting_key = key_encrypting_key
58
+ @encoding = encoding
59
+ end
60
+
61
+ # Returns the Encryption key in the clear.
62
+ def read
63
+ encrypted = ENV[key_env_var]
64
+ raise "The Environment Variable #{key_env_var} must be set with the encrypted encryption key." unless encrypted
65
+ binary = encoder.decode(encrypted)
66
+ key_encrypting_key.decrypt(binary)
67
+ end
68
+
69
+ # Write the encrypted Encryption key to `encrypted_key` attribute.
70
+ def write(key)
71
+ encrypted_key = key_encrypting_key.encrypt(key)
72
+ 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"
78
+ puts "********************************************************************************"
79
+ end
80
+
81
+ private
82
+
83
+ # Returns [SymmetricEncryption::Encoder] the encoder to use for the current encoding.
84
+ def encoder
85
+ @encoder ||= SymmetricEncryption::Encoder[encoding]
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,102 @@
1
+ module SymmetricEncryption
2
+ module Keystore
3
+ class File
4
+ attr_accessor :file_name, :key_encrypting_key
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.
30
+ #
31
+ # Increments the supplied version number by 1.
32
+ def self.new_key_config(key_path:, cipher_name:, app_name:, environment:, version: 0, dek: nil)
33
+ version >= 255 ? (version = 1) : (version += 1)
34
+
35
+ dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
36
+ kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
37
+ kekek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
38
+
39
+ 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)
41
+
42
+ kekek_file_name = ::File.join(key_path, "#{app_name}_#{environment}_v#{version}.kekek")
43
+ new(file_name: kekek_file_name).write(kekek.key)
44
+
45
+ {
46
+ cipher_name: dek.cipher_name,
47
+ version: version,
48
+ key_filename: dek_file_name,
49
+ iv: dek.iv,
50
+ key_encrypting_key: {
51
+ encrypted_key: kekek.encrypt(kek.key),
52
+ iv: kek.iv,
53
+ key_encrypting_key: {
54
+ key_filename: kekek_file_name,
55
+ iv: kekek.iv
56
+ }
57
+ }
58
+ }
59
+ end
60
+
61
+ # Stores the Encryption key in a file.
62
+ # 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
65
+ @key_encrypting_key = key_encrypting_key
66
+ end
67
+
68
+ # Returns the Encryption key in the clear.
69
+ def read
70
+ # TODO: Validate that file is not globally readable.
71
+ raise(SymmetricEncryption::ConfigError, "Symmetric Encryption key file: '#{file_name}' not found") unless ::File.exist?(file_name)
72
+
73
+ data = read_from_file
74
+ key_encrypting_key ? key_encrypting_key.decrypt(data) : data
75
+ end
76
+
77
+ # Encrypt and write the key to file.
78
+ def write(key)
79
+ data = key_encrypting_key ? key_encrypting_key.encrypt(key) : key
80
+ write_to_file(data)
81
+ end
82
+
83
+ private
84
+
85
+ # Read from the file, raising an exception if it is not found
86
+ def read_from_file
87
+ ::File.open(file_name, 'rb') { |f| f.read }
88
+ rescue Errno::ENOENT
89
+ raise(SymmetricEncryption::ConfigError, "Symmetric Encryption key file: '#{file_name}' not found or readable")
90
+ end
91
+
92
+ # Write to the supplied file_name, backing up the existing file if present
93
+ def write_to_file(data)
94
+ key_path = ::File.dirname(file_name)
95
+ ::FileUtils.mkdir_p(key_path) unless ::File.directory?(key_path)
96
+ ::File.rename(file_name, "#{file_name}.#{Time.now.to_i}") if ::File.exist?(file_name)
97
+ ::File.open(file_name, 'wb') { |file| file.write(data) }
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ module SymmetricEncryption
2
+ module Keystore
3
+ class Memory
4
+ attr_accessor :key_encrypting_key
5
+ attr_reader :encrypted_key
6
+
7
+ # Returns [Hash] a new cipher, and writes its encrypted key file.
8
+ #
9
+ # Increments the supplied version number by 1.
10
+ #
11
+ # Notes:
12
+ # * For development and testing purposes only!!
13
+ # * Never store the encrypted encryption key in the source code / config file.
14
+ def self.new_key_config(cipher_name:, app_name:, environment:, version: 0, dek: nil)
15
+ version >= 255 ? (version = 1) : (version += 1)
16
+
17
+ kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
18
+ dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
19
+
20
+ encrypted_key = new(key_encrypting_key: kek).write(dek.key)
21
+
22
+ {
23
+ cipher_name: cipher_name,
24
+ version: version,
25
+ encrypted_key: encrypted_key,
26
+ iv: iv,
27
+ key_encrypting_key: {
28
+ key: kek.key,
29
+ iv: kek.iv,
30
+ }
31
+ }
32
+ end
33
+
34
+ # Stores the Encryption key in a string.
35
+ # Secures the Encryption key by encrypting it with a key encryption key.
36
+ def initialize(encrypted_key: nil, key_encrypting_key:)
37
+ @encrypted_key = encrypted_key
38
+ @key_encrypting_key = key_encrypting_key
39
+ end
40
+
41
+ # Returns the Encryption key in the clear.
42
+ def read
43
+ key_encrypting_key.decrypt(encrypted_key)
44
+ end
45
+
46
+ # Write the encrypted Encryption key to `encrypted_key` attribute.
47
+ def write(key)
48
+ self.encrypted_key = key_encrypting_key.encrypt(key)
49
+ end
50
+
51
+ end
52
+ end
53
+ end