symmetric-encryption 3.9.1 → 4.0.0.beta3

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -0
  3. data/bin/symmetric-encryption +5 -0
  4. data/lib/symmetric_encryption/cipher.rb +162 -419
  5. data/lib/symmetric_encryption/cli.rb +343 -0
  6. data/lib/symmetric_encryption/coerce.rb +5 -20
  7. data/lib/symmetric_encryption/config.rb +128 -50
  8. data/lib/symmetric_encryption/extensions/mongo_mapper/plugins/encrypted_key.rb +2 -2
  9. data/lib/symmetric_encryption/generator.rb +3 -2
  10. data/lib/symmetric_encryption/header.rb +260 -0
  11. data/lib/symmetric_encryption/key.rb +106 -0
  12. data/lib/symmetric_encryption/keystore/environment.rb +90 -0
  13. data/lib/symmetric_encryption/keystore/file.rb +102 -0
  14. data/lib/symmetric_encryption/keystore/memory.rb +53 -0
  15. data/lib/symmetric_encryption/keystore.rb +124 -0
  16. data/lib/symmetric_encryption/railtie.rb +5 -7
  17. data/lib/symmetric_encryption/reader.rb +74 -55
  18. data/lib/symmetric_encryption/rsa_key.rb +24 -0
  19. data/lib/symmetric_encryption/symmetric_encryption.rb +64 -102
  20. data/lib/symmetric_encryption/utils/re_encrypt_files.rb +140 -0
  21. data/lib/symmetric_encryption/version.rb +1 -1
  22. data/lib/symmetric_encryption/writer.rb +104 -117
  23. data/lib/symmetric_encryption.rb +9 -4
  24. data/test/active_record_test.rb +61 -40
  25. data/test/cipher_test.rb +179 -236
  26. data/test/config/symmetric-encryption.yml +140 -82
  27. data/test/header_test.rb +218 -0
  28. data/test/key_test.rb +231 -0
  29. data/test/keystore/environment_test.rb +119 -0
  30. data/test/keystore/file_test.rb +125 -0
  31. data/test/keystore_test.rb +59 -0
  32. data/test/mongoid_test.rb +13 -13
  33. data/test/reader_test.rb +52 -53
  34. data/test/symmetric_encryption_test.rb +50 -135
  35. data/test/test_db.sqlite3 +0 -0
  36. data/test/writer_test.rb +52 -31
  37. metadata +26 -14
  38. data/examples/symmetric-encryption.yml +0 -108
  39. data/lib/rails/generators/symmetric_encryption/config/config_generator.rb +0 -22
  40. data/lib/rails/generators/symmetric_encryption/config/templates/symmetric-encryption.yml +0 -50
  41. data/lib/rails/generators/symmetric_encryption/heroku_config/heroku_config_generator.rb +0 -20
  42. data/lib/rails/generators/symmetric_encryption/heroku_config/templates/symmetric-encryption.yml +0 -78
  43. data/lib/rails/generators/symmetric_encryption/new_keys/new_keys_generator.rb +0 -14
  44. data/lib/symmetric_encryption/key_encryption_key.rb +0 -32
  45. data/lib/symmetric_encryption/railties/symmetric_encryption.rake +0 -84
  46. data/lib/symmetric_encryption/utils/re_encrypt_config_files.rb +0 -82
@@ -1,188 +1,50 @@
1
1
  require 'openssl'
2
2
  module SymmetricEncryption
3
3
  # Hold all information related to encryption keys
4
- # as well as encrypt and decrypt data using those keys
4
+ # as well as encrypt and decrypt data using those keys.
5
5
  #
6
6
  # Cipher is thread safe so that the same instance can be called by multiple
7
- # threads at the same time without needing an instance of Cipher per thread
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
10
  attr_accessor :cipher_name, :version, :iv, :always_add_header
11
- attr_reader :encoder, :encoding
11
+ attr_reader :encoding
12
12
  attr_writer :key
13
13
 
