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
@@ -0,0 +1,124 @@
1
+ module SymmetricEncryption
2
+ module Keystore
3
+ #@formatter:off
4
+ autoload :Environment, 'symmetric_encryption/keystore/environment'
5
+ autoload :File, 'symmetric_encryption/keystore/file'
6
+ autoload :Memory, 'symmetric_encryption/keystore/memory'
7
+ #@formatter:on
8
+
9
+ # Returns [Hash] a new configuration file after performing key rotation.
10
+ #
11
+ # Perform key rotation for each of the environments in the configuration file, by
12
+ # * generating a new key, and iv with an incremented version number.
13
+ #
14
+ # Params:
15
+ # config: [Hash]
16
+ # The current contents of `symmetric-encryption.yml`.
17
+ #
18
+ # environments: [Array<String>]
19
+ # List of environments for which to perform key rotation for.
20
+ # Default: All environments found in the current configuration file except development and test.
21
+ #
22
+ # rolling_deploy: [true|false]
23
+ # To support a rolling deploy of the new key it must added initially as the second key.
24
+ # Then in a subsequent deploy the key can be moved into the first position to activate it.
25
+ # In this way during a rolling deploy encrypted values written by updated servers will be readable
26
+ # by the servers that have not been updated yet.
27
+ # Default: false
28
+ #
29
+ # Notes:
30
+ # * iv_filename is no longer supported and is removed when creating a new random cipher.
31
+ # * `iv` does not need to be encrypted and is included in the clear.
32
+ def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false)
33
+ full_config.each_pair do |environment, cfg|
34
+ # Only rotate keys for specified environments. Default, all
35
+ next if !environments.empty? && !environments.include?(environment.to_sym)
36
+
37
+ # Find the highest version number
38
+ version = cfg[:ciphers].collect { |c| c[:version] || 0 }.max
39
+
40
+ config = cfg[:ciphers].first
41
+
42
+ # Only generate new keys for keystore's that have a key encrypting key
43
+ next unless config[:key_encrypting_key] || config[:private_rsa_key]
44
+
45
+ cipher_name = config[:cipher_name] || 'aes-256-cbc'
46
+ new_key_config =
47
+ if config.has_key?(:key_filename)
48
+ key_path = ::File.dirname(config[:key_filename])
49
+ Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
50
+ elsif config.has_key?(:key_env_var)
51
+ Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
52
+ elsif config.has_key?(:encrypted_key)
53
+ Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
54
+ end
55
+
56
+ # Add as second key so that key can be published now and only used in a later deploy.
57
+ if rolling_deploy
58
+ cfg[:ciphers].insert(1, new_key_config)
59
+ else
60
+ cfg[:ciphers].unshift(new_key_config)
61
+ end
62
+ end
63
+ full_config
64
+ end
65
+
66
+ # Rotates just the key encrypting keys for the current cipher version.
67
+ # The existing data encryption key is not changed, it is secured using the
68
+ # new key encrypting keys.
69
+ def self.rotate_key_encrypting_keys!(full_config, environments: [], app_name:)
70
+ full_config.each_pair do |environment, cfg|
71
+ # Only rotate keys for specified environments. Default, all
72
+ next if !environments.empty? && !environments.include?(environment.to_sym)
73
+
74
+ config = cfg[:ciphers].first
75
+
76
+ version = config.delete(:version) || 1
77
+ version -= 1
78
+ config.delete(:always_add_header)
79
+ config.delete(:encoding)
80
+
81
+ Key.migrate_config!(config)
82
+
83
+ # Only generate new keys for keystore's that have a key encrypting key
84
+ next unless config[:key_encrypting_key]
85
+
86
+ # The current data encrypting key without any of the key encrypting keys.
87
+ key = Key.from_config(config)
88
+ cipher_name = key.cipher_name
89
+ new_key_config =
90
+ if config.has_key?(:key_filename)
91
+ key_path = ::File.dirname(config[:key_filename])
92
+ Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
93
+ elsif config.has_key?(:key_env_var)
94
+ Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
95
+ elsif config.has_key?(:encrypted_key)
96
+ Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
97
+ end
98
+
99
+ new_key_config
100
+
101
+ # Replace existing config entry
102
+ cfg[:ciphers].shift
103
+ cfg[:ciphers].unshift(new_key_config)
104
+ end
105
+ full_config
106
+ end
107
+
108
+ # The default development config.
109
+ def self.dev_config
110
+ {
111
+ ciphers:
112
+ [
113
+ {
114
+ key: '1234567890ABCDEF',
115
+ iv: '1234567890ABCDEF',
116
+ cipher_name: 'aes-128-cbc',
117
+ version: 1
118
+ }
119
+ ]
120
+ }
121
+ end
122
+
123
+ end
124
+ end
@@ -16,10 +16,6 @@ module SymmetricEncryption #:nodoc:
16
16
  # end
