symmetric-encryption 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -7
  3. data/lib/symmetric_encryption/cipher.rb +11 -4
  4. data/lib/symmetric_encryption/cli.rb +39 -28
  5. data/lib/symmetric_encryption/config.rb +9 -6
  6. data/lib/symmetric_encryption/encoder.rb +6 -0
  7. data/lib/symmetric_encryption/extensions/mongoid/encrypted.rb +1 -0
  8. data/lib/symmetric_encryption/generator.rb +1 -1
  9. data/lib/symmetric_encryption/header.rb +7 -5
  10. data/lib/symmetric_encryption/key.rb +2 -62
  11. data/lib/symmetric_encryption/keystore/aws.rb +172 -0
  12. data/lib/symmetric_encryption/keystore/environment.rb +7 -30
  13. data/lib/symmetric_encryption/keystore/file.rb +8 -30
  14. data/lib/symmetric_encryption/keystore/heroku.rb +22 -0
  15. data/lib/symmetric_encryption/keystore/memory.rb +4 -3
  16. data/lib/symmetric_encryption/keystore.rb +151 -36
  17. data/lib/symmetric_encryption/railtie.rb +9 -4
  18. data/lib/symmetric_encryption/railties/symmetric_encryption_validator.rb +1 -0
  19. data/lib/symmetric_encryption/reader.rb +50 -58
  20. data/lib/symmetric_encryption/symmetric_encryption.rb +2 -1
  21. data/lib/symmetric_encryption/utils/aws.rb +141 -0
  22. data/lib/symmetric_encryption/utils/re_encrypt_files.rb +12 -5
  23. data/lib/symmetric_encryption/version.rb +1 -1
  24. data/lib/symmetric_encryption/writer.rb +33 -27
  25. data/lib/symmetric_encryption.rb +27 -6
  26. data/test/active_record_test.rb +25 -25
  27. data/test/cipher_test.rb +3 -3
  28. data/test/header_test.rb +1 -1
  29. data/test/key_test.rb +0 -157
  30. data/test/keystore/aws_test.rb +133 -0
  31. data/test/keystore/environment_test.rb +3 -51
  32. data/test/keystore/file_test.rb +13 -52
  33. data/test/keystore/heroku_test.rb +70 -0
  34. data/test/keystore_test.rb +200 -4
  35. data/test/mongoid_test.rb +15 -15
  36. data/test/reader_test.rb +28 -8
  37. data/test/symmetric_encryption_test.rb +2 -2
  38. data/test/test_db.sqlite3 +0 -0
  39. data/test/test_helper.rb +1 -0
  40. data/test/utils/aws_test.rb +74 -0
  41. data/test/writer_test.rb +48 -46
  42. metadata +29 -20
@@ -4,41 +4,20 @@ module SymmetricEncryption
4
4
  class Environment < Memory
5
5
  attr_accessor :key_env_var, :encoding
6
6
 
7
- # Returns [Hash] initial configuration for heroku.
8
- # Displays the keys that need to be added to the heroku environment.
9
- def self.new_config(app_name: 'symmetric-encryption',
10
- environments: %i[development test release production],
11
- cipher_name: 'aes-256-cbc')
12
-
13
- configs = {}
14
- environments.each do |environment|
15
- environment = environment.to_sym
16
- configs[environment] =
17
- if %i[development test].include?(environment)
18
- Keystore.dev_config
19
- else
20
- cfg = new_key_config(cipher_name: cipher_name, app_name: app_name, environment: environment)
21
- {
22
- ciphers: [cfg]
23
- }
24
- end
25
- end
26
- configs
27
- end
28
-
29
- # Returns [Hash] a new cipher, and writes its encrypted key file.
7
+ # Returns [Hash] a new keystore configuration after generating the data key.
30
8
  #
31
9
  # Increments the supplied version number by 1.
32
- def self.new_key_config(cipher_name:, app_name:, environment:, version: 0, dek: nil)
10
+ def self.generate_data_key(cipher_name:, app_name:, environment:, version: 0, dek: nil)
33
11
  version >= 255 ? (version = 1) : (version += 1)
34
12
 
35
- kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
13
+ kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
36
14
  dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
37
15
 
38
16
  key_env_var = "#{app_name}_#{environment}_v#{version}".upcase.tr('-', '_')
39
17
  new(key_env_var: key_env_var, key_encrypting_key: kek).write(dek.key)