14
- # Backward compatibility
15
- alias_method :cipher, :cipher_name
16
-
17
- # Defines the Header Structure returned when parsing the header
18
- HeaderStruct = Struct.new(
19
- # [true|false] Whether the data is compressed, if supplied in the header
20
- :compressed,
21
- # [String] IV used to encrypt the data, if supplied in the header
22
- :iv,
23
- # [String] Key used to encrypt the data, if supplied in the header
24
- :key,
25
- # [String] Name of the cipher used, if supplied in the header
26
- :cipher_name,
27
- # [Integer] Version of the cipher used, if supplied in the header
28
- :version,
29
- # [SymmetricEncryption::Cipher] Cipher matching the header, or SymmetricEncryption.cipher(default_version)
30
- :decryption_cipher
31
- )
32
-
33
- # Generate a new Symmetric Key pair
34
- #
35
- # Returns a hash containing a new random symmetric_key pair
36
- # consisting of a :key and :iv.
37
- # The cipher_name is also included for compatibility with the Cipher initializer
38
- #
39
- # Notes:
40
- # * The key _must_ be properly secured
41
- # * The iv can be stored in the clear and it is not necessary to encrypt it
42
- def self.random_key_pair(cipher_name = 'aes-256-cbc')
43
- openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
44
- openssl_cipher.encrypt
45
-
46
- {
47
- key: openssl_cipher.random_key,
48
- iv: openssl_cipher.random_iv,
49
- cipher_name: cipher_name
50
- }
14
+ # Returns [Cipher] from a cipher config instance.
15
+ def self.from_config(cipher_name: 'aes-256-cbc',
16
+ version: 0,
17
+ always_add_header: true,
18
+ encoding: :base64strict,
19
+ **config)
20
+
21
+ Key.migrate_config!(config)
22
+ key = Key.from_config(cipher_name: cipher_name, **config)
23
+
24
+ Cipher.new(
25
+ key: key.key,
26
+ iv: key.iv,
27
+ cipher_name: cipher_name,
28
+ version: version,
29
+ always_add_header: always_add_header,
30
+ encoding: encoding
31
+ )
51
32
  end
52
33
 
53
- # Generate new randomized keys and generate key and iv files if supplied.
54
- # Overwrites key files for the current environment.
34
+ # Returns [SymmetricEncryption::Cipher] for encryption and decryption purposes.
55
35
  #
56
- # Parameters
57
- # :key_filename
58
- # Name of file that will contain the symmetric key encrypted using the public
59
- # key from the private_rsa_key.
60
- # Or,
61
- # :encrypted_key
62
- # Symmetric key encrypted using the public key from the private_rsa_key
63
- # and then Base64 encoded
64
- #
65
- # Note:
66
- # If :key_filename and :encrypted_key are not supplied then a new :key will be returned.
67
- # :key is the Symmetric Key to use for encryption and decryption.
68
- #
69
- #
70
- # :iv_filename
71
- # Name of file containing symmetric key initialization vector
72
- # encrypted using the public key from the private_rsa_key
73
- # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV)
74
- # Or,
75
- # :encrypted_iv
76
- # Initialization vector encrypted using the public key from the private_rsa_key
77
- # and then Base64 encoded
78
- # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV)
79
- #
80
- # Note:
81
- # If :iv_filename and :encrypted_iv are not supplied then a new :iv will be returned.
82
- # :key is the Initialization Vector to use with Symmetric Key.
83
- #
84
- #
85
- # private_rsa_key [String]
86
- # Key encryption key.
87
- # To generate a new one: SymmetricEncryption::KeyEncryptionKey.generate
88
- # Required if :key_filename, :encrypted_key, :iv_filename, or :encrypted_iv is supplied
89
- #
90
- # :cipher_name [String]
91
- # Encryption Cipher to use.
92
- # Default: aes-256-cbc
36
+ # Parameters:
37
+ # key [String]
38
+ # The Symmetric Key to use for encryption and decryption.
93
39
  #
