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.
- checksums.yaml +4 -4
- data/README.md +72 -0
- data/bin/symmetric-encryption +5 -0
- data/lib/symmetric_encryption/cipher.rb +162 -419
- data/lib/symmetric_encryption/cli.rb +343 -0
- data/lib/symmetric_encryption/coerce.rb +5 -20
- data/lib/symmetric_encryption/config.rb +128 -50
- data/lib/symmetric_encryption/extensions/mongo_mapper/plugins/encrypted_key.rb +2 -2
- data/lib/symmetric_encryption/generator.rb +3 -2
- data/lib/symmetric_encryption/header.rb +260 -0
- data/lib/symmetric_encryption/key.rb +106 -0
- data/lib/symmetric_encryption/keystore/environment.rb +90 -0
- data/lib/symmetric_encryption/keystore/file.rb +102 -0
- data/lib/symmetric_encryption/keystore/memory.rb +53 -0
- data/lib/symmetric_encryption/keystore.rb +124 -0
- data/lib/symmetric_encryption/railtie.rb +5 -7
- data/lib/symmetric_encryption/reader.rb +74 -55
- data/lib/symmetric_encryption/rsa_key.rb +24 -0
- data/lib/symmetric_encryption/symmetric_encryption.rb +64 -102
- data/lib/symmetric_encryption/utils/re_encrypt_files.rb +140 -0
- data/lib/symmetric_encryption/version.rb +1 -1
- data/lib/symmetric_encryption/writer.rb +104 -117
- data/lib/symmetric_encryption.rb +9 -4
- data/test/active_record_test.rb +61 -40
- data/test/cipher_test.rb +179 -236
- data/test/config/symmetric-encryption.yml +140 -82
- data/test/header_test.rb +218 -0
- data/test/key_test.rb +231 -0
- data/test/keystore/environment_test.rb +119 -0
- data/test/keystore/file_test.rb +125 -0
- data/test/keystore_test.rb +59 -0
- data/test/mongoid_test.rb +13 -13
- data/test/reader_test.rb +52 -53
- data/test/symmetric_encryption_test.rb +50 -135
- data/test/test_db.sqlite3 +0 -0
- data/test/writer_test.rb +52 -31
- metadata +26 -14
- data/examples/symmetric-encryption.yml +0 -108
- data/lib/rails/generators/symmetric_encryption/config/config_generator.rb +0 -22
- data/lib/rails/generators/symmetric_encryption/config/templates/symmetric-encryption.yml +0 -50
- data/lib/rails/generators/symmetric_encryption/heroku_config/heroku_config_generator.rb +0 -20
- data/lib/rails/generators/symmetric_encryption/heroku_config/templates/symmetric-encryption.yml +0 -78
- data/lib/rails/generators/symmetric_encryption/new_keys/new_keys_generator.rb +0 -14
- data/lib/symmetric_encryption/key_encryption_key.rb +0 -32
- data/lib/symmetric_encryption/railties/symmetric_encryption.rake +0 -84
- 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
|