symmetric-encryption 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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