94
- # :encoding [Symbol]
95
- # :base64strict
96
- # Return as a base64 encoded string that does not include additional newlines
97
- # This is the recommended format since newlines in the values to
98
- # SQL queries are cumbersome. Also the newline reformatting is unnecessary
99
- # It is not the default for backward compatibility
100
- # :base64
101
- # Return as a base64 encoded string
102
- # :base16
103
- # Return as a Hex encoded string
104
- # :none
105
- # Return as raw binary data string. Note: String can contain embedded nulls
106
- # Default: :base64strict
107
- def self.generate_random_keys(params = {})
108
- params = params.dup
109
- private_rsa_key = params.delete(:private_rsa_key)
110
- cipher_name = params.delete(:cipher_name) || 'aes-256-cbc'
111
- encoding = params.delete(:encoding) || :base64strict
112
- unless private_rsa_key
113
- [:key_filename, :encrypted_key, :iv_filename, :encrypted_iv].each do |key|
114
- raise(SymmetricEncryption::ConfigError, "When :#{key} is supplied, :private_rsa_key is required.") if params.include?(key)
115
- end
116
- end
117
-
118
- key_encryption_key = KeyEncryptionKey.new(private_rsa_key) if private_rsa_key
119
- cipher_conf = {cipher_name: cipher_name, encoding: encoding}
120
-
121
- key_pair = SymmetricEncryption::Cipher.random_key_pair(cipher_name)
122
- key = key_pair[:key]
123
- iv = key_pair[:iv]
124
-
125
- if file_name = params.delete(:key_filename)
126
- cipher_conf[:key_filename] = file_name
127
- encrypted_key = key_encryption_key.encrypt(key)
128
- write_to_file(file_name, encrypted_key)
129
- elsif params.delete(:encrypted_key)
130
- encrypted_key = key_encryption_key.encrypt(key)
131
- cipher_conf[:encrypted_key] = SymmetricEncryption::Encoder[encoding].encode(encrypted_key)
132
- else
133
- params.delete(:key)
134
- cipher_conf[:key] = SymmetricEncryption::Encoder[encoding].encode(key.to_s)
135
- end
136
-
137
- if file_name = params.delete(:iv_filename)
138
- cipher_conf[:iv_filename] = file_name
139
- encrypted_iv = key_encryption_key.encrypt(iv)
140
- write_to_file(file_name, encrypted_iv)
141
- elsif params.delete(:encrypted_iv)
142
- encrypted_iv = key_encryption_key.encrypt(iv)
143
- cipher_conf[:encrypted_iv] = SymmetricEncryption::Encoder[encoding].encode(encrypted_iv)
144
- else
145
- params.delete(:iv)
146
- cipher_conf[:iv] = SymmetricEncryption::Encoder[encoding].encode(iv.to_s)
147
- end
148
-
149
- raise(ArgumentError, "SymmetricEncryption::Cipher Invalid options #{params.inspect}") if params.size > 0
150
- cipher_conf
151
- end
152
-
153
- # Create a Symmetric::Key for encryption and decryption purposes
40
+ # iv [String]
41
+ # The Initialization Vector to use.
154
42
  #
155
- # Parameters:
156
- # :key [String]
157
- # The Symmetric Key to use for encryption and decryption
158
- # Or,
159
- # :key_filename
160
- # Name of file containing symmetric key encrypted using the public
161
- # key from the private_rsa_key
162
- # Or,
163
- # :encrypted_key
164
- # Symmetric key encrypted using the public key from the private_rsa_key
165
- # and then Base64 encoded
166
- #
167
- # :iv [String]
168
- # Optional. The Initialization Vector to use with Symmetric Key
169
- # Highly Recommended as it is the input into the CBC algorithm
170
- # Or,
171
- # :iv_filename
172
- # Name of file containing symmetric key initialization vector
173
- # encrypted using the public key from the private_rsa_key
174
- # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV)
175
- # Or,
176
- # :encrypted_iv
177
- # Initialization vector encrypted using the public key from the private_rsa_key
178
- # and then Base64 encoded
179
- # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV)
180
- #
181
- # :cipher_name [String]
43
+ # cipher_name [String]
182
44
  # Optional. Encryption Cipher to use
183
45
  # Default: aes-256-cbc
184
46
  #
185
- # :encoding [Symbol]
47
+ # encoding [Symbol]
186
48
  # :base64strict
187
49
  # Return as a base64 encoded string that does not include additional newlines
188
50
  # This is the recommended format since newlines in the values to
@@ -194,74 +56,48 @@ module SymmetricEncryption
194
56
  # Return as a Hex encoded string
195
57
  # :none
196
58
  # Return as raw binary data string. Note: String can contain embedded nulls
197
- # Default: :base64
198
- # Recommended: :base64strict
59
+ # Default: :base64strict
199
60
  #
