symmetric-encryption 2.2.0 → 3.0.0

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: 21f271484936806efc343704e1bfac1392239fa0
4
- data.tar.gz: 53aff815835c31c9677eae87db94336cc72db0e5
3
+ metadata.gz: 8ef6c56bc1e2a9a0231a0f98f37b4cd4ca485842
4
+ data.tar.gz: 52318d2b70e96ecfc6067e35d8c7b3ca0a339933
5
5
  SHA512:
6
- metadata.gz: 89aab4b1b54a70e9e24023c074e03950b298376e6b97bc13a9a2597c5c6eaf1184ec7405ae6fe2ee898d31df7114589977a2d5e0cb2856ecd25641c186780f35
7
- data.tar.gz: 08e333431de1b029ced0888c182efe67ad09d1e33e5164cb99a9c9103b990e3f532daa95358ad1bb257faf7fbd41e881a6cb2644631b749271500e71d7b01faa
6
+ metadata.gz: 461b4904ad5b16293b4edb7f760486bbaf30a548a04debd3418c04a829d87e03f40db7a665e318e43435d140ed4fcbc07d45953158a23ac9e26c3dd725f894af
7
+ data.tar.gz: 100babba3863c0a0b4abcdb800e6b6afe6d56ee66e43c85ff9b5cdb957f931d7660ac462e73014c07b4754df927fb264f0b5d2c77b71da0e2ba88f623c6b68b3
data/README.md CHANGED
@@ -15,6 +15,30 @@ and consistent way
15
15
  Symmetric Encryption uses OpenSSL to encrypt and decrypt data, and can therefore
16
16
  expose all the encryption algorithms supported by OpenSSL.
17
17
 
18
+ ## Upgrading from earlier versions to SymmetricEncryption V3
19
+
20
+ In version 3 of SymmetricEncryption, the following changes have been made that
21
+ may have backward compatibility issues:
22
+
23
+ * SymmetricEncryption.decrypt no longer rotates through all the decryption keys
24
+ when previous ciphers fail to decrypt the encrypted string.
25
+ In a very small, yet significant number of cases it was possible to decrypt data
26
+ using the incorrect key. Clearly the data returned was garbage, but it still
27
+ returned a string of data instead of throwing an exception.
28
+ See SymmetricEncryption.select_cipher to supply your own custom logic to
29
+ determine the correct cipher to use when the encrypted string does not have a
30
+ header and multiple ciphers are defined.
31
+
32
+ * Configuration file format prior to V1 is no longer supported
33
+
34
+ * New configuration option has been added to support setting encryption keys
35
+ from environment variables
36
+
37
+ * Cipher.parse_magic_header! now returns a Struct instead of an Array
38
+
39
+ * New config options :encrypted_key and :encrypted_iv to support setting
40
+ the encryption key in environment variables
41
+
18
42
  ## Security
19
43
 
20
44
  Many solutions that encrypt data require the encryption keys to be stored in the
@@ -50,6 +74,12 @@ From a security perspective it is important then to properly secure the system s
50
74
  no hacker can switch to and run as the rails user and thereby gain access to the
51
75
  encryption and decryption capabilities
52
76
 
77
+ It is not necessary to encrypt the IV (initialization vector), and it can be placed
78
+ directly in the configuration file. The encryption key must be kept secure and
79
+ must never be placed in the configuration file or other Rails source file in production.
80
+ The IV should be generated using the rails generator described below to ensure
81
+ it is a truly random key from the key space.
82
+
53
83
  ## Limitations
54
84
 
55
85
  By default symmetric encryption uses the same initialization vector (IV) and
@@ -78,7 +108,7 @@ option will result in different encrypted output every time it is encrypted.
78
108
 
79
109
  * Encryption of passwords in configuration files
80
110
  * Encryption of ActiveRecord model attributes by prefixing attributes / column
81
- names with encrypted_
111
+ names with encrypted_
82
112
  * Encryption of Mongoid model fields by adding :encrypted option to field
83
113
  definitions
84
114
  * Externalization of symmetric encryption keys so that they are not in the
@@ -98,6 +128,14 @@ names with encrypted_
98
128
  * For maximum security supports fully random keys and initialization vectors