17
17
  config.symmetric_encryption = ::SymmetricEncryption
18
18
 
19
- rake_tasks do
20
- load 'symmetric_encryption/railties/symmetric_encryption.rake'
21
- end
22
-
23
19
  # Initialize Symmetry. This will look for a symmetry.yml in the config
24
20
  # directory and configure Symmetry appropriately.
25
21
  #
@@ -35,18 +31,20 @@ module SymmetricEncryption #:nodoc:
35
31
  config.before_configuration do
36
32
  # Check if already configured
37
33
  unless ::SymmetricEncryption.cipher?
34
+ app_name = Rails::Application.subclasses.first.parent.to_s.underscore
38
35
  config_file = Rails.root.join('config', 'symmetric-encryption.yml')
39
36
  if config_file.file?
40
37
  begin
41
- ::SymmetricEncryption::Config.load!(config_file, Rails.env)
38
+ ::SymmetricEncryption::Config.load!(file_name: config_file, env: Rails.env)
42
39
  rescue ArgumentError => exc
43
40
  puts "\nSymmetric Encryption not able to read keys."
44
41
  puts "#{exc.class.name} #{exc.message}"
45
- puts "To generate key files: bin/rails generate symmetric_encryption:new_keys #{Rails.env}\n\n"
42
+ puts "To generate a new config file and key files: symmetric-encryption --generate --key-path /etc/#{app_name} --app_name #{app_name}\n\n"
43
+ raise(exc)
46
44
  end
47
45
  else
48
46
  puts "\nSymmetric Encryption config not found."
49
- puts "To generate one for the first time: bin/rails generate symmetric_encryption:config\n\n"
47
+ puts "To generate a new config file and key files: symmetric-encryption --generate --key-path /etc/#{app_name} --app_name #{app_name}\n\n"
50
48
  end
51
49
  end
52
50
  end
@@ -10,32 +10,15 @@ module SymmetricEncryption
10
10
  # Open a file for reading, or use the supplied IO Stream
11
11
  #
12
12
  # Parameters:
13
- # filename_or_stream:
14
- # The filename to open if a string, otherwise the stream to use
13
+ # file_name_or_stream:
14
+ # The file_name to open if a string, otherwise the stream to use
15
15
  # The file or stream will be closed on completion, use .initialize to
16
16
  # avoid having the stream closed automatically
17
17
  #
18
- # options:
19
- # :mode
20
- # See File.open for open modes
21
- # Default: 'rb'
22
- #
23
- # :buffer_size
24
- # Amount of data to read at a time
25
- # Minimum Value 128
26
- # Default: 4096
27
- #
28
- # The following options are only used if the stream/file has no header
29
- # :compress [true|false]
30
- # Uses Zlib to decompress the data after it is decrypted
31
- # Note: This option is only used if the file does not have a header
32
- # indicating whether it is compressed
33
- # Default: false
34
- #
35
- # :version
36
- # Version of the encryption key to use when decrypting and the
37
- # file/stream does not include a header at the beginning
38
- # Default: Current primary key
18
+ # buffer_size:
19
+ # Amount of data to read at a time.
20
+ # Minimum Value 128
21
+ # Default: 16384
39
22
  #