200
- # :version [Fixnum]
61
+ # version [Fixnum]
201
62
  # Optional. The version number of this encryption key
202
63
  # Used by SymmetricEncryption to select the correct key when decrypting data
203
- # Maximum value: 255
64
+ # Valid Range: 0..255
65
+ # Default: 1
204
66
  #
205
- # :always_add_header [true|false]
67
+ # always_add_header [true|false]
206
68
  # Whether to always include the header when encrypting data.
207
69
  # ** Highly recommended to set this value to true **
208
70
  # Increases the length of the encrypted data by a few bytes, but makes
209
71
  # migration to a new key trivial
210
- # Default: false
211
- # Recommended: true
212
- #
213
- # private_rsa_key [String]
214
- # Key encryption key.
215
- # To generate a new one: SymmetricEncryption::KeyEncryptionKey.generate
216
- # Required if :key_filename, :encrypted_key, :iv_filename, or :encrypted_iv is supplied
217
- def initialize(params={})
218
- params = params.dup
219
- @cipher_name = params.delete(:cipher_name) || params.delete(:cipher) || 'aes-256-cbc'
220
- @version = params.delete(:version) || 0
221
- @always_add_header = params.delete(:always_add_header) || false
222
- self.encoding = (params.delete(:encoding) || :base64).to_sym
223
- private_rsa_key = params.delete(:private_rsa_key)
224
- unless private_rsa_key
225
- [:key_filename, :encrypted_key, :iv_filename, :encrypted_iv].each do |key|
226
- raise(SymmetricEncryption::ConfigError, "When :#{key} is supplied, :private_rsa_key is required.") if params.include?(key)
227
- end
228
- end
229
-
230
- key_encryption_key = KeyEncryptionKey.new(private_rsa_key) if private_rsa_key
231
- @key =
232
- if key = params.delete(:key)
233
- key
234
- elsif file_name = params.delete(:key_filename)
235
- encrypted_key = self.class.read_from_file(file_name)
236
- key_encryption_key.decrypt(encrypted_key)
237
- elsif encrypted_key = params.delete(:encrypted_key)
238
- binary = self.encoder.decode(encrypted_key)
239
- key_encryption_key.decrypt(binary)
240
- else
241
- raise(ArgumentError, 'Missing mandatory parameter :key, :key_filename, or :encrypted_key')
242
- end
243
-
244
- @iv =
245
- if iv = params.delete(:iv)
246
- iv
247
- elsif file_name = params.delete(:iv_filename)
248
- encrypted_iv = self.class.read_from_file(file_name)
249
- key_encryption_key.decrypt(encrypted_iv)
250
- elsif encrypted_iv = params.delete(:encrypted_iv)
251
- binary = self.encoder.decode(encrypted_iv)
252
- key_encryption_key.decrypt(binary)
253
- end
254
-
255
- raise(ArgumentError, "Cipher version has a valid range of 0 to 255. #{@version} is too high, or negative") if (@version.to_i > 255) || (@version.to_i < 0)
256
- raise(ArgumentError, "SymmetricEncryption::Cipher Invalid options #{params.inspect}") if params.size > 0
72
+ # Default: true
73
+ def initialize(key:,
74
+ iv: nil,
75
+ cipher_name: 'aes-256-cbc',
76
+ version: 0,
77
+ always_add_header: true,
78
+ encoding: :base64strict)
79
+
80
+ @key = key
81
+ @iv = iv
82
+ @cipher_name = cipher_name
83
+ self.encoding = encoding.to_sym
84
+ @version = version.to_i
85
+ @always_add_header = always_add_header
86
+
87
+ raise(ArgumentError, "Cipher version has a valid range of 0 to 255. #{@version} is too high, or negative") if (@version > 255) || (@version < 0)
257
88
  end
258
89
 
259
90
  # Change the encoding
260
91
  def encoding=(encoding)
261
- @encoder = SymmetricEncryption::Encoder[encoding]
92
+ @encoder = nil
262
93
  @encoding = encoding
263
94
  end
264
95
 
96
+ # Returns [SymmetricEncryption::Encoder] the encoder to use for the current encoding.
97
+ def encoder
98
+ @encoder ||= SymmetricEncryption::Encoder[encoding]
99
+ end
100
+
265
101
  # Encrypt and then encode a string
