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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ce8b40ac71f3b255ea4947fe3a41ba51145b5188
4
- data.tar.gz: de4ceb7712388671b92054ed66409e78ddcf21bd
3
+ metadata.gz: 94fbb4339aaed37c7526621cf7dd768aaae56fb7
4
+ data.tar.gz: bc40aa329637465ca0747be1efac9a29a9154007
5
5
  SHA512:
6
- metadata.gz: 83166719c8132a10e4108c6d8e3822b61dc02be4bcd0159a47d44a6f23ebc914b8ab5cf2b979cd7a4fffc413f6d0a1aa0b6e7f211afa2e4031173f72cf2c5ef4
7
- data.tar.gz: 1c0917a0a784610aaa517b526374fed613883203bb0d9ff04f56c6182581af415f4b2b73adb81c15948e8a5e2c2ed17d83d5820f9ed47b75498c4863d79c8668
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: 1234567890ABCDEF1234567890ABCDEF
10
- iv: 1234567890ABCDEF
11
- cipher: aes-128-cbc
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, :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
- buf = str.to_s.encode(SymmetricEncryption::UTF8_ENCODING)
84
- return str if buf.empty?
85
- encrypted = crypt(:encrypt, buf)
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
- buf = decoded.to_s.force_encoding(SymmetricEncryption::BINARY_ENCODING)
111
- return decoded if buf.empty?
112
- crypt(:decrypt, buf).force_encoding(SymmetricEncryption::UTF8_ENCODING)
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
- buf = decoded.to_s
120
- return decoded if buf.empty?
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
- end
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 = options.fetch(:mode, 'rb')
80
+ mode = options.fetch(:mode, 'rb')
78
81
  compress = options.fetch(:compress, false)
79
- ios = filename_or_stream.is_a?(String) ? ::File.open(filename_or_stream, mode) : filename_or_stream
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 = options[: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 primary cipher by default, but allow a secondary cipher to be selected for encryption
300
- @cipher = SymmetricEncryption.cipher(@version)
301
- raise "Cipher with version:#{@version.inspect} not found in any of the configured SymmetricEncryption ciphers" unless @cipher
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, then the cipher matching that version will be
31
- # returned or nil if no match was found
32
- def self.cipher(version = 0)
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? || (version == 0) || (@@cipher.version == version)
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
 
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module SymmetricEncryption #:nodoc
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.1"
4
4
  end
@@ -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
- # Version of the encryption key to use when encrypting
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 = ios
85
- header = options.fetch(:header, true)
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
- @compress = options.fetch(:compress, false)
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
- # Use primary cipher by default, but allow a secondary cipher to be selected for encryption
90
- @cipher = SymmetricEncryption.cipher(options[:version])
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
- write_header if header
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 'Reader' do
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
- assert_equal @config[:ciphers][0][:cipher], SymmetricEncryption.cipher.cipher
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 EncryptionWriterTest < Test::Unit::TestCase
16
- context 'EncryptionWriter' do
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.0.0
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-03-07 00:00:00.000000000 Z
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