symmetric-encryption 1.0.0 → 1.1.1
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 +22 -0
- data/lib/rails/generators/symmetric_encryption/config/templates/symmetric-encryption.yml +7 -5
- data/lib/symmetric_encryption/cipher.rb +164 -16
- data/lib/symmetric_encryption/railties/symmetric_encryption.rake +47 -0
- data/lib/symmetric_encryption/reader.rb +20 -21
- data/lib/symmetric_encryption/symmetric_encryption.rb +11 -8
- data/lib/symmetric_encryption/version.rb +1 -1
- data/lib/symmetric_encryption/writer.rb +27 -21
- data/nbproject/private/private.xml +4 -4
- data/nbproject/private/rake-d.txt +4 -4
- data/test/cipher_test.rb +20 -0
- data/test/config/symmetric-encryption.yml +2 -0
- data/test/reader_test.rb +68 -3
- data/test/symmetric_encryption_test.rb +28 -2
- data/test/test_db.sqlite3 +0 -0
- data/test/writer_test.rb +4 -4
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 94fbb4339aaed37c7526621cf7dd768aaae56fb7
|
4
|
+
data.tar.gz: bc40aa329637465ca0747be1efac9a29a9154007
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04f23adcb3fbdf114a28a7460c9e120aa33fbb3d5897b2adf7700b559cf4fa78ae8cebb7b972383816c9a4b1f5202fead4abb5d87e1335cc65cdb6020169e6bc
|
7
|
+
data.tar.gz: bce09dff95ff11023aab38965f02a12f1ae6c49069256c54963b7db8bc5dd0889680c878d4b906b1f603599dbf9431d4239c987004d13ee00c5751420793f444
|
data/README.md
CHANGED
@@ -12,6 +12,9 @@ in configuration files have to be encrypted
|
|
12
12
|
This Gem helps achieve compliance by supporting encryption of data in a simple
|
13
13
|
and consistent way
|
14
14
|
|
15
|
+
Symmetric Encryption uses OpenSSL to encrypt and decrypt data, and can therefore
|
16
|
+
expose all the encryption algorithms supported by OpenSSL.
|
17
|
+
|
15
18
|
## Security
|
16
19
|
|
17
20
|
Many solutions that encrypt data require the encryption keys to be stored in the
|
@@ -239,6 +242,25 @@ Encrypt a known value, such as a password:
|
|
239
242
|
Note: Passwords must be encrypted in the environment in which they will be used.
|
240
243
|
Since each environment should have its own symmetric encryption keys
|
241
244
|
|
245
|
+
Encrypt a file
|
246
|
+
|
247
|
+
INFILE="Gemfile.lock" OUTFILE="Gemfile.lock.encrypted" rake symmetric_encryption:encrypt_file
|
248
|
+
|
249
|
+
Encrypt and compress a file
|
250
|
+
|
251
|
+
INFILE="Gemfile.lock" OUTFILE="Gemfile.lock.encrypted" COMPRESS=1 rake symmetric_encryption:encrypt_file
|
252
|
+
|
253
|
+
Decrypt a file encrypted and optionally compressed using symmetric encryption
|
254
|
+
|
255
|
+
INFILE="Gemfile.lock.encrypted" OUTFILE="Gemfile.lock2" rake symmetric_encryption:decrypt_file
|
256
|
+
|
257
|
+
When decrypting a compressed file it is not necessary to specify whether the file was compressed
|
258
|
+
since the header embedded in the file will indicate whether it was compressed
|
259
|
+
|
260
|
+
The file header also contains a random key and iv used to encrypt the files contents.
|
261
|
+
The key and iv is encrypted with the global encryption key being used by the symmetric
|
262
|
+
encryption installation.
|
263
|
+
|
242
264
|
## Installation
|
243
265
|
|
244
266
|
### Add to an existing Rails project
|
@@ -6,9 +6,9 @@
|
|
6
6
|
# can be placed directly in the source code.
|
7
7
|
# And therefore no RSA private key is required
|
8
8
|
development: &development_defaults
|
9
|
-
key:
|
10
|
-
iv:
|
11
|
-
cipher:
|
9
|
+
key: 1234567890ABCDEF1234567890ABCDEF
|
10
|
+
iv: 1234567890ABCDEF
|
11
|
+
cipher: aes-128-cbc
|
12
12
|
|
13
13
|
test:
|
14
14
|
<<: *development_defaults
|
@@ -29,7 +29,8 @@ release:
|
|
29
29
|
iv_filename: <%= File.join(key_path, "#{app_name}_release.iv") %>
|
30
30
|
cipher: aes-256-cbc
|
31
31
|
# Base64 encode encrypted data without newlines
|
32
|
-
encoding: base64strict
|
32
|
+
encoding: :base64strict
|
33
|
+
version: 1
|
33
34
|
|
34
35
|
production:
|
35
36
|
# Since the key to encrypt and decrypt with must NOT be stored along with the
|
@@ -47,4 +48,5 @@ production:
|
|
47
48
|
iv_filename: <%= File.join(key_path, "#{app_name}_production.iv") %>
|
48
49
|
cipher: aes-256-cbc
|
49
50
|
# Base64 encode encrypted data without newlines
|
50
|
-
encoding: base64strict
|
51
|
+
encoding: :base64strict
|
52
|
+
version: 1
|
@@ -7,7 +7,7 @@ module SymmetricEncryption
|
|
7
7
|
# threads at the same time without needing an instance of Cipher per thread
|
8
8
|
class Cipher
|
9
9
|
# Cipher to use for encryption and decryption
|
10
|
-
attr_reader :cipher, :version
|
10
|
+
attr_reader :cipher, :version
|
11
11
|
attr_accessor :encoding
|
12
12
|
|
13
13
|
# Available encodings
|
@@ -29,6 +29,17 @@ module SymmetricEncryption
|
|
29
29
|
}
|
30
30
|
end
|
31
31
|
|
32
|
+
# Returns a new Cipher with a random key and iv
|
33
|
+
#
|
34
|
+
# The cipher and encoding used are from the global encryption cipher
|
35
|
+
#
|
36
|
+
def self.random_cipher(cipher=nil, encoding=nil)
|
37
|
+
global_cipher = SymmetricEncryption.cipher
|
38
|
+
options = random_key_pair(cipher || global_cipher.cipher)
|
39
|
+
options[:encoding] = encoding || global_cipher.encoding
|
40
|
+
new(options)
|
41
|
+
end
|
42
|
+
|
32
43
|
# Create a Symmetric::Key for encryption and decryption purposes
|
33
44
|
#
|
34
45
|
# Parameters:
|
@@ -61,11 +72,13 @@ module SymmetricEncryption
|
|
61
72
|
# :version [Fixnum]
|
62
73
|
# Optional. The version number of this encryption key
|
63
74
|
# Used by SymmetricEncryption to select the correct key when decrypting data
|
75
|
+
# Maximum value: 255
|
64
76
|
def initialize(parms={})
|
65
77
|
raise "Missing mandatory parameter :key" unless @key = parms[:key]
|
66
78
|
@iv = parms[:iv]
|
67
79
|
@cipher = parms[:cipher] || 'aes-256-cbc'
|
68
80
|
@version = parms[:version]
|
81
|
+
raise "Cipher version has a maximum of 255. #{@version} is too high" if @version.to_i > 255
|
69
82
|
@encoding = (parms[:encoding] || :base64).to_sym
|
70
83
|
|
71
84
|
raise("Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding)
|
@@ -80,9 +93,9 @@ module SymmetricEncryption
|
|
80
93
|
if defined?(Encoding)
|
81
94
|
def encrypt(str, encode = true)
|
82
95
|
return if str.nil?
|
83
|
-
|
84
|
-
return str if
|
85
|
-
encrypted = crypt(:encrypt,
|
96
|
+
str = str.to_s #.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
97
|
+
return str if str.empty?
|
98
|
+
encrypted = crypt(:encrypt, str)
|
86
99
|
encode ? self.encode(encrypted) : encrypted
|
87
100
|
end
|
88
101
|
else
|
@@ -107,18 +120,25 @@ module SymmetricEncryption
|
|
107
120
|
decoded = self.decode(str) if decode
|
108
121
|
return unless decoded
|
109
122
|
|
110
|
-
|
111
|
-
|
112
|
-
|
123
|
+
return decoded if decoded.empty?
|
124
|
+
crypt(:decrypt, decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns a binary decrypted string
|
128
|
+
def decrypt_binary(str, decode = true)
|
129
|
+
decoded = self.decode(str) if decode
|
130
|
+
return unless decoded
|
131
|
+
|
132
|
+
return decoded if decoded.empty?
|
133
|
+
crypt(:decrypt, decoded).force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
113
134
|
end
|
114
135
|
else
|
115
136
|
def decrypt(str, decode = true)
|
116
137
|
decoded = self.decode(str) if decode
|
117
138
|
return unless decoded
|
118
139
|
|
119
|
-
|
120
|
-
|
121
|
-
crypt(:decrypt, buf)
|
140
|
+
return decoded if decoded.empty?
|
141
|
+
crypt(:decrypt, decoded)
|
122
142
|
end
|
123
143
|
end
|
124
144
|
|
@@ -133,20 +153,24 @@ module SymmetricEncryption
|
|
133
153
|
::OpenSSL::Cipher::Cipher.new(@cipher).block_size
|
134
154
|
end
|
135
155
|
|
156
|
+
# Returns UTF8 encoded string after encoding the supplied Binary string
|
157
|
+
#
|
136
158
|
# Encode the supplied string using the encoding in this cipher instance
|
137
159
|
# Returns nil if the supplied string is nil
|
138
160
|
# Note: No encryption or decryption is performed
|
161
|
+
#
|
162
|
+
# Returned string is UTF8 encoded except for encoding :none
|
139
163
|
def encode(binary_string)
|
140
164
|
return unless binary_string
|
141
165
|
|
142
166
|
# Now encode data based on encoding setting
|
143
167
|
case encoding
|
144
168
|
when :base64
|
145
|
-
::Base64.encode64(binary_string)
|
169
|
+
::Base64.encode64(binary_string).force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
146
170
|
when :base64strict
|
147
|
-
::Base64.encode64(binary_string).gsub(/\n/, '')
|
171
|
+
::Base64.encode64(binary_string).gsub(/\n/, '').force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
148
172
|
when :base16
|
149
|
-
binary_string.to_s.unpack('H*').first
|
173
|
+
binary_string.to_s.unpack('H*').first.force_encoding(SymmetricEncryption::UTF8_ENCODING)
|
150
174
|
else
|
151
175
|
binary_string
|
152
176
|
end
|
@@ -154,19 +178,139 @@ module SymmetricEncryption
|
|
154
178
|
|
155
179
|
# Decode the supplied string using the encoding in this cipher instance
|
156
180
|
# Note: No encryption or decryption is performed
|
181
|
+
#
|
182
|
+
# Returned string is Binary encoded
|
157
183
|
def decode(encoded_string)
|
158
184
|
return unless encoded_string
|
159
185
|
|
160
186
|
case encoding
|
161
187
|
when :base64, :base64strict
|
162
|
-
::Base64.decode64(encoded_string)
|
188
|
+
::Base64.decode64(encoded_string).force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
163
189
|
when :base16
|
164
|
-
[encoded_string].pack('H*')
|
190
|
+
[encoded_string].pack('H*').force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
165
191
|
else
|
166
192
|
encoded_string
|
167
193
|
end
|
168
194
|
end
|
169
195
|
|
196
|
+
# Returns an Array with the first element being Symmetric Cipher that must
|
197
|
+
# be used to decrypt the data. The second element indicates whether the data
|
198
|
+
# must be decompressed after decryption
|
199
|
+
#
|
200
|
+
# If the buffer does not start with the Magic Header the global cipher will
|
201
|
+
# be returned
|
202
|
+
#
|
203
|
+
# The supplied buffer will be updated directly and will have the header
|
204
|
+
# portion removed
|
205
|
+
#
|
206
|
+
# Parameters
|
207
|
+
# buffer
|
208
|
+
# String to extract the header from if present
|
209
|
+
#
|
210
|
+
# default_version
|
211
|
+
# If no header is present, this is the default value for the version
|
212
|
+
# of the cipher to use
|
213
|
+
#
|
214
|
+
# default_compressed
|
215
|
+
# If no header is present, this is the default value for the compression
|
216
|
+
def self.parse_magic_header!(buffer, default_version=nil, default_compressed=false)
|
217
|
+
buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
218
|
+
return [SymmetricEncryption.cipher(default_version), default_compressed] unless buffer.start_with?(MAGIC_HEADER)
|
219
|
+
|
220
|
+
# Header includes magic header and version byte
|
221
|
+
# Remove header and extract flags
|
222
|
+
header, flags = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK)
|
223
|
+
compressed = (flags & 0b1000_0000_0000_0000) != 0
|
224
|
+
include_iv = (flags & 0b0100_0000_0000_0000) != 0
|
225
|
+
include_key = (flags & 0b0010_0000_0000_0000) != 0
|
226
|
+
include_cipher= (flags & 0b0001_0000_0000_0000) != 0
|
227
|
+
version = flags & 0b0000_0000_1111_1111
|
228
|
+
decryption_cipher = SymmetricEncryption.cipher(version)
|
229
|
+
raise "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers" unless decryption_cipher
|
230
|
+
iv, key, cipher = nil
|
231
|
+
|
232
|
+
if include_iv
|
233
|
+
len = buffer.slice!(0..1).unpack('v').first
|
234
|
+
iv = buffer.slice!(0..len-1)
|
235
|
+
end
|
236
|
+
if include_key
|
237
|
+
len = buffer.slice!(0..1).unpack('v').first
|
238
|
+
key = decryption_cipher.send(:crypt, :decrypt, buffer.slice!(0..len-1))
|
239
|
+
end
|
240
|
+
if include_cipher
|
241
|
+
len = buffer.slice!(0..1).unpack('v').first
|
242
|
+
cipher = buffer.slice!(0..len-1)
|
243
|
+
end
|
244
|
+
|
245
|
+
if iv || key || cipher
|
246
|
+
decryption_cipher = SymmetricEncryption::Cipher.new(
|
247
|
+
:iv => iv,
|
248
|
+
:key => key || decryption_cipher.key,
|
249
|
+
:cipher => cipher || decryption_cipher.cipher
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
[decryption_cipher, compressed]
|
254
|
+
end
|
255
|
+
|
256
|
+
# Returns a magic header for this cipher instance that can be placed at
|
257
|
+
# the beginning of a file or stream to indicate how the data was encrypted
|
258
|
+
#
|
259
|
+
# Parameters
|
260
|
+
# compressed
|
261
|
+
# Sets the compressed indicator in the header
|
262
|
+
#
|
263
|
+
# include_iv
|
264
|
+
# Includes the encrypted Initialization Vector from this cipher if present
|
265
|
+
# The IV is encrypted using the global encryption key
|
266
|
+
#
|
267
|
+
# include_key
|
268
|
+
# Includes the encrypted Key in this cipher
|
269
|
+
# The key is encrypted using the global encryption key
|
270
|
+
#
|
271
|
+
# include_cipher
|
272
|
+
# Includes the cipher used. For example 'aes-256-cbc'
|
273
|
+
#
|
274
|
+
# encryption_cipher
|
275
|
+
# Encryption cipher to use when encrypting the iv and key.
|
276
|
+
# When supplied, the version is set to it's version so that decryption
|
277
|
+
# knows which cipher to use
|
278
|
+
# Default: Global cipher: SymmetricEncryption.cipher
|
279
|
+
def magic_header(compressed=false, include_iv=false, include_key=false, include_cipher=false, encryption_cipher=nil)
|
280
|
+
# Ruby V2 named parameters would be perfect here
|
281
|
+
|
282
|
+
# Encryption version indicator if available
|
283
|
+
flags = version || 0 # Same as 0b0000_0000_0000_0000
|
284
|
+
|
285
|
+
# Replace version with cipher used to encrypt Random IV and Key
|
286
|
+
if include_iv || include_key
|
287
|
+
encryption_cipher ||= SymmetricEncryption.cipher
|
288
|
+
flags = (encryption_cipher.version || 0)
|
289
|
+
end
|
290
|
+
|
291
|
+
# If the data is to be compressed before being encrypted, set the
|
292
|
+
# compressed bit in the flags word
|
293
|
+
flags |= 0b1000_0000_0000_0000 if compressed
|
294
|
+
flags |= 0b0100_0000_0000_0000 if @iv && include_iv
|
295
|
+
flags |= 0b0010_0000_0000_0000 if include_key
|
296
|
+
flags |= 0b0001_0000_0000_0000 if include_cipher
|
297
|
+
header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
298
|
+
if @iv && include_iv
|
299
|
+
header << [@iv.length].pack('v')
|
300
|
+
header << @iv
|
301
|
+
end
|
302
|
+
if include_key
|
303
|
+
encrypted = encryption_cipher.crypt(:encrypt, @key).force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
304
|
+
header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
305
|
+
header << encrypted
|
306
|
+
end
|
307
|
+
if include_cipher
|
308
|
+
header << [cipher.length].pack('v')
|
309
|
+
header << cipher
|
310
|
+
end
|
311
|
+
header
|
312
|
+
end
|
313
|
+
|
170
314
|
protected
|
171
315
|
|
172
316
|
# Only for use by Symmetric::EncryptedStream
|
@@ -188,8 +332,12 @@ module SymmetricEncryption
|
|
188
332
|
openssl_cipher.iv = @iv if @iv
|
189
333
|
result = openssl_cipher.update(string)
|
190
334
|
result << openssl_cipher.final
|
335
|
+
result.force_encoding(SymmetricEncryption::BINARY_ENCODING)
|
191
336
|
end
|
192
337
|
|
193
|
-
|
338
|
+
private
|
194
339
|
|
340
|
+
attr_reader :key, :iv
|
341
|
+
|
342
|
+
end
|
195
343
|
end
|
@@ -30,4 +30,51 @@ namespace :symmetric_encryption do
|
|
30
30
|
puts "Encrypted: #{SymmetricEncryption.encrypt(p)}\n\n"
|
31
31
|
end
|
32
32
|
|
33
|
+
desc 'Decrypt a file. Example: INFILE="encrypted_filename" OUTFILE="filename" rake symmetric_encryption:decrypt_file'
|
34
|
+
task :decrypt_file => :environment do
|
35
|
+
input_filename = ENV['INFILE']
|
36
|
+
output_filename = ENV['OUTFILE']
|
37
|
+
block_size = ENV['BLOCKSIZE'] || 65535
|
38
|
+
|
39
|
+
if input_filename && output_filename
|
40
|
+
puts "\nDecrypting file: #{input_filename} and writing to: #{output_filename}\n\n"
|
41
|
+
::File.open(output_filename, 'wb') do |output_file|
|
42
|
+
SymmetricEncryption::Reader.open(input_filename) do |input_file|
|
43
|
+
while !input_file.eof?
|
44
|
+
output_file.write(input_file.read(block_size))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
puts "\n#{output_filename} now contains the decrypted contents of #{input_filename}\n\n"
|
49
|
+
else
|
50
|
+
puts "Missing input and/or output filename. Usage:"
|
51
|
+
puts ' INFILE="encrypted_filename" OUTFILE="filename" rake symmetric_encryption:decrypt_file'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'Encrypt a file. Example: INFILE="filename" OUTFILE="encrypted_filename" rake symmetric_encryption:encrypt_file'
|
56
|
+
task :encrypt_file => :environment do
|
57
|
+
input_filename = ENV['INFILE']
|
58
|
+
output_filename = ENV['OUTFILE']
|
59
|
+
compress = (ENV['COMPRESS'] != nil)
|
60
|
+
block_size = ENV['BLOCKSIZE'] || 65535
|
61
|
+
|
62
|
+
if input_filename && output_filename
|
63
|
+
puts "\nEncrypting file: #{input_filename} and writing to: #{output_filename}\n\n"
|
64
|
+
::File.open(input_filename, 'rb') do |input_file|
|
65
|
+
SymmetricEncryption::Writer.open(output_filename, :compress => compress) do |output_file|
|
66
|
+
while !input_file.eof?
|
67
|
+
output_file.write(input_file.read(block_size))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
puts "\n#{output_filename} now contains the encrypted #{"and compressed " if compress}contents of #{input_filename}\n\n"
|
72
|
+
else
|
73
|
+
puts "Missing input and/or output filename. Usage:"
|
74
|
+
puts ' INFILE="filename" OUTFILE="encrypted_filename" rake symmetric_encryption:encrypt_file'
|
75
|
+
puts "To compress the file before encrypting:"
|
76
|
+
puts ' COMPRESS=1 INFILE="filename" OUTFILE="encrypted_filename" rake symmetric_encryption:encrypt_file'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
33
80
|
end
|
@@ -14,6 +14,17 @@ module SymmetricEncryption
|
|
14
14
|
# avoid having the stream closed automatically
|
15
15
|
#
|
16
16
|
# options:
|
17
|
+
# :mode
|
18
|
+
# See File.open for open modes
|
19
|
+
# Default: 'rb'
|
20
|
+
#
|
21
|
+
# :buffer_size
|
22
|
+
# Amount of data to read at a time
|
23
|
+
# The buffer size must be at least large enough to hold the entire
|
24
|
+
# magic header if one is present
|
25
|
+
# Default: 4096
|
26
|
+
#
|
27
|
+
# The following options are only used if the stream/file has no header
|
17
28
|
# :compress [true|false]
|
18
29
|
# Uses Zlib to decompress the data after it is decrypted
|
19
30
|
# Note: This option is only used if the file does not have a header
|
@@ -25,14 +36,6 @@ module SymmetricEncryption
|
|
25
36
|
# file/stream does not include a header at the beginning
|
26
37
|
# Default: Current primary key
|
27
38
|
#
|
28
|
-
# :mode
|
29
|
-
# See File.open for open modes
|
30
|
-
# Default: 'r'
|
31
|
-
#
|
32
|
-
# :buffer_size
|
33
|
-
# Amount of data to read at a time
|
34
|
-
# Default: 4096
|
35
|
-
#
|
36
39
|
# Note: Decryption occurs before decompression
|
37
40
|
#
|
38
41
|
# # Example: Read and decrypt a line at a time from a file
|
@@ -74,9 +77,9 @@ module SymmetricEncryption
|
|
74
77
|
# end
|
75
78
|
def self.open(filename_or_stream, options={}, &block)
|
76
79
|
raise "options must be a hash" unless options.respond_to?(:each_pair)
|
77
|
-
mode
|
80
|
+
mode = options.fetch(:mode, 'rb')
|
78
81
|
compress = options.fetch(:compress, false)
|
79
|
-
ios
|
82
|
+
ios = filename_or_stream.is_a?(String) ? ::File.open(filename_or_stream, mode) : filename_or_stream
|
80
83
|
|
81
84
|
begin
|
82
85
|
file = self.new(ios, options)
|
@@ -91,7 +94,7 @@ module SymmetricEncryption
|
|
91
94
|
def initialize(ios,options={})
|
92
95
|
@ios = ios
|
93
96
|
@buffer_size = options.fetch(:buffer_size, 4096).to_i
|
94
|
-
@version
|
97
|
+
@version = options[:version]
|
95
98
|
read_header
|
96
99
|
end
|
97
100
|
|
@@ -288,17 +291,13 @@ module SymmetricEncryption
|
|
288
291
|
|
289
292
|
# Read first block and check for the header
|
290
293
|
buf = @ios.read(@buffer_size)
|
291
|
-
if buf.start_with?(MAGIC_HEADER)
|
292
|
-
# Header includes magic header and version byte
|
293
|
-
# Remove header and extract flags
|
294
|
-
header, flags = buf.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK)
|
295
|
-
@compressed = (flags & 0b1000_0000_0000_0000) != 0
|
296
|
-
@version = @compressed ? flags - 0b1000_0000_0000_0000 : flags
|
297
|
-
end
|
298
294
|
|
299
|
-
# Use
|
300
|
-
@cipher = SymmetricEncryption.
|
301
|
-
|
295
|
+
# Use cipher specified in header, or global cipher if it has no header
|
296
|
+
@cipher, @compressed = SymmetricEncryption::Cipher.parse_magic_header!(buf, @version)
|
297
|
+
|
298
|
+
# Use supplied version if cipher could not be detected due to missing header
|
299
|
+
@cipher ||= SymmetricEncryption.cipher(@version)
|
300
|
+
|
302
301
|
@stream_cipher = @cipher.send(:openssl_cipher, :decrypt)
|
303
302
|
|
304
303
|
# First call to #update should return an empty string anyway
|
@@ -22,17 +22,18 @@ module SymmetricEncryption
|
|
22
22
|
# :cipher => 'aes-128-cbc'
|
23
23
|
# )
|
24
24
|
def self.cipher=(cipher)
|
25
|
-
raise "Cipher must be similar to SymmetricEncryption::Ciphers" unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt)
|
25
|
+
raise "Cipher must be similar to SymmetricEncryption::Ciphers" unless cipher.nil? || (cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt))
|
26
26
|
@@cipher = cipher
|
27
27
|
end
|
28
28
|
|
29
29
|
# Returns the Primary Symmetric Cipher being used
|
30
|
-
# If a version is supplied
|
31
|
-
#
|
32
|
-
|
30
|
+
# If a version is supplied
|
31
|
+
# Returns the primary cipher if no match was found and version == 0
|
32
|
+
# Returns nil if no match was found and version != 0
|
33
|
+
def self.cipher(version = nil)
|
33
34
|
raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher
|
34
|
-
return @@cipher if version.nil? || (
|
35
|
-
secondary_ciphers.find {|c| c.version == version}
|
35
|
+
return @@cipher if version.nil? || (@@cipher.version == version)
|
36
|
+
secondary_ciphers.find {|c| c.version == version} || (@@cipher if version == 0)
|
36
37
|
end
|
37
38
|
|
38
39
|
# Set the Secondary Symmetric Ciphers Array to be used
|
@@ -227,7 +228,8 @@ module SymmetricEncryption
|
|
227
228
|
:cipher => cipher_cfg['cipher'] || default_cipher,
|
228
229
|
:key_filename => key_filename,
|
229
230
|
:iv_filename => iv_filename,
|
230
|
-
:encoding => cipher_cfg['encoding']
|
231
|
+
:encoding => cipher_cfg['encoding'],
|
232
|
+
:version => cipher_cfg['version']
|
231
233
|
}
|
232
234
|
end
|
233
235
|
|
@@ -287,7 +289,8 @@ module SymmetricEncryption
|
|
287
289
|
:key => rsa.private_decrypt(encrypted_key),
|
288
290
|
:iv => iv,
|
289
291
|
:cipher => cipher_conf[:cipher],
|
290
|
-
:encoding => cipher_conf[:encoding]
|
292
|
+
:encoding => cipher_conf[:encoding],
|
293
|
+
:version => cipher_conf[:version]
|
291
294
|
)
|
292
295
|
end
|
293
296
|
|
@@ -20,8 +20,20 @@ module SymmetricEncryption
|
|
20
20
|
# :compress [true|false]
|
21
21
|
# Uses Zlib to compress the data before it is encrypted and
|
22
22
|
# written to the file
|
23
|
+
# If true, it forces header to true.
|
23
24
|
# Default: false
|
24
25
|
#
|
26
|
+
# :random_key [true|false]
|
27
|
+
# Generates a new random key and iv for every new file or stream
|
28
|
+
# If true, it forces header to true. Version below then has no effect
|
29
|
+
# The Random key and iv will be written to the file/stream in encrypted
|
30
|
+
# form as part of the header
|
31
|
+
# The key and iv are both encrypted using the global key
|
32
|
+
# Default: true
|
33
|
+
# Recommended: true. Setting to false will eventually expose the
|
34
|
+
# encryption key since too much data will be encrypted using the
|
35
|
+
# same encryption key
|
36
|
+
#
|
25
37
|
# :header [true|false]
|
26
38
|
# Whether to include the magic header that indicates the file
|
27
39
|
# is encrypted and whether its contents are compressed
|
@@ -32,7 +44,11 @@ module SymmetricEncryption
|
|
32
44
|
# Default: true
|
33
45
|
#
|
34
46
|
# :version
|
35
|
-
#
|
47
|
+
# When random_key is true, the version of the encryption key to use
|
48
|
+
# when encrypting the header portion of the file
|
49
|
+
#
|
50
|
+
# When random_key is false, the version of the encryption key to use
|
51
|
+
# to encrypt the entire file
|
36
52
|
# Default: Current primary key
|
37
53
|
#
|
38
54
|
# :mode
|
@@ -81,18 +97,22 @@ module SymmetricEncryption
|
|
81
97
|
|
82
98
|
# Encrypt data before writing to the supplied stream
|
83
99
|
def initialize(ios,options={})
|
84
|
-
@ios
|
85
|
-
header
|
100
|
+
@ios = ios
|
101
|
+
header = options.fetch(:header, true)
|
86
102
|
# Compress is only used at this point for setting the flag in the header
|
87
|
-
|
103
|
+
random_key = options.fetch(:random_key, true)
|
104
|
+
compress = options.fetch(:compress, false)
|
105
|
+
# Force header if compressed or using random iv, key pair
|
106
|
+
header = true if compress || random_key
|
88
107
|
|
89
|
-
#
|
90
|
-
@cipher
|
108
|
+
# Create random cipher or use global primary cipher
|
109
|
+
@cipher = random_key ? SymmetricEncryption::Cipher.random_cipher : SymmetricEncryption.cipher(options[:version])
|
91
110
|
raise "Cipher with version:#{options[:version]} not found in any of the configured SymmetricEncryption ciphers" unless @cipher
|
92
111
|
|
93
112
|
@stream_cipher = @cipher.send(:openssl_cipher, :encrypt)
|
94
113
|
|
95
|
-
|
114
|
+
# Write the Encryption header including the random iv, key, and cipher
|
115
|
+
@ios.write(@cipher.magic_header(compress, random_key, random_key, random_key)) if header
|
96
116
|
end
|
97
117
|
|
98
118
|
# Close the IO Stream
|
@@ -139,19 +159,5 @@ module SymmetricEncryption
|
|
139
159
|
@ios.flush
|
140
160
|
end
|
141
161
|
|
142
|
-
private
|
143
|
-
|
144
|
-
# Write the Encryption header if this is the first write
|
145
|
-
def write_header
|
146
|
-
# Include Header and encryption version indicator
|
147
|
-
flags = @cipher.version || 0 # Same as 0b0000_0000_0000_0000
|
148
|
-
|
149
|
-
# If the data is to be compressed before being encrypted, set the
|
150
|
-
# compressed bit in the version byte
|
151
|
-
flags |= 0b1000_0000_0000_0000 if @compress
|
152
|
-
|
153
|
-
@ios.write "#{MAGIC_HEADER}#{[flags].pack('v')}"
|
154
|
-
end
|
155
|
-
|
156
162
|
end
|
157
163
|
end
|
@@ -5,13 +5,13 @@
|
|
5
5
|
<url>lib/symmetric/encryption.rb</url>
|
6
6
|
<line>62</line>
|
7
7
|
</file>
|
8
|
-
<file>
|
9
|
-
<url>lib/symmetric_encryption/symmetric_encryption.rb</url>
|
10
|
-
<line>75</line>
|
11
|
-
</file>
|
12
8
|
<file>
|
13
9
|
<url>lib/symmetric_encryption/encryption.rb</url>
|
14
10
|
<line>60</line>
|
15
11
|
</file>
|
12
|
+
<file>
|
13
|
+
<url>lib/symmetric_encryption/symmetric_encryption.rb</url>
|
14
|
+
<line>76</line>
|
15
|
+
</file>
|
16
16
|
</editor-bookmarks>
|
17
17
|
</project-private>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
clean=
|
2
|
-
clobber=
|
3
|
-
gem=
|
4
|
-
test=
|
1
|
+
clean=Remove any temporary products.
|
2
|
+
clobber=Remove any generated file.
|
3
|
+
gem=Build gem
|
4
|
+
test=Run Test Suite
|
data/test/cipher_test.rb
CHANGED
@@ -6,6 +6,9 @@ require 'test/unit'
|
|
6
6
|
require 'shoulda'
|
7
7
|
require 'symmetric_encryption'
|
8
8
|
|
9
|
+
# Load Symmetric Encryption keys
|
10
|
+
SymmetricEncryption.load!(File.join(File.dirname(__FILE__), 'config', 'symmetric-encryption.yml'), 'test')
|
11
|
+
|
9
12
|
# Unit Test for SymmetricEncryption::Cipher
|
10
13
|
#
|
11
14
|
class CipherTest < Test::Unit::TestCase
|
@@ -90,5 +93,22 @@ class CipherTest < Test::Unit::TestCase
|
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
96
|
+
context "magic header" do
|
97
|
+
|
98
|
+
should "create and parse magic header" do
|
99
|
+
random_cipher = SymmetricEncryption::Cipher.random_cipher
|
100
|
+
header = random_cipher.magic_header(compressed=true, include_iv=true, include_key=true, include_cipher=true)
|
101
|
+
cipher, compressed = SymmetricEncryption::Cipher.parse_magic_header!(header)
|
102
|
+
assert_equal true, compressed
|
103
|
+
assert_equal random_cipher.cipher, cipher.cipher, "Ciphers differ"
|
104
|
+
assert_equal random_cipher.send(:key), cipher.send(:key), "Keys differ"
|
105
|
+
assert_equal random_cipher.send(:iv), cipher.send(:iv), "IVs differ"
|
106
|
+
|
107
|
+
string = "Hellow World"
|
108
|
+
# Test Encryption
|
109
|
+
assert_equal random_cipher.encrypt(string, false), cipher.encrypt(string, false), "Encrypted values differ"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
93
113
|
end
|
94
114
|
end
|
@@ -41,6 +41,7 @@ test:
|
|
41
41
|
cipher: aes-128-cbc
|
42
42
|
# Base64 encode encrypted data without newlines
|
43
43
|
encoding: base64strict
|
44
|
+
version: 1
|
44
45
|
|
45
46
|
# Previous Symmetric Encryption Key
|
46
47
|
- key_filename: /Users/rmorrison/Sandbox/symmetric-encryption/test/config/test_secondary_1.key
|
@@ -48,4 +49,5 @@ test:
|
|
48
49
|
cipher: aes-128-cbc
|
49
50
|
# Base64 encode encrypted data without newlines
|
50
51
|
encoding: base64
|
52
|
+
version: 0
|
51
53
|
|
data/test/reader_test.rb
CHANGED
@@ -13,7 +13,7 @@ SymmetricEncryption.load!(File.join(File.dirname(__FILE__), 'config', 'symmetric
|
|
13
13
|
# Unit Test for SymmetricEncrypted::ReaderStream
|
14
14
|
#
|
15
15
|
class ReaderTest < Test::Unit::TestCase
|
16
|
-
context
|
16
|
+
context SymmetricEncryption::Reader do
|
17
17
|
setup do
|
18
18
|
@data = [
|
19
19
|
"Hello World\n",
|
@@ -70,7 +70,7 @@ class ReaderTest < Test::Unit::TestCase
|
|
70
70
|
|
71
71
|
context "reading from file" do
|
72
72
|
# With and without header
|
73
|
-
[{:header => false}, {:compress => false}, {:compress => true}].each_with_index do |options, i|
|
73
|
+
[{:header => false, :version => 1}, {:header => false, :random_key => false, :version => 1}, {:compress => false}, {:compress => true}, {:random_key => false}].each_with_index do |options, i|
|
74
74
|
context "with#{'out' unless options[:header]} header #{i}" do
|
75
75
|
setup do
|
76
76
|
@filename = '._test'
|
@@ -112,5 +112,70 @@ class ReaderTest < Test::Unit::TestCase
|
|
112
112
|
end
|
113
113
|
|
114
114
|
end
|
115
|
+
|
116
|
+
context "reading from files with previous keys" do
|
117
|
+
setup do
|
118
|
+
@filename = '._test'
|
119
|
+
# Create encrypted file with old encryption key
|
120
|
+
SymmetricEncryption::Writer.open(@filename, :version => 0) do |file|
|
121
|
+
@data.inject(0) {|sum,str| sum + file.write(str)}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
teardown do
|
126
|
+
File.delete(@filename) if File.exist?(@filename)
|
127
|
+
end
|
128
|
+
|
129
|
+
should "decrypt from file in a single read" do
|
130
|
+
decrypted = SymmetricEncryption::Reader.open(@filename) {|file| file.read}
|
131
|
+
assert_equal @data_str, decrypted
|
132
|
+
end
|
133
|
+
|
134
|
+
should "decrypt from file a line at a time" do
|
135
|
+
decrypted = SymmetricEncryption::Reader.open(@filename) do |file|
|
136
|
+
i = 0
|
137
|
+
file.each_line do |line|
|
138
|
+
assert_equal @data[i], line
|
139
|
+
i += 1
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
should "support rewind" do
|
145
|
+
decrypted = SymmetricEncryption::Reader.open(@filename) do |file|
|
146
|
+
file.read
|
147
|
+
file.rewind
|
148
|
+
file.read
|
149
|
+
end
|
150
|
+
assert_equal @data_str, decrypted
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "reading from files with previous keys without a header" do
|
155
|
+
setup do
|
156
|
+
@filename = '._test'
|
157
|
+
# Create encrypted file with old encryption key
|
158
|
+
SymmetricEncryption::Writer.open(@filename, :version => 0, :header => false, :random_key => false) do |file|
|
159
|
+
@data.inject(0) {|sum,str| sum + file.write(str)}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
teardown do
|
164
|
+
File.delete(@filename) if File.exist?(@filename)
|
165
|
+
end
|
166
|
+
|
167
|
+
should "decrypt from file in a single read" do
|
168
|
+
decrypted = SymmetricEncryption::Reader.open(@filename, :version => 0) {|file| file.read}
|
169
|
+
assert_equal @data_str, decrypted
|
170
|
+
end
|
171
|
+
|
172
|
+
should "decrypt from file in a single read with different version" do
|
173
|
+
# Should fail since file was encrypted using version 0 key
|
174
|
+
assert_raise OpenSSL::Cipher::CipherError do
|
175
|
+
SymmetricEncryption::Reader.open(@filename, :version => 1) {|file| file.read}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
115
180
|
end
|
116
|
-
end
|
181
|
+
end
|
@@ -17,10 +17,36 @@ class SymmetricEncryptionTest < Test::Unit::TestCase
|
|
17
17
|
context 'configuration' do
|
18
18
|
setup do
|
19
19
|
@config = SymmetricEncryption.send(:read_config, File.join(File.dirname(__FILE__), 'config', 'symmetric-encryption.yml'), 'test')
|
20
|
+
assert @cipher_v1 = @config[:ciphers][0]
|
21
|
+
assert @cipher_v0 = @config[:ciphers][1]
|
20
22
|
end
|
21
23
|
|
22
|
-
should "match config file" do
|
23
|
-
|
24
|
+
should "match config file for first cipher" do
|
25
|
+
cipher = SymmetricEncryption.cipher
|
26
|
+
assert_equal @cipher_v1[:cipher], cipher.cipher
|
27
|
+
assert_equal @cipher_v1[:version], cipher.version
|
28
|
+
assert_equal false, SymmetricEncryption.secondary_ciphers.include?(cipher)
|
29
|
+
end
|
30
|
+
|
31
|
+
should "match config file for v1 cipher" do
|
32
|
+
cipher = SymmetricEncryption.cipher(1)
|
33
|
+
assert @cipher_v1[:cipher]
|
34
|
+
assert @cipher_v1[:version]
|
35
|
+
assert_equal @cipher_v1[:cipher], cipher.cipher
|
36
|
+
assert_equal @cipher_v1[:version], cipher.version
|
37
|
+
assert_equal false, SymmetricEncryption.secondary_ciphers.include?(cipher)
|
38
|
+
end
|
39
|
+
|
40
|
+
should "match config file for v0 cipher" do
|
41
|
+
cipher = SymmetricEncryption.cipher(0)
|
42
|
+
assert @cipher_v0[:cipher]
|
43
|
+
assert @cipher_v0[:version]
|
44
|
+
assert_equal @cipher_v0[:cipher], cipher.cipher
|
45
|
+
assert_equal @cipher_v0[:version], cipher.version
|
46
|
+
assert_equal true, SymmetricEncryption.secondary_ciphers.include?(cipher)
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'read ciphers from config file' do
|
24
50
|
end
|
25
51
|
end
|
26
52
|
|
data/test/test_db.sqlite3
CHANGED
Binary file
|
data/test/writer_test.rb
CHANGED
@@ -12,8 +12,8 @@ SymmetricEncryption.load!(File.join(File.dirname(__FILE__), 'config', 'symmetric
|
|
12
12
|
|
13
13
|
# Unit Test for Symmetric::EncryptedStream
|
14
14
|
#
|
15
|
-
class
|
16
|
-
context
|
15
|
+
class WriterTest < Test::Unit::TestCase
|
16
|
+
context SymmetricEncryption::Writer do
|
17
17
|
setup do
|
18
18
|
@data = [
|
19
19
|
"Hello World\n",
|
@@ -32,7 +32,7 @@ class EncryptionWriterTest < Test::Unit::TestCase
|
|
32
32
|
|
33
33
|
should "encrypt to string stream" do
|
34
34
|
stream = StringIO.new
|
35
|
-
file = SymmetricEncryption::Writer.new(stream, :header => false)
|
35
|
+
file = SymmetricEncryption::Writer.new(stream, :header => false, :random_key => false)
|
36
36
|
written_len = @data.inject(0) {|sum,str| sum + file.write(str)}
|
37
37
|
file.close
|
38
38
|
|
@@ -53,7 +53,7 @@ class EncryptionWriterTest < Test::Unit::TestCase
|
|
53
53
|
|
54
54
|
should "encrypt to file using .open" do
|
55
55
|
written_len = nil
|
56
|
-
SymmetricEncryption::Writer.open(@filename, :header => false) do |file|
|
56
|
+
SymmetricEncryption::Writer.open(@filename, :header => false, :random_key => false) do |file|
|
57
57
|
written_len = @data.inject(0) {|sum,str| sum + file.write(str)}
|
58
58
|
end
|
59
59
|
assert_equal @data_len, written_len
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: symmetric-encryption
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Reid Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: SymmetricEncryption supports encrypting ActiveRecord data, Mongoid data,
|
14
14
|
passwords in configuration files, encrypting and decrypting of large files through
|