266
102
  #
267
103
  # Returns data encrypted and then encoded according to the encoding setting
@@ -278,31 +114,28 @@ module SymmetricEncryption
278
114
  # random_iv [true|false]
279
115
  # Whether the encypted value should use a random IV every time the
280
116
  # field is encrypted.
281
- # It is recommended to set this to true where feasible. If the encrypted
282
- # value could be used as part of a SQL where clause, or as part
283
- # of any lookup, then it must be false.
284
- # Setting random_iv to true will result in a different encrypted output for
285
- # the same input string.
286
- # Note: Only set to true if the field will never be used as part of
287
- # the where clause in an SQL query.
288
- # Note: When random_iv is true it will add a 8 byte header, plus the bytes
289
- # to store the random IV in every returned encrypted string, prior to the
290
- # encoding if any.
117
+ # Notes:
118
+ # * Setting random_iv to true will result in a different encrypted output for
119
+ # the same input string.
120
+ # * It is recommended to set this to true, except if it will be used as a lookup key.
121
+ # * Only set to true if the field will never be used as a lookup key, since
122
+ # the encrypted value needs to be same every time in this case.
123
+ # * When random_iv is true it adds the random IV string to the header.
291
124
  # Default: false
292
125
  # Highly Recommended where feasible: true
293
126
  #
294
127
  # compress [true|false]
295
- # Whether to compress str before encryption
296
- # Should only be used for large strings since compression overhead and
297
- # the overhead of adding the encryption header may exceed any benefits of
298
- # compression
299
- # Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
128
+ # Whether to compress str before encryption.
300
129
  # Default: false
301
- def encrypt(str, random_iv=false, compress=false)
130
+ # Notes:
131
+ # * Should only be used for large strings since compression overhead and
132
+ # the overhead of adding the encryption header may exceed any benefits of
133
+ # compression
134
+ def encrypt(str, random_iv: false, compress: false, header: always_add_header)
302
135
  return if str.nil?
303
136
  str = str.to_s
304
137
  return str if str.empty?
305
- encrypted = binary_encrypt(str, random_iv, compress)
138
+ encrypted = binary_encrypt(str, random_iv: random_iv, compress: compress, header: header)
306
139
  self.encode(encrypted)
307
140
  end
308
141
 
@@ -329,12 +162,12 @@ module SymmetricEncryption
329
162
 
330
163
  return decoded if decoded.empty?
331
164
  decrypted = binary_decrypt(decoded)
332
- if defined?(Encoding)
333
- # Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary
334
- unless decrypted.force_encoding(SymmetricEncryption::UTF8_ENCODING).valid_encoding?
335
- decrypted.force_encoding(SymmetricEncryption::BINARY_ENCODING)
336
- end
165
+
166
+ # Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary
167
+ unless decrypted.force_encoding(SymmetricEncryption::UTF8_ENCODING).valid_encoding?
168
+ decrypted.force_encoding(SymmetricEncryption::BINARY_ENCODING)
337
169
  end
170
+
338
171
  decrypted
339
172
  end
340
173
 
@@ -362,166 +195,77 @@ module SymmetricEncryption
362
195
  # Return a new random key using the configured cipher_name
363
196
  # Useful for generating new symmetric keys
364
197
  def random_key
365
- ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key
198
+ ::OpenSSL::Cipher.new(cipher_name).random_key
366
199
  end
367
200
 
368
- # Returns the block size for the configured cipher_name
369
- def block_size
370
- ::OpenSSL::Cipher::Cipher.new(@cipher_name).block_size
201
+ # Return a new random IV using the configured cipher_name
202
+ # Useful for generating new symmetric keys
203
+ def random_iv
204
+ ::OpenSSL::Cipher.new(cipher_name).random_iv
371
205
  end
372
206
 
373
- # Returns whether the supplied buffer starts with a symmetric_encryption header
374
- # Note: The encoding of the supplied buffer is forced to binary if not already binary
375
- def self.has_header?(buffer)
376
- return false if buffer.nil? || (buffer == '')
377
- buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) if buffer.respond_to?(:force_encoding)
378
- buffer.start_with?(MAGIC_HEADER)
207
+ # Returns the block size for the configured cipher_name
208
+ def block_size
209
+ ::OpenSSL::Cipher.new(cipher_name).block_size
379
210
  end