40
23
  # Note: Decryption occurs before decompression
41
24
  #
@@ -76,30 +59,66 @@ module SymmetricEncryption
76
59
  # ensure
77
60
  # csv.close if csv
78
61
  # end
79
- def self.open(filename_or_stream, options={}, &block)
80
- raise(ArgumentError, 'options must be a hash') unless options.respond_to?(:each_pair)
81
- mode = options.fetch(:mode, 'rb')
82
- compress = options.fetch(:compress, false)
83
- ios = filename_or_stream.is_a?(String) ? ::File.open(filename_or_stream, mode) : filename_or_stream
62
+ def self.open(file_name_or_stream, buffer_size: 16384, **args, &block)
63
+ ios = file_name_or_stream.is_a?(String) ? ::File.open(file_name_or_stream, 'rb') : file_name_or_stream
84
64
 
85
65
  begin
86
- file = self.new(ios, options)
87
- file = Zlib::GzipReader.new(file) if !file.eof? && (file.compressed? || compress)
66
+ file = self.new(ios, buffer_size: buffer_size, **args)
67
+ file = Zlib::GzipReader.new(file) if !file.eof? && file.compressed?
88
68
  block ? block.call(file) : file
89
69
  ensure
90
70
  file.close if block && file && (file.respond_to?(:closed?) && !file.closed?)
91
71
  end
92
72
  end
93
73
 
74
+ # Read the entire contents of a file or stream into memory.
75
+ #
76
+ # Notes:
77
+ # * Do not use this method for reading large files.
78
+ def self.read(file_name_or_stream, **args)
79
+ open(file_name_or_stream, **args) { |f| f.read }
80
+ end
81
+
82
+ # Decrypt an entire file.
83
+ #
84
+ # Returns [Integer] the number of unencrypted bytes written to the target file.
85
+ #
86
+ # Params:
87
+ # source: [String|IO]
88
+ # Source file_name or IOStream
89
+ #
90
+ # target: [String|IO]
91
+ # Target file_name or IOStream
92
+ #
93
+ # block_size: [Integer]
94
+ # Number of bytes to read into memory for each read.
95
+ # For very large files using a larger block size is faster.
96
+ # Default: 65535
97
+ #
98
+ # Notes:
99
+ # * The file contents are streamed so that the entire file is _not_ loaded into memory.
100
+ def self.decrypt(source:, target:, block_size: 65535, **args)
101
+ target_ios = target.is_a?(String) ? ::File.open(target, 'wb') : target
102
+ bytes_written = 0
103
+ open(source, **args) do |input_ios|
104
+ while !input_ios.eof?
105
+ bytes_written += target_ios.write(input_ios.read(block_size))
106
+ end
107
+ end
108
+ bytes_written
109
+ ensure
110
+ target_ios.close if target_ios && target_ios.respond_to?(:closed?) && !target_ios.closed?
111
+ end
112
+
94
113
  # Returns [true|false] whether the file or stream contains any data
95
114
  # excluding the header should it have one
96
- def self.empty?(filename_or_stream)
97
- open(filename_or_stream) { |file| file.eof? }
115
+ def self.empty?(file_name_or_stream)
116
+ open(file_name_or_stream) { |file| file.eof? }
98
117
  end
99
118
 
100
119
  # Returns [true|false] whether the file contains the encryption header
101
- def self.header_present?(filename)
102
- ::File.open(filename, 'rb') { |file| new(file).header_present? }
120
+ def self.header_present?(file_name)
121
+ ::File.open(file_name, 'rb') { |file| new(file).header_present? }
103
122
  end
104
123
 
105
124
  # After opening a file Returns [true|false] whether the file being