99
129
  extracted from the entire encryption key space
100
130
 
131
+ ## Recommendations
132
+
133
+ * Add the encryption header to all encrypted strings.
134
+ See the _always_add_header_ option in the configuration file.
135
+
136
+ * Set :random_iv => true for all ActiveRecord attributes and Mongoid fields
137
+ which are not used in indexes and will not be used as part of a query.
138
+
101
139
  ## Examples
102
140
 
103
141
  ### Encryption Example
data/Rakefile CHANGED
@@ -5,26 +5,11 @@ require 'rubygems'
5
5
  require 'rubygems/package'
6
6
  require 'rake/clean'
7
7
  require 'rake/testtask'
8
- require 'date'
9
8
  require 'symmetric_encryption/version'
10
9
 
11
10
  desc "Build gem"
12
11
  task :gem do |t|
13
- gemspec = Gem::Specification.new do |s|
14
- s.name = 'symmetric-encryption'
15
- s.version = SymmetricEncryption::VERSION
16
- s.platform = Gem::Platform::RUBY
17
- s.authors = ['Reid Morrison']
18
- s.email = ['reidmo@gmail.com']
19
- s.homepage = 'https://github.com/ClarityServices/symmetric-encryption'
20
- s.date = Date.today.to_s
21
- s.summary = "Symmetric Encryption for Ruby, and Ruby on Rails"
22
- s.description = "SymmetricEncryption supports encrypting ActiveRecord data, Mongoid data, passwords in configuration files, encrypting and decrypting of large files through streaming"
23
- s.files = FileList["./**/*"].exclude(/.gem$/, /.log$/,/^nbproject/).map{|f| f.sub(/^\.\//, '')}
24
- s.license = "Apache License V2.0"
25
- s.has_rdoc = true
26
- end
27
- Gem::Package.build gemspec
12
+ Gem::Package.build(Gem::Specification.load('symmetric-encryption.gemspec'))
28
13
  end
29
14
 
30
15
  desc "Run Test Suite"
@@ -34,5 +19,8 @@ task :test do
34
19
  t.verbose = true
35
20
  end
36
21
 
22
+ # For mongoid
23
+ ENV['RACK_ENV'] = 'test'
24
+
37
25
  Rake::Task['functional'].invoke
38
26
  end
@@ -7,8 +7,8 @@ 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_name, :version
11
- attr_accessor :encoding
10
+ attr_reader :cipher_name, :version, :iv
11
+ attr_accessor :encoding, :always_add_header
12
12
 
13
13
  # Available encodings
14
14
  ENCODINGS = [:none, :base64, :base64strict, :base16]
@@ -16,18 +16,29 @@ module SymmetricEncryption
16
16
  # Backward compatibility
17
17
  alias_method :cipher, :cipher_name
18
18
 
19
+ # Defines the Header Structure returned when parsing the header
20
+ HeaderStruct = Struct.new(
21
+ :compressed, # [true|false] Whether the data is compressed, if supplied in the header
22
+ :binary, # [true|false] Whether the data is binary, if supplied in the header
23
+ :iv, # [String] IV used to encrypt the data, if supplied in the header
24
+ :key, # [String] Key used to encrypt the data, if supplied in the header
25
+ :cipher_name, # [String] Name of the cipher used, if supplied in the header
26
+ :version, # [Integer] Version of the cipher used, if supplied in the header
27
+ :decryption_cipher, # [SymmetricEncryption::Cipher] Cipher matching the header, or SymmetricEncryption.cipher(default_version)
28
+ )
29
+
19
30
  # Generate a new Symmetric Key pair
20
31
  #
21
32
  # Returns a hash containing a new random symmetric_key pair
22
33
  # consisting of a :key and :iv.
23
34
  # The cipher_name is also included for compatibility with the Cipher initializer
24
- def self.random_key_pair(cipher_name = 'aes-256-cbc', generate_iv = true)
35
+ def self.random_key_pair(cipher_name = 'aes-256-cbc')
25
36
  openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
26
37
  openssl_cipher.encrypt
27
38
 
28
39
  {
29
40
  :key => openssl_cipher.random_key,
30
- :iv => generate_iv ? openssl_cipher.random_iv : nil,
41
+ :iv => openssl_cipher.random_iv,
31
42
  :cipher_name => cipher_name
32
43
  }
33
44
  end
@@ -65,18 +76,34 @@ module SymmetricEncryption
65
76
  # Optional. The version number of this encryption key
66
77
  # Used by SymmetricEncryption to select the correct key when decrypting data
67
78
  # Maximum value: 255
68
- def initialize(parms={})
69
- raise "Missing mandatory parameter :key" unless @key = parms[:key]
70
- @iv = parms[:iv]
71
- @cipher_name = parms[:cipher_name] || parms[:cipher] || 'aes-256-cbc'
72
- @version = parms[:version]
73
- raise "Cipher version has a maximum of 255. #{@version} is too high" if @version.to_i > 255
74
- @encoding = (parms[:encoding] || :base64).to_sym
75
-
76
- raise("Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding)
79
+ #
80
+ # :always_add_header [true|false]
81
+ # Whether to always include the header when encrypting data.
82
+ # ** Highly recommended to set this value to true **
83
+ # Increases the length of the encrypted data by a few bytes, but makes
84
+ # migration to a new key trivial
85
+ # Default: false
86
+ # Recommended: true
87
+ #
88
+ def initialize(params={})
89
+ parms = params.dup
90
+ @key = parms.delete(:key)
91
+ @iv = parms.delete(:iv)
92
+ @cipher_name = parms.delete(:cipher_name) || parms.delete(:cipher) || 'aes-256-cbc'
93
+ @version = parms.delete(:version)
94
+ @always_add_header = parms.delete(:always_add_header) || false
95
+ @encoding = (parms.delete(:encoding) || :base64).to_sym
96
+
97
+ raise "Missing mandatory parameter :key" unless @key
98
+ raise "Invalid Encoding: #{@encoding}" unless ENCODINGS.include?(@encoding)
99
+ raise "Cipher version has a valid rage of 0 to 255. #{@version} is too high, or negative" if (@version.to_i > 255) || (@version.to_i < 0)
100
+ parms.each_pair {|k,v| warn "SymmetricEncryption::Cipher Ignoring unknown option #{k.inspect} = #{v.inspect}"}
77
101
  end
78
102
 
79
- # Returns encrypted and then encoded string
103
+ # Encrypt and then encode a binary or UTF-8 string
104
+ #
105
+ # Returns data encrypted and then encoded according to the encoding setting
106
+ # of this cipher
80
107
  # Returns nil if str is nil
81
108
  # Returns "" str is empty
82
109
  #
@@ -105,7 +132,7 @@ module SymmetricEncryption
105
132
  # compress [true|false]
106
133
  # Whether to compress str before encryption
107
134
  # Should only be used for large strings since compression overhead and
108
- # the overhead of adding the 'magic' header may exceed any benefits of
135
+ # the overhead of adding the encryption header may exceed any benefits of
109
136
  # compression
110
137
  # Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
111
138
  # Default: false
@@ -117,40 +144,36 @@ module SymmetricEncryption
117
144
  self.encode(encrypted)
118
145
  end
119
146
 
120
- # Decryption of supplied string
147
+ # Decode and Decrypt string
148
+ # Returns a decrypted string after decoding it first according to the
149
+ # encoding setting of this cipher
150
+ # Returns nil if encrypted_string is nil
151
+ # Returns '' if encrypted_string == ''
121
152
  #
122
- # Decodes string first if decode is true
153
+ # Parameters
154
+ # encrypted_string [String]
155
+ # Binary encrypted string to decrypt
123
156
  #
124
- # Returns a UTF-8 encoded, decrypted string
125
- # Returns nil if the supplied str is nil
126
- # Returns "" if it is a string and it is empty
127
- if defined?(Encoding)
128
- def decrypt(str)
129
- decoded = self.decode(str)
130
- return unless decoded
131
-
132
- return decoded if decoded.empty?
133
- binary_decrypt(decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING)
134
- end
135
- else
136
- def decrypt(str)
137
- decoded = self.decode(str)
138
- return unless decoded
139
-
140
- return decoded if decoded.empty?
141
- crypt(:decrypt, decoded)
142
- end
143
- end
144
-
145
- # Return a new random key using the configured cipher_name
146
- # Useful for generating new symmetric keys
147
- def random_key
148
- ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key
149
- end
157
+ # header [HeaderStruct]
158
+ # Optional header for the supplied encrypted_string
159
+ #
160
+ # binary [true|false]
161
+ # If no header is supplied then determines whether the string returned
162
+ # is binary or UTF8
163
+ #
164
+ # Reads the 'magic' header if present for key, iv, cipher_name and compression
165
+ #
166
+ # encrypted_string must be in raw binary form when calling this method
167
+ #
168
+ # Creates a new OpenSSL::Cipher with every call so that this call
169
+ # is thread-safe and can be called concurrently by multiple threads with
170
+ # the same instance of Cipher
171
+ def decrypt(str)
172
+ decoded = self.decode(str)
173
+ return unless decoded
150
174
 
151
- # Returns the block size for the configured cipher_name
152
- def block_size
153
- ::OpenSSL::Cipher::Cipher.new(@cipher_name).block_size
175
+ return decoded if decoded.empty?
176
+ binary_decrypt(decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING)
154
177
  end
155
178
 
156
179
  # Returns UTF8 encoded string after encoding the supplied Binary string
@@ -161,16 +184,22 @@ module SymmetricEncryption
161
184
  #
162
185
  # Returned string is UTF8 encoded except for encoding :none
163
186
  def encode(binary_string)
164
- return unless binary_string
187
+ return binary_string if binary_string.nil? || (binary_string == '')
165
188
 
166
189
  # Now encode data based on encoding setting
167
190
  case encoding
168
191
  when :base64
169
- ::Base64.encode64(binary_string).force_encoding(SymmetricEncryption::UTF8_ENCODING)
192
+ encoded_string = ::Base64.encode64(binary_string)
193
+ # Support Ruby 1.9 encoding
194
+ defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
170
195
  when :base64strict
171
- ::Base64.encode64(binary_string).gsub(/\n/, '').force_encoding(SymmetricEncryption::UTF8_ENCODING)
196
+ encoded_string = ::Base64.encode64(binary_string).gsub(/\n/, '')
197
+ # Support Ruby 1.9 encoding
198
+ defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
172
199
  when :base16
173
- binary_string.to_s.unpack('H*').first.force_encoding(SymmetricEncryption::UTF8_ENCODING)
200
+ encoded_string = binary_string.to_s.unpack('H*').first
201
+ # Support Ruby 1.9 encoding
202
+ defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
174
203
  else
175
204
  binary_string
176
205
  end
@@ -181,42 +210,53 @@ module SymmetricEncryption
181
210
  #
182
211
  # Returned string is Binary encoded
183
212
  def decode(encoded_string)
184
- return unless encoded_string
213
+ return encoded_string if encoded_string.nil? || (encoded_string == '')
185
214
 
186
215
  case encoding
187
216
  when :base64, :base64strict
188
- ::Base64.decode64(encoded_string).force_encoding(SymmetricEncryption::BINARY_ENCODING)
217
+ decoded_string = ::Base64.decode64(encoded_string)
218
+ # Support Ruby 1.9 encoding
219
+ defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
189
220
  when :base16
190
- [encoded_string].pack('H*').force_encoding(SymmetricEncryption::BINARY_ENCODING)
221
+ decoded_string = [encoded_string].pack('H*')
222
+ # Support Ruby 1.9 encoding
223
+ defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
191
224
  else
192
225
  encoded_string
193
226
  end
194
227
  end
195
228
 
196
- # Returns an Array of the following values extracted from header or nil
197
- # if any value was not specified in the header
198
- # compressed [true|false]
199
- # iv [String]
200
- # key [String]
201
- # cipher_name [String}
202
- # decryption_cipher [SymmetricEncryption::Cipher]
229
+ # Return a new random key using the configured cipher_name
230
+ # Useful for generating new symmetric keys
231
+ def random_key
232
+ ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key
233
+ end
234
+
235
+ # Returns the block size for the configured cipher_name
236
+ def block_size
237
+ ::OpenSSL::Cipher::Cipher.new(@cipher_name).block_size
238
+ end
239
+
240
+ # Returns whether the supplied buffer starts with a symmetric_encryption header
241
+ # Note: The encoding of the supplied buffer is forced to binary if not already binary
242
+ def self.has_header?(buffer)
243
+ return false if buffer.nil? || (buffer == '')
244
+ buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) if buffer.respond_to?(:force_encoding)
245
+ buffer.start_with?(MAGIC_HEADER)
246
+ end
247
+
248
+ # Returns HeaderStruct of the header parsed from the supplied string
249
+ # Returns nil if no header is present
203
250
  #
204
- # The supplied buffer will be updated directly and will have the header
205
- # portion removed
251
+ # The supplied buffer will be updated directly and its header will be
252
+ # stripped if present
206
253
  #
207
254
  # Parameters
208
255
  # buffer
209
- # String to extract the header from if present
210
- #
211
- # default_version
212
- # If no header is present, this is the default value for the version
213
- # of the cipher to use
256
+ # String to extract the header from
214
257
  #
215
- # default_compressed
216
- # If no header is present, this is the default value for the compression
217
- def self.parse_magic_header!(buffer, default_version=nil, default_compressed=false)
218
- buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) if buffer
219
- return [default_compressed, nil, nil, nil, nil, SymmetricEncryption.cipher(default_version)] unless buffer && buffer.start_with?(MAGIC_HEADER)
258
+ def self.parse_header!(buffer)
259
+ return unless has_header?(buffer)
220
260
 
221
261
  # Header includes magic header and version byte
222
262
  # Remove header and extract flags
@@ -225,6 +265,7 @@ module SymmetricEncryption
225
265
  include_iv = (flags & 0b0100_0000_0000_0000) != 0
226
266
  include_key = (flags & 0b0010_0000_0000_0000) != 0
227
267
  include_cipher= (flags & 0b0001_0000_0000_0000) != 0
268
+ binary = (flags & 0b0000_1000_0000_0000) != 0
228
269
  # Version of the key to use to decrypt the key if present,
229
270
  # otherwise to decrypt the data following the header
230
271
  version = flags & 0b0000_0000_1111_1111
@@ -238,14 +279,14 @@ module SymmetricEncryption
238
279
  end
239
280
  if include_key
240
281
  len = buffer.slice!(0..1).unpack('v').first
241
- key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1))
282
+ key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1), header=false, binary=true)
242
283
  end