380
211
 
381
- # Returns HeaderStruct of the header parsed from the supplied string
382
- # Returns nil if no header is present
383
- #
384
- # The supplied buffer will be updated directly and its header will be
385
- # stripped if present
212
+ # Advanced use only
386
213
  #
387
- # Parameters
388
- # buffer
389
- # String to extract the header from
214
+ # Returns a Binary encrypted string without applying Base64, or any other encoding.
390
215
  #
391
- def self.parse_header!(buffer)
392
- return unless has_header?(buffer)
393
-
394
- # Header includes magic header and version byte
395
- #
396
- # The encryption header consists of:
397
- # 4 Byte Magic Header Prefix: @Enc
398
- # Followed by 2 Bytes (16 bits)
399
- # Bit 0 through 7: The version of the cipher used to encrypt the header
400
- # Bit 8 though 10: Reserved
401
- # Bit 11: Whether the encrypted data is Binary (otherwise UTF8 text)
402
- # Bit 12: Whether the Cipher Name is included
403
- # Bit 13: Whether the Key is included
404
- # Bit 14: Whether the IV is included
405
- # Bit 15: Whether the data is compressed
406
- # 2 Byte IV Length if included
407
- # IV in binary form
408
- # 2 Byte Key Length if included
409
- # Key in binary form
410
- # 2 Byte Cipher Name Length if included
411
- # Cipher name it UTF8 text
412
-
413
- # Remove header and extract flags
414
- _, flags = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK)
415
- compressed = (flags & 0b1000_0000_0000_0000) != 0
416
- include_iv = (flags & 0b0100_0000_0000_0000) != 0
417
- include_key = (flags & 0b0010_0000_0000_0000) != 0
418
- include_cipher = (flags & 0b0001_0000_0000_0000) != 0
419
- # Version of the key to use to decrypt the key if present,
420
- # otherwise to decrypt the data following the header
421
- version = flags & 0b0000_0000_1111_1111
422
- decryption_cipher = SymmetricEncryption.cipher(version)
423
- raise(SymmetricEncryption::CipherError, "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers") unless decryption_cipher
424
- iv, key, cipher_name = nil
425
-
426
- if include_iv
427
- len = buffer.slice!(0..1).unpack('v').first
428
- iv = buffer.slice!(0..len-1)
429
- end
430
- if include_key
431
- len = buffer.slice!(0..1).unpack('v').first
432
- key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1), false)
433
- end
434
- if include_cipher
435
- len = buffer.slice!(0..1).unpack('v').first
436
- cipher_name = buffer.slice!(0..len-1)
437
- end
438
-
439
- HeaderStruct.new(compressed, iv, key, cipher_name, version, decryption_cipher)
440
- end
441
-
442
- # Returns a magic header for this cipher instance that can be placed at
443
- # the beginning of a file or stream to indicate how the data was encrypted
216
+ # str [String]
217
+ # String to be encrypted. If str is not a string, #to_s will be called on it
218
+ # to convert it to a string
444
219
  #
445
- # Parameters
446
- # compressed
447
- # Sets the compressed indicator in the header
220
+ # random_iv [true|false]
221
+ # Whether the encypted value should use a random IV every time the
222
+ # field is encrypted.
223
+ # Notes:
224
+ # * Setting random_iv to true will result in a different encrypted output for
225
+ # the same input string.
226
+ # * It is recommended to set this to true, except if it will be used as a lookup key.
227
+ # * Only set to true if the field will never be used as a lookup key, since
228
+ # the encrypted value needs to be same every time in this case.
229
+ # * When random_iv is true it adds the random IV string to the header.
448
230
  # Default: false
231
+ # Highly Recommended where feasible: true
449
232
  #