@@ -109,10 +128,10 @@ module SymmetricEncryption
109
128
  end
110
129
 
111
130
  # Decrypt data before reading from the supplied stream
112
- def initialize(ios, options={})
131
+ def initialize(ios, buffer_size: 4096, version: nil)
113
132
  @ios = ios
114
- @buffer_size = options.fetch(:buffer_size, 4096).to_i
115
- @version = options[:version]
133
+ @buffer_size = buffer_size
134
+ @version = version
116
135
  @header_present = false
117
136
  @closed = false
118
137
 
@@ -316,33 +335,33 @@ module SymmetricEncryption
316
335
 
317
336
  # Read the header from the file if present
318
337
  def read_header
319
- @pos = 0
338
+ @pos = 0
320
339
 
321
340
  # Read first block and check for the header
322
- buf = @ios.read(@buffer_size)
341
+ buf = @ios.read(@buffer_size)
323
342
 
324
343
  # Use cipher specified in header, or global cipher if it has no header
325
- iv, key = nil
326
- cipher_name = nil
327
- decryption_cipher = nil
328
- if header = SymmetricEncryption::Cipher.parse_header!(buf)
329
- @header_present = true
330
- @compressed = header.compressed
331
- decryption_cipher = header.decryption_cipher
332
- cipher_name = header.cipher_name || decryption_cipher.cipher_name
333
- key = header.key
334
- iv = header.iv
344
+ iv, key, cipher_name, cipher = nil
345
+ header = Header.new
346
+ if header.parse!(buf)
347
+ @header_present = true
348
+ @compressed = header.compressed?
349
+ @version = header.version
350
+ cipher = header.cipher
351
+ cipher_name = header.cipher_name || cipher.cipher_name
352
+ key = header.key
353
+ iv = header.iv
335
354
  else
336
- @header_present = false
337
- @compressed = nil
338
- decryption_cipher = SymmetricEncryption.cipher(@version)
339
- cipher_name = decryption_cipher.cipher_name
355
+ @header_present = false
356
+ @compressed = nil
357
+ cipher = SymmetricEncryption.cipher(@version)
358
+ cipher_name = cipher.cipher_name
340
359
  end
341
360
 
342
361
  @stream_cipher = ::OpenSSL::Cipher.new(cipher_name)
343
362
  @stream_cipher.decrypt
344
- @stream_cipher.key = key || decryption_cipher.send(:key)
345
- @stream_cipher.iv = iv || decryption_cipher.iv
363
+ @stream_cipher.key = key || cipher.send(:key)
364
+ @stream_cipher.iv = iv || cipher.iv
346
365
 
347
366
  # First call to #update should return an empty string anyway
348
367
  if buf && buf.length > 0
@@ -0,0 +1,24 @@
1
+ require 'openssl'
2
+ module SymmetricEncryption
3
+ # DEPRECATED - Internal use only
4
+ class RSAKey
5
+ # DEPRECATED - Internal use only
6
+ def initialize(private_rsa_key)
7
+ @rsa = OpenSSL::PKey::RSA.new(private_rsa_key)
8
+ end
9
+
10
+ # DEPRECATED - Internal use only
11
+ def encrypt(key)
12
+ rsa.public_encrypt(key)
13
+ end
14
+
15
+ # DEPRECATED - Internal use only
16
+ def decrypt(encrypted_key)
17
+ rsa.private_decrypt(encrypted_key)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :rsa
23
+ end
24
+ end
@@ -26,7 +26,7 @@ module SymmetricEncryption
26
26
  # :date => Date
27
27
  # :json => Uses JSON serialization, useful for hashes and arrays
28
28
  # :yaml => Uses YAML serialization, useful for hashes and arrays
29
- COERCION_TYPES = [:string, :integer, :float, :decimal, :datetime, :time, :date, :boolean, :json, :yaml]
29
+ COERCION_TYPES = [:string, :integer, :float, :decimal, :datetime, :time, :date, :boolean, :json, :yaml]
30
30
 