243
284
  if include_cipher
244
285
  len = buffer.slice!(0..1).unpack('v').first
245
286
  cipher_name = buffer.slice!(0..len-1)
246
287
  end
247
288
 
248
- [compressed, iv, key, cipher_name, version, decryption_cipher]
289
+ HeaderStruct.new(compressed, binary, iv, key, cipher_name, version, decryption_cipher)
249
290
  end
250
291
 
251
292
  # Returns a magic header for this cipher instance that can be placed at
@@ -269,16 +310,15 @@ module SymmetricEncryption
269
310
  # Includes the cipher_name used. For example 'aes-256-cbc'
270
311
  # The cipher_name string to to put in the header
271
312
  # Default: nil : Exclude cipher_name name from header
272
- def self.magic_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil)
313
+ #
314
+ # binary
315
+ # Whether the data being encrypted is binary.
316
+ # When the header is read, it sets the encoding of the string returned to Binary
317
+ def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil, binary=false)
273
318
  # Ruby V2 named parameters would be perfect here
274
319
 
275
- # Encryption version indicator if available
276
- flags = version || 0 # Same as 0b0000_0000_0000_0000
277
-
278
- # Replace version with global cipher that will be used to encrypt the random key
279
- if iv || key
280
- flags = (SymmetricEncryption.cipher.version || 0)
281
- end
320
+ # Version number of supplied encryption key, or use the global cipher version if none was supplied
321
+ flags = iv || key ? (SymmetricEncryption.cipher.version || 0) : (version || 0) # Same as 0b0000_0000_0000_0000
282
322
 
