symmetric-encryption 3.9.1 → 4.0.0.beta3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|