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.
- checksums.yaml +4 -4
- data/README.md +72 -0
- data/bin/symmetric-encryption +5 -0
- data/lib/symmetric_encryption/cipher.rb +162 -419
- data/lib/symmetric_encryption/cli.rb +343 -0
- data/lib/symmetric_encryption/coerce.rb +5 -20
- data/lib/symmetric_encryption/config.rb +128 -50
- data/lib/symmetric_encryption/extensions/mongo_mapper/plugins/encrypted_key.rb +2 -2
- data/lib/symmetric_encryption/generator.rb +3 -2
- data/lib/symmetric_encryption/header.rb +260 -0
- data/lib/symmetric_encryption/key.rb +106 -0
- data/lib/symmetric_encryption/keystore/environment.rb +90 -0
- data/lib/symmetric_encryption/keystore/file.rb +102 -0
- data/lib/symmetric_encryption/keystore/memory.rb +53 -0
- data/lib/symmetric_encryption/keystore.rb +124 -0
- data/lib/symmetric_encryption/railtie.rb +5 -7
- data/lib/symmetric_encryption/reader.rb +74 -55
- data/lib/symmetric_encryption/rsa_key.rb +24 -0
- data/lib/symmetric_encryption/symmetric_encryption.rb +64 -102
- data/lib/symmetric_encryption/utils/re_encrypt_files.rb +140 -0
- data/lib/symmetric_encryption/version.rb +1 -1
- data/lib/symmetric_encryption/writer.rb +104 -117
- data/lib/symmetric_encryption.rb +9 -4
- data/test/active_record_test.rb +61 -40
- data/test/cipher_test.rb +179 -236
- data/test/config/symmetric-encryption.yml +140 -82
- data/test/header_test.rb +218 -0
- data/test/key_test.rb +231 -0
- data/test/keystore/environment_test.rb +119 -0
- data/test/keystore/file_test.rb +125 -0
- data/test/keystore_test.rb +59 -0
- data/test/mongoid_test.rb +13 -13
- data/test/reader_test.rb +52 -53
- data/test/symmetric_encryption_test.rb +50 -135
- data/test/test_db.sqlite3 +0 -0
- data/test/writer_test.rb +52 -31
- metadata +26 -14
- data/examples/symmetric-encryption.yml +0 -108
- data/lib/rails/generators/symmetric_encryption/config/config_generator.rb +0 -22
- data/lib/rails/generators/symmetric_encryption/config/templates/symmetric-encryption.yml +0 -50
- data/lib/rails/generators/symmetric_encryption/heroku_config/heroku_config_generator.rb +0 -20
- data/lib/rails/generators/symmetric_encryption/heroku_config/templates/symmetric-encryption.yml +0 -78
- data/lib/rails/generators/symmetric_encryption/new_keys/new_keys_generator.rb +0 -14
- data/lib/symmetric_encryption/key_encryption_key.rb +0 -32
- data/lib/symmetric_encryption/railties/symmetric_encryption.rake +0 -84
- 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:
|
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
|
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
|
-
#
|
14
|
-
# The
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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(
|
80
|
-
|
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,
|
87
|
-
file = Zlib::GzipReader.new(file) if !file.eof? &&
|
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?(
|
97
|
-
open(
|
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?(
|
102
|
-
::File.open(
|
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,
|
131
|
+
def initialize(ios, buffer_size: 4096, version: nil)
|
113
132
|
@ios = ios
|
114
|
-
@buffer_size =
|
115
|
-
@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
|
338
|
+
@pos = 0
|
320
339
|
|
321
340
|
# Read first block and check for the header
|
322
|
-
buf
|
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
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
@
|
330
|
-
@
|
331
|
-
|
332
|
-
cipher_name
|
333
|
-
key
|
334
|
-
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
|
337
|
-
@compressed
|
338
|
-
|
339
|
-
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 ||
|
345
|
-
@stream_cipher.iv = 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
|
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
|
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
|
-
#
|
75
|
-
#
|
76
|
-
# Returns
|
77
|
-
# Returns
|
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
|
-
#
|
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
|
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
|
115
|
+
str = encrypted_and_encoded_string.to_s
|
113
116
|
|
114
117
|
# Decode before decrypting supplied string
|
115
|
-
decoded =
|
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
|
121
|
-
header.
|
124
|
+
if header.parse!(decoded)
|
125
|
+
header.cipher.binary_decrypt(decoded, header: header)
|
122
126
|
else
|
123
|
-
|
124
|
-
|
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
|
178
|
-
|
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
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
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]
|
205
|
-
# Parameters:
|
206
|
-
# encrypted_data: Encrypted string
|
227
|
+
# Returns [true|false] whether the string is encrypted.
|
207
228
|
#
|
208
|
-
#
|
209
|
-
#
|
210
|
-
#
|
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
|
-
|
233
|
+
return false if encrypted_data.nil? || (encrypted_data == '')
|
213
234
|
|
214
|
-
|
215
|
-
|
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
|
-
#
|
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!(
|
257
|
-
Config.load!(
|
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
|
-
#
|
317
|
-
|
318
|
-
|
319
|
-
|
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')
|