283
323
  # If the data is to be compressed before being encrypted, set the
284
324
  # compressed bit in the flags word
@@ -286,13 +326,14 @@ module SymmetricEncryption
286
326
  flags |= 0b0100_0000_0000_0000 if iv
287
327
  flags |= 0b0010_0000_0000_0000 if key
288
328
  flags |= 0b0001_0000_0000_0000 if cipher_name
329
+ flags |= 0b0000_1000_0000_0000 if binary
289
330
  header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
290
331
  if iv
291
332
  header << [iv.length].pack('v')
292
333
  header << iv
293
334
  end
294
335
  if key
295
- encrypted = SymmetricEncryption.cipher.binary_encrypt(key, false, false)
336
+ encrypted = SymmetricEncryption.cipher.binary_encrypt(key, false, false, false)
296
337
  header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING)
297
338
  header << encrypted
298
339
  end
@@ -307,21 +348,33 @@ module SymmetricEncryption
307
348
  #
308
349
  # Returns a Binary encrypted string without applying any Base64, or other encoding
309
350
  #
310
- # Adds the 'magic' header if a random_iv is required or compression is enabled
351
+ # add_header [nil|true|false]
352
+ # Whether to add a header to the encrypted string
353
+ # If not supplied it defaults to true if always_add_header || random_iv || compress
354
+ # Default: nil
311
355
  #