450
- # iv
451
- # The iv to to put in the header
452
- # Default: nil : Exclude from header
453
- #
454
- # key
455
- # The key to to put in the header
456
- # The key is encrypted using the global encryption key
457
- # Default: nil : Exclude key from header
458
- #
459
- # cipher_name
460
- # Includes the cipher_name used. For example 'aes-256-cbc'
461
- # The cipher_name string to to put in the header
462
- # Default: nil : Exclude cipher_name name from header
463
- def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil)
464
- version ||= SymmetricEncryption.cipher.version
465
-
466
- flags = version # Same as 0b0000_0000_0000_0000
467
-
468
- # If the data is to be compressed before being encrypted, set the
469
- # compressed bit in the flags word
470
- flags |= 0b1000_0000_0000_0000 if compressed
471
- flags |= 0b0100_0000_0000_0000 if iv
472
- flags |= 0b0010_0000_0000_0000 if key
473
- flags |= 0b0001_0000_0000_0000 if cipher_name
474
- header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
475
- if iv
476
- header << [iv.length].pack('v')
477
- header << iv
478
- end
479
- if key
480
- encrypted = SymmetricEncryption.cipher(version).binary_encrypt(key, false, false, false)
481
- header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING)
482
- header << encrypted
483
- end
484
- if cipher_name
485
- header << [cipher_name.length].pack('v')
486
- header << cipher_name
487
- end
488
- header
489
- end
490
-
491
- # Advanced use only
492
- #
493
- # Returns a Binary encrypted string without applying any Base64, or other encoding
494
- #
495
- # add_header [nil|true|false]
496
- # Whether to add a header to the encrypted string
497
- # If not supplied it defaults to true if always_add_header || random_iv || compress
498
- # Default: nil
233
+ # compress [true|false]
234
+ # Whether to compress str before encryption.
235
+ # Default: false
236
+ # Notes:
237
+ # * Should only be used for large strings since compression overhead and
238
+ # the overhead of adding the encryption header may exceed any benefits of
239
+ # compression
499
240
  #
500
- # Creates a new OpenSSL::Cipher with every call so that this call
501
- # is thread-safe
241
+ # header [true|false]
242
+ # Whether to add a header to the encrypted string.
243
+ # Default: `always_add_header`
502
244
  #
503
- # See #encrypt to encrypt and encode the result as a string
504
- def binary_encrypt(str, random_iv=false, compress=false, add_header=nil)
245
+ # See #encrypt to encrypt and encode the result as a string.
246
+ def binary_encrypt(str, random_iv: false, compress: false, header: always_add_header)
505
247
  return if str.nil?
506
248
  string = str.to_s
507
249
  return string if string.empty?
508
250
 
509
- # Creates a new OpenSSL::Cipher with every call so that this call
510
- # is thread-safe
511
- openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
251
+ # Header required when adding a random_iv or compressing
252
+ header = Header.new(version: version, compress: compress) if (header == true) || random_iv || compress
253
+
254
+ # Creates a new OpenSSL::Cipher with every call so that this call is thread-safe.
255
+ openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
512
256
  openssl_cipher.encrypt
513
257
  openssl_cipher.key = @key
514
- add_header = always_add_header || random_iv || compress if add_header.nil?
515
- result =
516
- if add_header
517
- # Random iv and compress both add the magic header
518
- iv = random_iv ? openssl_cipher.random_iv : @iv
519
- openssl_cipher.iv = iv if iv
520
- # Set the binary indicator on the header if string is Binary Encoded
521
- self.class.build_header(version, compress, random_iv ? iv : nil, nil, nil) +
522
- openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string)
258
+
259
+ result =
260
+ if header
261
+ if random_iv
262
+ openssl_cipher.iv = header.iv = openssl_cipher.random_iv
263
+ elsif self.iv
264
+ openssl_cipher.iv = self.iv
265
+ end
266
+ header.to_s + openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string)
523
267
  else
524
- openssl_cipher.iv = @iv if defined?(@iv) && @iv
268
+ openssl_cipher.iv = iv if iv
525
269
  openssl_cipher.update(string)
526
270
  end
527
271
  result << openssl_cipher.final
@@ -542,7 +286,7 @@ module SymmetricEncryption
542
286
  # encrypted_string [String]
543
287
  # Binary encrypted string to decrypt
544
288
  #
545
- # header [HeaderStruct]
289
+ # header [SymmetricEncryption::Header]
546
290
  # Optional header for the supplied encrypted_string
547
291
  #
548
292
  # Reads the 'magic' header if present for key, iv, cipher_name and compression
@@ -556,57 +300,56 @@ module SymmetricEncryption
556
300
  # Note:
557
301
  # When a string is encrypted and the header is used, its decrypted form