40
18
 
41
19
  {
20
+ keystore: :environment,
42
21
  cipher_name: dek.cipher_name,
43
22
  version: version,
44
23
  key_env_var: key_env_var,
@@ -62,6 +41,7 @@ module SymmetricEncryption
62
41
  def read
63
42
  encrypted = ENV[key_env_var]
64
43
  raise "The Environment Variable #{key_env_var} must be set with the encrypted encryption key." unless encrypted
44
+
65
45
  binary = encoder.decode(encrypted)
66
46
  key_encrypting_key.decrypt(binary)
67
47
  end
@@ -70,11 +50,8 @@ module SymmetricEncryption
70
50
  def write(key)
71
51
  encrypted_key = key_encrypting_key.encrypt(key)
72
52
  puts "\n\n********************************************************************************"
73
- puts "Add the environment key to Heroku:\n\n"
74
- puts " heroku config:add #{key_env_var}=#{encoder.encode(encrypted_key)}"
75
- puts
76
- puts "Or, if using environment variables on another system set the environment variable as follows:\n\n"
77
- puts " export #{key_env_var}=\"#{encoder.encode(encrypted_key)}\"\n\n"
53
+ puts 'Set the environment variable as follows:'
54
+ puts " export #{key_env_var}=\"#{encoder.encode(encrypted_key)}\""
78
55
  puts '********************************************************************************'
79
56
  end
80
57
 
@@ -3,46 +3,24 @@ module SymmetricEncryption
3
3
  class File
4
4
  attr_accessor :file_name, :key_encrypting_key
5
5
 
6
- # Returns [Hash] initial configuration.
7
- # Generates the encrypted key file for every environment except development and test.
8
- def self.new_config(key_path: '/etc/symmetric-encryption',
9
- app_name: 'symmetric-encryption',
10
- environments: %i[development test release production],
11
- cipher_name: 'aes-256-cbc')
12
-
13
- configs = {}
14
- environments.each do |environment|
15
- environment = environment.to_sym
16
- configs[environment] =
17
- if %i[development test].include?(environment)
18
- Keystore.dev_config
19
- else
20
- cfg = new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, environment: environment)
21
- {
22
- ciphers: [cfg]
23
- }
24
- end
25
- end
26
- configs
27
- end
28
-
29
- # Returns [Hash] a new cipher, and writes its encrypted key file.
6
+ # Returns [Hash] a new keystore configuration after generating the data key.
30
7
  #
31
8
  # Increments the supplied version number by 1.
32
- def self.new_key_config(key_path:, cipher_name:, app_name:, environment:, version: 0, dek: nil)
9
+ def self.generate_data_key(key_path:, cipher_name:, app_name:, environment:, version: 0, dek: nil)
33
10
  version >= 255 ? (version = 1) : (version += 1)
34
11
 
35
- dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
12
+ dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
36
13
  kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
37
14
  kekek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
38
15
 
39
16
  dek_file_name = ::File.join(key_path, "#{app_name}_#{environment}_v#{version}.encrypted_key")
40
- new(file_name: dek_file_name, key_encrypting_key: kek).write(dek.key)
17
+ new(key_filename: dek_file_name, key_encrypting_key: kek).write(dek.key)
41
18
 
42
19
  kekek_file_name = ::File.join(key_path, "#{app_name}_#{environment}_v#{version}.kekek")
43
- new(file_name: kekek_file_name).write(kekek.key)
20
+ new(key_filename: kekek_file_name).write(kekek.key)
44
21
 