312
356
  # Creates a new OpenSSL::Cipher with every call so that this call
313
357
  # is thread-safe
314
358
  #
315
359
  # See #encrypt to encrypt and encode the result as a string
316
- def binary_encrypt(string, random_iv=false, compress=false)
360
+ def binary_encrypt(str, random_iv=false, compress=false, add_header=nil)
361
+ return if str.nil?
362
+ string = str.to_s
363
+ return string if string.empty?
364
+
365
+ # Creates a new OpenSSL::Cipher with every call so that this call
366
+ # is thread-safe
317
367
  openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
318
368
  openssl_cipher.encrypt
319
369
  openssl_cipher.key = @key
320
- result = if random_iv || compress
370
+ add_header = always_add_header || random_iv || compress if add_header.nil?
371
+ result = if add_header
321
372
  # Random iv and compress both add the magic header
322
373
  iv = random_iv ? openssl_cipher.random_iv : @iv
323
374
  openssl_cipher.iv = iv if iv
324
- self.class.magic_header(version, compress, random_iv ? iv : nil) +
375
+ # Set the binary indicator on the header if string is Binary Encoded
376
+ binary = (string.encoding == SymmetricEncryption::BINARY_ENCODING)
377
+ self.class.build_header(version, compress, random_iv ? iv : nil, binary) +
325
378
  openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string)