558
302
  # is automatically set to the same UTF-8 or Binary encoding
559
- def binary_decrypt(encrypted_string, header=nil)
303
+ def binary_decrypt(encrypted_string, header: Header.new)
560
304
  return if encrypted_string.nil?
561
305
  str = encrypted_string.to_s
562
- str.force_encoding(SymmetricEncryption::BINARY_ENCODING) if str.respond_to?(:force_encoding)
306
+ str.force_encoding(SymmetricEncryption::BINARY_ENCODING)
563
307
  return str if str.empty?
564
308
 
565
- if header || self.class.has_header?(str)
566
- str = str.dup
567
- header ||= self.class.parse_header!(str)
568
-
569
- openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || self.cipher_name)
570
- openssl_cipher.decrypt
571
- openssl_cipher.key = header.key || @key
572
- iv = header.iv || @iv
573
- openssl_cipher.iv = iv if iv
574
- result = openssl_cipher.update(str)
575
- result << openssl_cipher.final
576
- header.compressed ? Zlib::Inflate.inflate(result) : result
577
- else
578
- openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
579
- openssl_cipher.decrypt
580
- openssl_cipher.key = @key
581
- openssl_cipher.iv = @iv if @iv
582
- result = openssl_cipher.update(str)
583
- result << openssl_cipher.final
309
+ offset = header.parse(str)
310
+ data = offset > 0 ? str[offset..-1] : str
311
+
312
+ openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || cipher_name)
313
+ openssl_cipher.decrypt
314
+ openssl_cipher.key = header.key || @key
315
+ if iv = (header.iv || @iv)
316
+ openssl_cipher.iv = iv
584
317
  end
318
+ result = openssl_cipher.update(data)
319
+ result << openssl_cipher.final
320
+ header.compressed? ? Zlib::Inflate.inflate(result) : result
321
+ end
322
+
323
+ # Returns the magic header after applying the encoding in this cipher
324
+ def encoded_magic_header
325
+ @encoded_magic_header ||= encoder.encode(SymmetricEncryption::Header::MAGIC_HEADER).gsub('=', '').strip
585
326
  end
586
327
 
587
328
  # Returns [String] object represented as a string, filtering out the key
588
329
  def inspect
589
- "#<#{self.class}:0x#{self.__id__.to_s(16)} @key=\"[FILTERED]\" @iv=#{iv.inspect} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}, @always_add_header=#{always_add_header.inspect}"
330
+ "#<#{self.class}:0x#{self.__id__.to_s(16)} @key=\"[FILTERED]\" @iv=#{iv.inspect} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}, @always_add_header=#{always_add_header.inspect}>"
590
331
  end
591
332
 
592
- private
593
-
594
- attr_reader :key
333
+ # DEPRECATED
334
+ def self.has_header?(buffer)
335
+ SymmetricEncryption::Header.present?(buffer)
336
+ end
595
337
 
596
- # Read from the file, raising an exception if it is not found
597
- def self.read_from_file(file_name)
598
- File.open(file_name, 'rb') { |f| f.read }
599
- rescue Errno::ENOENT => exc
600
- puts "\nSymmetric Encryption key file: '#{file_name}' not found or readable."
601
- puts "To generate the keys for the first time run: bin/rails generate symmetric_encryption:new_keys production\n\n"
602
- raise(exc)
338
+ # DEPRECATED
339
+ def self.parse_header!(buffer)
340
+ header = SymmetricEncryption::Header.new
341
+ header.parse!(buffer) ? header : nil
603
342
  end
604
343
 
605
- # Write to the supplied filename, backing up the existing file if present
606
- def self.write_to_file(file_name, data)
607
- File.rename(file_name, "#{file_name}.#{Time.now.to_i}") if File.exist?(file_name)
608
- File.open(file_name, 'wb') { |file| file.write(data) }
344
+ # DEPRECATED
345
+ def self.build_header(version, compress = false, iv = nil, key = nil, cipher_name = nil)
346
+ h = Header.new(version: version, compress: compress, iv: iv, key: key, cipher_name: cipher_name)
347
+ h.to_s
609
348
  end
610
349
 
350
+ private
351
+
352
+ attr_reader :key
353
+
611
354
  end
612
355
  end