45
22
  {
23
+ keystore: :file,
46
24
  cipher_name: dek.cipher_name,
47
25
  version: version,
48
26
  key_filename: dek_file_name,
@@ -60,8 +38,8 @@ module SymmetricEncryption
60
38
 
61
39
  # Stores the Encryption key in a file.
62
40
  # Secures the Encryption key by encrypting it with a key encryption key.
63
- def initialize(file_name:, key_encrypting_key: nil)
64
- @file_name = file_name
41
+ def initialize(key_filename:, key_encrypting_key: nil)
42
+ @file_name = key_filename
65
43
  @key_encrypting_key = key_encrypting_key
66
44
  end
67
45
 
@@ -0,0 +1,22 @@
1
+ module SymmetricEncryption
2
+ module Keystore
3
+ # Heroku uses environment variables too.
4
+ class Heroku < Environment
5
+ # Returns [Hash] a new keystore configuration after generating the data key.
6
+ def self.generate_data_key(**args)
7
+ config = super(**args)
8
+ config[:keystore] = :heroku
9
+ config
10
+ end
11
+
12
+ # Write the encrypted Encryption key to `encrypted_key` attribute.
13
+ def write(key)
14
+ encrypted_key = key_encrypting_key.encrypt(key)
15
+ puts "\n\n********************************************************************************"
16
+ puts "Add the environment key to Heroku:\n\n"
17
+ puts " heroku config:add #{key_env_var}=#{encoder.encode(encrypted_key)}"
18
+ puts '********************************************************************************'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -5,22 +5,23 @@ module SymmetricEncryption
5
5
  attr_accessor :key_encrypting_key
6
6
  attr_reader :encrypted_key
7
7
 
8
- # Returns [Hash] a new cipher, and writes its encrypted key file.
8
+ # Returns [Hash] a new keystore configuration after generating the data key.
9
9
  #
10
10
  # Increments the supplied version number by 1.
11
11
  #
12
12
  # Notes:
13
13
  # * For development and testing purposes only!!
14
14
  # * Never store the encrypted encryption key in the source code / config file.
15
- def self.new_key_config(cipher_name:, app_name:, environment:, version: 0, dek: nil)
15
+ def self.generate_data_key(cipher_name:, app_name:, environment:, version: 0, dek: nil)
16
16
  version >= 255 ? (version = 1) : (version += 1)
17
17
 
18
- kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
18
+ kek = SymmetricEncryption::Key.new(cipher_name: cipher_name)
19
19
  dek ||= SymmetricEncryption::Key.new(cipher_name: cipher_name)
20
20
 
21
21
  encrypted_key = new(key_encrypting_key: kek).write(dek.key)
22
22
 
23
23
  {
24
+ keystore: :memory,
24
25
  cipher_name: cipher_name,
25
26
  version: version,
26
27
  encrypted_key: encrypted_key,
@@ -2,11 +2,33 @@ module SymmetricEncryption
2
2
  # Encryption keys are secured in Keystores
3
3
  module Keystore
4
4
  # @formatter:off
5
+ autoload :Aws, 'symmetric_encryption/keystore/aws'
5
6
  autoload :Environment, 'symmetric_encryption/keystore/environment'
6
7
  autoload :File, 'symmetric_encryption/keystore/file'
8
+ autoload :Heroku, 'symmetric_encryption/keystore/heroku'
7
9
  autoload :Memory, 'symmetric_encryption/keystore/memory'
8
10
  # @formatter:on
9
11
 
12
+ # Returns [Hash] a new keystore configuration after generating data keys for each environment.
13
+ def self.generate_data_keys(keystore:, environments: %i[development test release production], **args)
14
+ keystore_class = keystore.is_a?(Symbol) || keystore.is_a?(String) ? constantize_symbol(keystore) : keystore
15
+
16
+ configs = {}
17
+ environments.each do |environment|
18
+ environment = environment.to_sym
19
+ configs[environment] =
20
+ if %i[development test].include?(environment)
21
+ dev_config
22
+ else
23
+ cfg = keystore_class.generate_data_key(environment: environment, **args)
24
+ {
25
+ ciphers: [cfg]
26
+ }
27
+ end
28
+ end
29
+ configs
30
+ end
31
+
10
32
  # Returns [Hash] a new configuration file after performing key rotation.
11
33
  #
12
34
  # Perform key rotation for each of the environments in the configuration file, by
@@ -27,10 +49,13 @@ module SymmetricEncryption
27
49
  # by the servers that have not been updated yet.
28
50
  # Default: false
29
51
  #
52
+ # keystore: [Symbol]
53
+ # If supplied, changes the keystore during key rotation.
54
+ #
30
55
  # Notes:
31
56
  # * iv_filename is no longer supported and is removed when creating a new random cipher.
32
57
  # * `iv` does not need to be encrypted and is included in the clear.
33
- def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false)
58
+ def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false, keystore: nil)
34
59
  full_config.each_pair do |environment, cfg|
35
60
  # Only rotate keys for specified environments. Default, all
36
61
  next if !environments.empty? && !environments.include?(environment.to_sym)
@@ -43,22 +68,24 @@ module SymmetricEncryption
43
68
  # Only generate new keys for keystore's that have a key encrypting key
44
69
  next unless config[:key_encrypting_key] || config[:private_rsa_key]