326
379
  else
327
380
  openssl_cipher.iv = @iv if @iv
@@ -331,48 +384,81 @@ module SymmetricEncryption
331
384
  end
332
385
 
333
386
  # Advanced use only
387
+ # See #decrypt to decrypt encoded strings
334
388
  #
335
389
  # Returns a Binary decrypted string without decoding the string first
336
390
  #
391
+ # Decryption of supplied string
392
+ # Returns the decrypted string
393
+ # Returns nil if encrypted_string is nil
394
+ # Returns '' if encrypted_string == ''
395
+ #
396
+ # Parameters
397
+ # encrypted_string [String]
398
+ # Binary encrypted string to decrypt
399
+ #
400
+ # header [HeaderStruct]
401
+ # Optional header for the supplied encrypted_string
402
+ #
403
+ # binary [true|false]
404
+ # If no header is supplied then determines whether the string returned
405
+ # is binary or UTF8
406
+ #
337
407
  # Reads the 'magic' header if present for key, iv, cipher_name and compression
338
408
  #
339
409
  # encrypted_string must be in raw binary form when calling this method
340
410
  #
341
411
  # Creates a new OpenSSL::Cipher with every call so that this call
342
- # is thread-safe
343
- #
344
- # See #decrypt to decrypt encoded strings
345
- def binary_decrypt(encrypted_string)
412
+ # is thread-safe and can be called concurrently by multiple threads with
413
+ # the same instance of Cipher
414
+ #
415
+ # Note:
416
+ # When a string is encrypted and the header is used, its decrypted form
417
+ # is automatically set to the same UTF-8 or Binary encoding
418
+ def binary_decrypt(encrypted_string, header=nil, binary=false)
419
+ return if encrypted_string.nil?
346
420
  str = encrypted_string.to_s