31
31
  # Set the Primary Symmetric Cipher to be used
32
32
  #
@@ -39,6 +39,7 @@ module SymmetricEncryption
39
39
  # )
40
40
  def self.cipher=(cipher)
41
41
  raise(ArgumentError, 'Cipher must respond to :encrypt and :decrypt') unless cipher.nil? || (cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt))
42
+
42
43
  @@cipher = cipher
43
44
  end
44
45
 
@@ -47,7 +48,8 @@ module SymmetricEncryption
47
48
  # Returns the primary cipher if no match was found and version == 0
48
49
  # Returns nil if no match was found and version != 0
49
50
  def self.cipher(version = nil)
50
- raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher
51
+ raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless cipher?
52
+
51
53
  return @@cipher if version.nil? || (@@cipher.version == version)
52
54
  secondary_ciphers.find { |c| c.version == version } || (@@cipher if version == 0)
53
55
  end
@@ -60,6 +62,7 @@ module SymmetricEncryption
60
62
  # Set the Secondary Symmetric Ciphers Array to be used
61
63
  def self.secondary_ciphers=(secondary_ciphers)
62
64
  raise(ArgumentError, 'secondary_ciphers must be a collection') unless secondary_ciphers.respond_to? :each
65
+
63
66
  secondary_ciphers.each do |cipher|
64
67
  raise(ArgumentError, 'secondary_ciphers can only consist of SymmetricEncryption::Ciphers') unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt)
65
68
  end
@@ -71,17 +74,18 @@ module SymmetricEncryption
71
74
  @@secondary_ciphers
72
75
  end
73
76
 
74
- # AES Symmetric Decryption of supplied string
75
- # Returns decrypted value
76
- # Returns nil if the supplied value is nil
77
- # Returns "" if it is a string and it is empty
77
+ # Decrypt supplied string.
78
+ #
79
+ # Returns [String] the decrypted string.
80
+ # Returns [nil] if the supplied value is nil.
81
+ # Returns [''] if it is a string and it is empty.
78
82
  #
79
83
  # Parameters
80
- # str
81
- # Encrypted string to decrypt
82
- # version
84
+ # string [String]
85
+ # Encrypted string to decrypt.
86
+ # version [Integer]
83
87
  # Specify which cipher version to use if no header is present on the
84
- # encrypted string
88
+ # encrypted string.
85
89
  # type [:string|:integer|:float|:decimal|:datetime|:time|:date|:boolean]
86
90
  # If value is set to something other than :string, then the coercible gem
87
91
  # will be use to coerce the unencrypted string value into the specified
@@ -105,23 +109,32 @@ module SymmetricEncryption
105
109
  # yet significant number of cases it is possible to decrypt data using
106
110
  # the incorrect key. Clearly the data returned is garbage, but it still
107
111
  # successfully returns a string of data
108
- def self.decrypt(encrypted_and_encoded_string, version=nil, type=:string)
109
- raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher
112
+ def self.decrypt(encrypted_and_encoded_string, version: nil, type: :string)
110
113
  return encrypted_and_encoded_string if encrypted_and_encoded_string.nil? || (encrypted_and_encoded_string == '')
111
114
 
112
- str = encrypted_and_encoded_string.to_s
115
+ str = encrypted_and_encoded_string.to_s
113
116
 
114
117
  # Decode before decrypting supplied string
115
- decoded = @@cipher.decode(str)
118
+ decoded = cipher.decode(str)
116
119
  return unless decoded
117
120
  return decoded if decoded.empty?
118
121
 
122
+ header = Header.new
119
123
  decrypted =
120
- if header = Cipher.parse_header!(decoded)
121
- header.decryption_cipher.binary_decrypt(decoded, header)
124
+ if header.parse!(decoded)
125
+ header.cipher.binary_decrypt(decoded, header: header)
122
126
  else