45
70
 
46
- cipher_name = config[:cipher_name] || 'aes-256-cbc'
47
- new_key_config =
48
- if config.key?(:key_filename)
49
- key_path = ::File.dirname(config[:key_filename])
50
- Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
51
- elsif config.key?(:key_env_var)
52
- Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
53
- elsif config.key?(:encrypted_key)
54
- Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment)
55
- end
71
+ cipher_name = config[:cipher_name] || 'aes-256-cbc'
72
+
73
+ keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(config)
74
+
75
+ args = {
76
+ cipher_name: cipher_name,
77
+ app_name: app_name,
78
+ version: version,
79
+ environment: environment
80
+ }
81
+ args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename)
82
+ new_data_key = keystore_class.generate_data_key(args)
56
83
 
57
84
  # Add as second key so that key can be published now and only used in a later deploy.
58
85
  if rolling_deploy
59
- cfg[:ciphers].insert(1, new_key_config)
86
+ cfg[:ciphers].insert(1, new_data_key)
60
87
  else
61
- cfg[:ciphers].unshift(new_key_config)
88
+ cfg[:ciphers].unshift(new_data_key)
62
89
  end
63
90
  end
64
91
  full_config
@@ -77,33 +104,35 @@ module SymmetricEncryption
77
104
  # Only generate new keys for keystore's that have a key encrypting key
78
105
  next unless config[:key_encrypting_key]
79
106
 
80
- version = config.delete(:version) || 1
107
+ version = config.delete(:version) || 1
81
108
  version -= 1
82
109
 
83
110
  always_add_header = config.delete(:always_add_header)
84
111
  encoding = config.delete(:encoding)
85
112
 
86
- Key.migrate_config!(config)
113
+ migrate_config!(config)
87
114
 
88
115
  # The current data encrypting key without any of the key encrypting keys.
89
- key = Key.from_config(config)
116
+ key = Keystore.read_key(config)
90
117
  cipher_name = key.cipher_name
91
- new_key_config =
92
- if config.key?(:key_filename)
93
- key_path = ::File.dirname(config[:key_filename])
94
- Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
95
- elsif config.key?(:key_env_var)
96
- Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
97
- elsif config.key?(:encrypted_key)
98
- Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key)
99
- end
118
+ keystore_class = keystore_for(config)
100
119
 
101
- new_key_config[:always_add_header] = always_add_header
102
- new_key_config[:encoding] = encoding
120
+ args = {
121
+ cipher_name: cipher_name,
122
+ app_name: app_name,
123
+ version: version,
124
+ environment: environment,
125
+ dek: key
126
+ }
127
+ args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename)
128
+
129
+ new_config = keystore_class.generate_data_key(args)
130
+ new_config[:always_add_header] = always_add_header
131
+ new_config[:encoding] = encoding
103
132
 
104
133
  # Replace existing config entry
105
134
  cfg[:ciphers].shift
106
- cfg[:ciphers].unshift(new_key_config)
135
+ cfg[:ciphers].unshift(new_config)
107
136
  end
108
137
  full_config
109
138
  end
@@ -112,15 +141,101 @@ module SymmetricEncryption
112
141
  def self.dev_config
113
142
  {
114
143
  ciphers:
115
- [
116
- {
117
- key: '1234567890ABCDEF',
118
- iv: '1234567890ABCDEF',
119
- cipher_name: 'aes-128-cbc',
120
- version: 1
121
- }
122
- ]
144
+ [
145
+ {
146
+ key: '1234567890ABCDEF',
147
+ iv: '1234567890ABCDEF',
148
+ cipher_name: 'aes-128-cbc',
149
+ version: 1
150
+ }
151
+ ]
123
152
  }
124
153
  end
