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