symmetric-encryption 2.2.0 → 3.0.0

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