154
+
155
+ # Returns [Key] by recursively navigating the config tree.
156
+ #
157
+ # Supports N level deep key encrypting keys.
158
+ def self.read_key(key: nil, iv:, key_encrypting_key: nil, cipher_name: 'aes-256-cbc', keystore: nil, version: 0, **args)
159
+ if key_encrypting_key.is_a?(Hash)
160
+ # Recurse up the chain returning the parent key_encrypting_key
161
+ key_encrypting_key = read_key(cipher_name: cipher_name, **key_encrypting_key)
162
+ end
163
+
164
+ unless key
165
+ keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(args)
166
+ store = keystore_class.new(key_encrypting_key: key_encrypting_key, **args)
167
+ key = store.read
168
+ end
169
+
170
+ Key.new(key: key, iv: iv, cipher_name: cipher_name)
171
+ end
172
+
173
+ #
174
+ # Internal use only methods
175
+ #
176
+
177
+ def self.keystore_for(config)
178
+ if config[:keystore]
179
+ constantize_symbol(config[:keystore])
180
+ elsif config[:encrypted_key]
181
+ Keystore::Memory
182
+ elsif config[:key_filename]
183
+ Keystore::File
184
+ elsif config[:key_env_var]
185
+ Keystore::Environment
186
+ else
187
+ raise(ArgumentError, 'Unknown keystore supplied in config')
188
+ end
189
+ end
190
+
191
+ def self.constantize_symbol(symbol, namespace = 'SymmetricEncryption::Keystore')
192
+ klass = "#{namespace}::#{camelize(symbol.to_s)}"
193
+ begin
194
+ Object.const_get(klass)
195
+ rescue NameError
196
+ raise(ArgumentError, "Keystore: #{symbol.inspect} not found. Looking for: #{klass}")
197
+ end
198
+ end
199
+
200
+ # Borrow from Rails, when not running Rails
201
+ def self.camelize(term)
202
+ string = term.to_s
203
+ string = string.sub(/^[a-z\d]*/, &:capitalize)
204
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
205
+ string.gsub!('/'.freeze, '::'.freeze)
206
+ string
207
+ end
208
+
209
+ # Migrate a prior config.
210
+ #
211
+ # Note:
212
+ # * The config cannot be saved back to the config file once
213
+ # migrated, without generating new Key Encrypting Keys.
214
+ # * Only run this migration in the target environment so that the
215
+ # current key encrypting files are present.
216
+ def self.migrate_config!(config)
217
+ # Backward compatibility - Deprecated
218
+ private_rsa_key = config.delete(:private_rsa_key)
219
+
220
+ # Migrate old encrypted_iv
221
+ if (encrypted_iv = config.delete(:encrypted_iv)) && private_rsa_key
222
+ encrypted_iv = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
223
+ config[:iv] = ::Base64.decode64(encrypted_iv)
224
+ end
225
+
226
+ # Migrate old iv_filename
227
+ if (file_name = config.delete(:iv_filename)) && private_rsa_key
228
+ encrypted_iv = ::File.read(file_name)
229
+ config[:iv] = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
230
+ end
231
+
232
+ # Backward compatibility - Deprecated
233
+ config[:key_encrypting_key] = RSAKey.new(private_rsa_key) if private_rsa_key
234
+
235
+ # Migrate old encrypted_key to new binary format
236
+ if (encrypted_key = config[:encrypted_key]) && private_rsa_key
237
+ config[:encrypted_key] = ::Base64.decode64(encrypted_key)
238
+ end
239
+ end
125
240
  end
126
241
  end
@@ -29,20 +29,25 @@ module SymmetricEncryption #:nodoc:
29
29
  config.before_configuration do
30
30
  # Check if already configured
31
31
  unless ::SymmetricEncryption.cipher?
32
- app_name = Rails::Application.subclasses.first.parent.to_s.underscore
33
- config_file = Rails.root.join('config', 'symmetric-encryption.yml')
32
+ app_name = Rails::Application.subclasses.first.parent.to_s.underscore
33
+ config_file =
34
+ if (env_var = ENV['SYMMETRIC_ENCRYPTION_CONFIG'])
35
+ Pathname.new File.expand_path(env_var)
36
+ else
37
+ Rails.root.join('config', 'symmetric-encryption.yml')
38
+ end
34
39
  if config_file.file?
35
40
  begin
36
41
  ::SymmetricEncryption::Config.load!(file_name: config_file, env: ENV['SYMMETRIC_ENCRYPTION_ENV'] || Rails.env)
37
42
  rescue ArgumentError => exc
38
43
  puts "\nSymmetric Encryption not able to read keys."
39
44
  puts "#{exc.class.name} #{exc.message}"
40
- puts "To generate a new config file and key files: symmetric-encryption --generate --key-path /etc/#{app_name} --app-name #{app_name}\n\n"
45
+ puts "To generate a new config file and key files: symmetric-encryption --generate --app-name #{app_name}\n\n"
41
46
  raise(exc)
42
47
  end
43
48
  else
44
49
  puts "\nSymmetric Encryption config not found."