123
- # Use cipher_selector if present to decide which cipher to use
124
- c = @@select_cipher.nil? ? cipher(version) : @@select_cipher.call(str, decoded)
127
+ c =
128
+ if version
129
+ # Supplied version takes preference
130
+ cipher(version)
131
+ elsif @@select_cipher
132
+ # Use cipher_selector if present to decide which cipher to use
133
+ @@select_cipher.call(str, decoded)
134
+ else
135
+ # Global cipher
136
+ cipher
137
+ end
125
138
  c.binary_decrypt(decoded)
126
139
  end
127
140
 
@@ -132,6 +145,19 @@ module SymmetricEncryption
132
145
  Coerce.coerce_from_string(decrypted, type)
133
146
  end
134
147
 
148
+ # Returns the header for the encrypted string
149
+ # Returns [nil] if no header is present
150
+ def self.header(encrypted_and_encoded_string)
151
+ return if encrypted_and_encoded_string.nil? || (encrypted_and_encoded_string == '')
152
+
153
+ # Decode before decrypting supplied string
154
+ decoded = cipher.encoder.decode(encrypted_and_encoded_string.to_s)
155
+ return if decoded.nil? || decoded.empty?
156
+
157
+ h = Header.new
158
+ h.parse(decoded) == 0 ? nil : h
159
+ end
160
+
135
161
  # AES Symmetric Encryption of supplied string
136
162
  # Returns result as a Base64 encoded string
137
163
  # Returns nil if the supplied str is nil
@@ -174,11 +200,11 @@ module SymmetricEncryption
174
200
  # Note: If type is set to something other than :string, it's expected that
175
201
  # the coercible gem is available in the path.
176
202
  # Default: :string
177
- def self.encrypt(str, random_iv=false, compress=false, type=:string)
178
- raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher
203
+ def self.encrypt(str, random_iv: false, compress: false, type: :string, header: cipher.always_add_header)
204
+ return str if str.nil? || (str == '')
179
205
 
180
206
  # Encrypt and then encode the supplied string
181
- @@cipher.encrypt(Coerce.coerce_to_string(str, type), random_iv, compress)
207
+ cipher.encrypt(Coerce.coerce_to_string(str, type), random_iv: random_iv, compress: compress, header: header)
182
208
  end
183
209
 
184
210
  # Invokes decrypt
@@ -193,27 +219,21 @@ module SymmetricEncryption
193
219
  # WARNING: It is possible to decrypt data using the wrong key, so the value
194
220
  # returned should not be relied upon
195
221
  def self.try_decrypt(str)
196
- raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher
197
- begin
198
- decrypt(str)
199
- rescue OpenSSL::Cipher::CipherError, SymmetricEncryption::CipherError
200
- nil
201
- end
222
+ decrypt(str)
223
+ rescue OpenSSL::Cipher::CipherError, SymmetricEncryption::CipherError
224
+ nil
202
225
  end
203
226
 
204
- # Returns [true|false] as to whether the data could be decrypted
205
- # Parameters:
206
- # encrypted_data: Encrypted string
227
+ # Returns [true|false] whether the string is encrypted.
207
228
  #
208
- # WARNING: This method can only be relied upon if the encrypted data includes the
209
- # symmetric encryption header. In some cases data decrypted using the
210
- # wrong key will decrypt and return garbage
229
+ # Notes:
230
+ # * This method only works reliably when the encrypted data includes the symmetric encryption header.
231
+ # * nil and '' are considered "encrypted" so that validations do not blow up on empty values.
211
232
  def self.encrypted?(encrypted_data)
212
- raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher
233
+ return false if encrypted_data.nil? || (encrypted_data == '')
213
234
 
214
- # For now have to decrypt it fully
215
- result = try_decrypt(encrypted_data)
216
- !(result.nil? || result == '')
235
+ @header ||= SymmetricEncryption.cipher.encoded_magic_header
236
+ encrypted_data.to_s.start_with?(@header)
217
237
  end