347
- if str.start_with?(MAGIC_HEADER)
421
+ str.force_encoding(SymmetricEncryption::BINARY_ENCODING) if str.respond_to?(:force_encoding)
422
+ return str if str.empty?
423
+
424
+ decrypted_string = if header || self.class.has_header?(str)
348
425
  str = str.dup
349
- compressed, iv, key, cipher_name = self.class.parse_magic_header!(str)
350
- openssl_cipher = ::OpenSSL::Cipher.new(cipher_name || self.cipher_name)
426
+ header ||= self.class.parse_header!(str)
427
+ binary = header.binary
428
+
429
+ openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || self.cipher_name)
351
430
  openssl_cipher.decrypt
352
- openssl_cipher.key = key || @key
353
- iv ||= @iv
431
+ openssl_cipher.key = header.key || @key
432
+ iv = header.iv || @iv
354
433
  openssl_cipher.iv = iv if iv
355
434
  result = openssl_cipher.update(str)
356
435
  result << openssl_cipher.final
357
- compressed ? Zlib::Inflate.inflate(result) : result
436
+ header.compressed ? Zlib::Inflate.inflate(result) : result
358
437
  else
359
438
  openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
360
439
  openssl_cipher.decrypt
361
440
  openssl_cipher.key = @key
362
441
  openssl_cipher.iv = @iv if @iv
363
- result = openssl_cipher.update(encrypted_string)
442
+ result = openssl_cipher.update(str)
364
443
  result << openssl_cipher.final
365
444
  end
445
+
446
+ # Support Ruby 1.9 and above Encoding
447
+ if defined?(Encoding)
448
+ # Sets the encoding of the result string to UTF8 or BINARY based on the binary header
449
+ binary ? decrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decrypted_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
450
+ else
451
+ decrypted_string
452
+ end
366
453
  end
367
454
 
368
- # Returns [String] object represented as a string
369
- # Excluding the key and iv
455
+ # Returns [String] object represented as a string, filtering out the key
370
456
  def inspect
371
- "#<#{self.class}:0x#{self.__id__.to_s(16)} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}"
457
+ "#<#{self.class}:0x#{self.__id__.to_s(16)} @key=\"[FILTERED]\" @iv=#{iv.inspect} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}"
372
458
  end
373
459
 
374
460
  private
375
461
 
376
- attr_reader :key, :iv
462
+ attr_reader :key
377
463
  end
378
- end
464
+ end
@@ -311,20 +311,31 @@ module SymmetricEncryption
311
311
 
312
312
  # Read the header from the file if present
313
313
  def read_header
314
- @compressed = nil
315
314
  @pos = 0
316
315
 
317
316
  # Read first block and check for the header
318
317
  buf = @ios.read(@buffer_size)
319
318
 
320
319
  # Use cipher specified in header, or global cipher if it has no header
321
- @compressed, iv, key, cipher_name, version, decryption_cipher = SymmetricEncryption::Cipher.parse_magic_header!(buf, @version)
322
- @header_present = true if iv || key || version
320
+ iv, key, cipher_name, decryption_cipher = nil
321
+ if header = SymmetricEncryption::Cipher.parse_header!(buf)
322
+ @header_present = true
323
+ @compressed = header.compressed
324
+ decryption_cipher = header.decryption_cipher
325
+ cipher_name = header.cipher_name || decryption_cipher.cipher_name
326
+ key = header.key
327
+ iv = header.iv
328
+ else
329
+ @header_present = false
330
+ @compressed = nil
331
+ decryption_cipher = SymmetricEncryption.cipher(@version)
332
+ cipher_name = decryption_cipher.cipher_name
333
+ end
323
334
 
324
- @stream_cipher = ::OpenSSL::Cipher.new(cipher_name || decryption_cipher.cipher_name)
335
+ @stream_cipher = ::OpenSSL::Cipher.new(cipher_name)
325
336
  @stream_cipher.decrypt
326
337
  @stream_cipher.key = key || decryption_cipher.send(:key)
327
- @stream_cipher.iv = iv || decryption_cipher.send(:iv)
338
+ @stream_cipher.iv = iv || decryption_cipher.iv
328
339
 
329
340
  # First call to #update should return an empty string anyway
330
341
  if buf && buf.length > 0