45
- 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
+ puts "To generate a new config file and key files: symmetric-encryption --generate --app-name #{app_name}\n\n"
46
51
  end
47
52
  end
48
53
  end
@@ -14,6 +14,7 @@
14
14
  class SymmetricEncryptionValidator < ActiveModel::EachValidator
15
15
  def validate_each(record, attribute, value)
16
16
  return if value.blank? || SymmetricEncryption.encrypted?(value)
17
+
17
18
  record.errors.add(attribute, 'must be a value encrypted using SymmetricEncryption.encrypt')
18
19
  end
19
20
  end
@@ -76,7 +76,7 @@ module SymmetricEncryption
76
76
  # Notes:
77
77
  # * Do not use this method for reading large files.
78
78
  def self.read(file_name_or_stream, **args)
79
- self.open(file_name_or_stream, **args, &:read)
79
+ Reader.open(file_name_or_stream, **args, &:read)
80
80
  end
81
81
 
82
82
  # Decrypt an entire file.
@@ -90,22 +90,10 @@ module SymmetricEncryption
90
90
  # target: [String|IO]
91
91
  # Target file_name or IOStream
92
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
93
  # Notes:
99
94
  # * The file contents are streamed so that the entire file is _not_ loaded into memory.
100
- def self.decrypt(source:, target:, block_size: 65_535, **args)
101
- target_ios = target.is_a?(String) ? ::File.open(target, 'wb') : target
102
- bytes_written = 0
103
- self.open(source, **args) do |input_ios|
104
- bytes_written += target_ios.write(input_ios.read(block_size)) until input_ios.eof?
105
- end
106
- bytes_written
107
- ensure
108
- target_ios.close if target_ios&.respond_to?(:closed?) && !target_ios.closed?
95
+ def self.decrypt(source:, target:, **args)
96
+ Reader.open(source, **args) { |input_file| IO.copy_stream(input_file, target) }
109
97
  end
110
98
 
111
99
  # Returns [true|false] whether the file or stream contains any data
@@ -132,6 +120,7 @@ module SymmetricEncryption
132
120
  @version = version
133
121
  @header_present = false
134
122
  @closed = false
123
+ @read_buffer = ''.b
135
124
 
136
125
  raise(ArgumentError, 'Buffer size cannot be smaller than 128') unless @buffer_size >= 128
137
126
 
@@ -170,6 +159,7 @@ module SymmetricEncryption
170
159
  # ensure that the encrypted stream is closed before the stream itself is closed
171
160
  def close(close_child_stream = true)
172
161
  return if closed?
162
+
173
163
  @ios.close if close_child_stream
174
164
  @closed = true
175
165
  end
@@ -194,35 +184,25 @@ module SymmetricEncryption
194
184
  #
195
185
  # At end of file, it returns nil if no more data is available, or the last
196
186
  # remaining bytes
197
- def read(length = nil)
198
- data = nil
199
- if length
200
- return '' if length.zero?
201
- return nil if eof?
202
- # Read length bytes
203
- read_block while (@read_buffer.length < length) && !@ios.eof?
204
- if @read_buffer.empty?
205
- data = nil
206
- elsif @read_buffer.length > length
207
- data = @read_buffer.slice!(0..length - 1)
187
+ def read(length = nil, outbuf = nil)
188
+ data = outbuf.to_s.clear
189
+ remaining_length = length
190
+
191
+ until remaining_length == 0 || eof?
192
+ read_block(remaining_length) if @read_buffer.empty?
193
+
194
+ if remaining_length && remaining_length < @read_buffer.length
195
+ data << @read_buffer.slice!(0, remaining_length)
208
196
  else
209
- data = @read_buffer
210
- @read_buffer = ''
211
- end
212
- else
213
- # Capture anything already in the buffer
214
- data = @read_buffer
215
- @read_buffer = ''
216
-
217
- unless @ios.eof?
218
- # Read entire file
219
- buf = @ios.read || ''
220
- data << @stream_cipher.update(buf) if buf && !buf.empty?
221
- data << @stream_cipher.final
197
+ data << @read_buffer
198
+ @read_buffer.clear
222
199
  end
200
+
201
+ remaining_length = length - data.length if length
223
202
  end
203
+
224
204
  @pos += data.length
225
- data
205
+ data unless data.empty? && length && length.positive?
226
206
  end
227
207
 