218
238
 
219
239
  # When no header is present in the encrypted data, this custom Block/Proc is
@@ -246,79 +266,21 @@ module SymmetricEncryption
246
266
  end
247
267
 
248
268
  # Load the Encryption Configuration from a YAML file
249
- # filename:
269
+ # file_name:
250
270
  # Name of file to read.
251
271
  # Mandatory for non-Rails apps
252
272
  # Default: Rails.root/config/symmetric-encryption.yml
253
273
  # environment:
254
274
  # Which environments config to load. Usually: production, development, etc.
255
275
  # Default: Rails.env
256
- def self.load!(filename = nil, environment = nil)
257
- Config.load!(filename, environment)
258
- end
259
-
260
- # Generate new random symmetric keys for use with this Encryption library
261
- #
262
- # Note: Only the current Encryption key settings are used
263
- #
264
- # Creates Symmetric Key .key and initialization vector .iv
265
- # which is encrypted with the key encryption key.
266
- #
267
- # Existing key files will be renamed if present
268
- def self.generate_symmetric_key_files(filename = nil, environment = nil)
269
- config = Config.read_config(filename, environment)
270
-
271
- # Only regenerating the first configured cipher
272
- cipher_config = config[:ciphers].first
273
-
274
- # Delete unused config keys to generate new random keys
275
- [:version, :always_add_header].each do |key|
276
- cipher_config.delete(key)
277
- end
278
-
279
- key_config = {private_rsa_key: config[:private_rsa_key]}
280
- cipher_cfg = Cipher.generate_random_keys(key_config.merge(cipher_config))
281
-
282
- puts
283
- if encoded_encrypted_key = cipher_cfg[:encrypted_key]
284
- puts 'If running in Heroku, add the environment specific key:'
285
- puts "heroku config:add #{environment.upcase}_KEY1=#{encoded_encrypted_key}\n"
286
- end
287
-
288
- if encoded_encrypted_iv = cipher_cfg[:encrypted_iv]
289
- puts 'If running in Heroku, add the environment specific key:'
290
- puts "heroku config:add #{environment.upcase}_IV1=#{encoded_encrypted_iv}"
291
- end
292
-
293
- if key = cipher_cfg[:key]
294
- puts "Please add the key: #{key} to your config file"
295
- end
296
-
297
- if iv = cipher_cfg[:iv]
298
- puts "Please add the iv: #{iv} to your config file"
299
- end
300
-
301
- if file_name = cipher_cfg[:key_filename]
302
- puts("Please copy #{file_name} to the other servers in #{environment}.")
303
- end
304
-
305
- if file_name = cipher_cfg[:iv_filename]
306
- puts("Please copy #{file_name} to the other servers in #{environment}.")
307
- end
308
- cipher_cfg
309
- end
310
-
311
- # Generate a 22 character random password
312
- def self.random_password
313
- Base64.encode64(OpenSSL::Cipher.new('aes-128-cbc').random_key)[0..-4].strip
276
+ def self.load!(file_name = nil, env = nil)
277
+ Config.load!(file_name: file_name, env: env)
314
278
  end
315
279
 
316
- # Binary encrypted data includes this magic header so that we can quickly
317
- # identify binary data versus base64 encoded data that does not have this header
318
- unless defined? MAGIC_HEADER
319
- MAGIC_HEADER = '@EnC'
320
- MAGIC_HEADER_SIZE = MAGIC_HEADER.size
321
- MAGIC_HEADER_UNPACK = "a#{MAGIC_HEADER_SIZE}v"
280
+ # Generate a Random password
281
+ def self.random_password(size = 22)
282
+ require 'securerandom' unless defined?(SecureRandom)
283
+ SecureRandom.urlsafe_base64(size)
322
284
  end
323
285
 
324
286
  BINARY_ENCODING = Encoding.find('binary')