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 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