228
208
  # Reads a single decrypted line from the file up to and including the optional sep_string.
@@ -242,12 +222,14 @@ module SymmetricEncryption
242
222
  # Read more data until we get the sep_string
243
223
  while (index = @read_buffer.index(sep_string)).nil? && !@ios.eof?
244
224
  break if length && @read_buffer.length >= length
225
+
245
226
  read_block
246
227
  end
247
228
  index ||= -1
248
- data = @read_buffer.slice!(0..index)
249
- @pos += data.length
229
+ data = @read_buffer.slice!(0..index)
230
+ @pos += data.length
250
231
  return nil if data.empty? && eof?
232
+
251
233
  data
252
234
  end
253
235
 
@@ -272,7 +254,7 @@ module SymmetricEncryption
272
254
 
273
255
  # Rewind back to the beginning of the file
274
256
  def rewind
275
- @read_buffer = ''
257
+ @read_buffer.clear
276
258
  @ios.rewind
277
259
  read_header
278
260
  end
@@ -307,10 +289,10 @@ module SymmetricEncryption
307
289
  # Read and decrypt entire file a block at a time to get its total
308
290
  # unencrypted size
309
291
  size = 0
310
- until eof
292
+ until eof?
311
293
  read_block
312
- size += @read_buffer.size
313
- @read_buffer = ''
294
+ size += @read_buffer.size
295
+ @read_buffer.clear
314
296
  end
315
297
  rewind
316
298
  offset = size + amount
@@ -328,7 +310,7 @@ module SymmetricEncryption
328
310
  @pos = 0
329
311
 
330
312
  # Read first block and check for the header
331
- buf = @ios.read(@buffer_size)
313
+ buf = @ios.read(@buffer_size, @output_buffer ||= ''.b)
332
314
 
333
315
  # Use cipher specified in header, or global cipher if it has no header
334
316
  iv, key, cipher_name, cipher = nil
@@ -353,20 +335,30 @@ module SymmetricEncryption
353
335
  @stream_cipher.key = key || cipher.send(:key)
354
336
  @stream_cipher.iv = iv || cipher.iv
355
337
 
356
- # First call to #update should return an empty string anyway
357
- if buf && !buf.empty?
358
- @read_buffer = @stream_cipher.update(buf)
359
- @read_buffer << @stream_cipher.final if @ios.eof?
360
- else
361
- @read_buffer = ''
362
- end
338
+ decrypt(buf)
363
339
  end
364
340
 
365
341
  # Read a block of data and append the decrypted data in the read buffer
366
- def read_block
367
- buf = @ios.read(@buffer_size)
368
- @read_buffer << @stream_cipher.update(buf) if buf && !buf.empty?
369
- @read_buffer << @stream_cipher.final if @ios.eof?
342
+ def read_block(length = nil)
343
+ buf = @ios.read(length || @buffer_size, @output_buffer ||= ''.b)
344
+ decrypt(buf)
345
+ end
346
+
347
+ # Decrypts the given chunk of data and returns the result
348
+ if defined?(JRuby)
349
+ def decrypt(buf)
350
+ return if buf.nil? || buf.empty?
351
+
352
+ @read_buffer << @stream_cipher.update(buf)
353
+ @read_buffer << @stream_cipher.final if @ios.eof?
354
+ end
355
+ else
356
+ def decrypt(buf)
357
+ return if buf.nil? || buf.empty?
358
+
359
+ @read_buffer << @stream_cipher.update(buf, @cipher_buffer ||= ''.b)
360
+ @read_buffer << @stream_cipher.final if @ios.eof?
361
+ end
370
362
  end
371
363
 
372
364
  def closed?
@@ -55,6 +55,7 @@ module SymmetricEncryption
55
55
  end
56
56
 
57
57
  return @@cipher if version.nil? || (@@cipher.version == version)
58
+
58
59
  secondary_ciphers.find { |c| c.version == version } || (@@cipher if version.zero?)
59
60
  end
60
61
 
@@ -264,7 +265,7 @@ module SymmetricEncryption
264
265
  # encoded_str.end_with?("\n") ? SymmetricEncryption.cipher(0) : SymmetricEncryption.cipher
265
266
  # end
266
267
  def self.select_cipher(&block)
267
- @@select_cipher = block ? block : nil
268
+ @@select_cipher = block || nil
268
269
  end
269
270
 
270
271
  # Load the Encryption Configuration from a YAML file