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,140 @@
1
+ # Used for re-encrypting encrypted passwords stored in configuration files.
2
+ #
3
+ # Search for any encrypted value and re-encrypt it using the latest encryption key.
4
+ # Note:
5
+ # * Only works with encrypted values that have the standard header.
6
+ # * The search looks for the header and then replaces the encrypted value.
7
+ #
8
+ # Example:
9
+ # re_encrypt = SymmetricEncryption::Utils::ReEncryptConfigFiles.new(version: 4)
10
+ # re_encrypt.process_directory('../../**/*.yml')
11
+ #
12
+ # Notes:
13
+ # * Only supports the output from encrypting data.
14
+ # * I.e. Manually adding newlines to base 64 output is not supported.
15
+ # * For now only supports one encrypted value per line.
16
+ module SymmetricEncryption
17
+ module Utils
18
+ # ReEncrypt files
19
+ #
20
+ # If a file is encrypted, it is re-encrypted with the cipher that has the highest version number.
21
+ # A file that is already encrypted with the specified key version is not re-encrypted.
22
+ # If an encrypted value cannot be decypted in the current environment it is left unmodified.
23
+ #
24
+ # If a file is not encrypted, the file is searched for any encrypted values, and those values are re-encrypted.
25
+ #
26
+ # symmetric_encryption --reencrypt "**/*.yml"
27
+ class ReEncryptFiles
28
+ attr_accessor :cipher, :version
29
+
30
+ # Parameters:
31
+ # version: [Integer]
32
+ # Version of the encryption key to use when re-encrypting the value.
33
+ # Default: Default cipher ( first in the list of configured ciphers )
34
+ def initialize(version: SymmetricEncryption.cipher.version)
35
+ @version = version || SymmetricEncryption.cipher.version
36
+ @cipher = SymmetricEncryption.cipher(@version)
37
+ raise(ArgumentError, "Undefined encryption key version: #{version}") if @cipher.nil?
38
+ end
39
+
40
+ # Re-encrypt the supplied encrypted value with the new cipher
41
+ def re_encrypt(encrypted)
42
+ if unencrypted = SymmetricEncryption.try_decrypt(encrypted)
43
+ cipher.encrypt(unencrypted)
44
+ else
45
+ encrypted
46
+ end
47
+ end
48
+
49
+ # Process a single file.
50
+ #
51
+ # Returns [Integer] number of encrypted values re-encrypted.
52
+ def re_encrypt_contents(file_name)
53
+ return 0 if File.size(file_name) > 256 * 1024
54
+
55
+ hits = 0
56
+ lines = File.read(file_name)
57
+ output_lines = ''
58
+ r = regexp
59
+ lines.each_line do |line|
60
+ line.force_encoding(SymmetricEncryption::UTF8_ENCODING)
61
+ output_lines <<
62
+ if line.valid_encoding? && (result = line.match(r))
63
+ encrypted = result[0]
64
+ new_value = re_encrypt(encrypted)
65
+ if new_value != encrypted
66
+ hits += 1
67
+ line.gsub(encrypted, new_value)
68
+ else
69
+ line
70
+ end
71
+ else
72
+ line
73
+ end
74
+ end
75
+ if hits
76
+ File.open(file_name, 'wb') { |file| file.write(output_lines) }
77
+ end
78
+ hits
79
+ rescue
80
+ puts "Failed re-encrypting the file contents of: #{file_name}"
81
+ raise
82
+ end
83
+
84
+ # Re Encrypt an entire file
85
+ def re_encrypt_file(file_name)
86
+ temp_file_name = "__re_encrypting_#{file_name}"
87
+ SymmetricEncryption::Reader.open(file_name) do |source|
88
+ SymmetricEncryption::Writer.encrypt(source: source, target: temp_file_name, compress: true, version: version)
89
+ end
90
+ File.delete(file_name)
91
+ File.rename(temp_file_name, file_name)
92
+ rescue
93
+ File.delete(temp_file_name) if temp_file_name && File.exist?(temp_file_name)
94
+ raise
95
+ end
96
+
97
+ # Process a directory of files.
98
+ #
99
+ # Parameters:
100
+ # path: [String]
101
+ # Search path to look for files in.
102
+ # Example: '../../**/*.yml'
103
+ def process_directory(path)
104
+ Dir[path].each do |file_name|
105
+ next if File.directory?(file_name)
106
+
107
+ if v = encrypted_file_version(file_name)
108
+ if v == version
109
+ puts "Skipping already re-encrypted file: #{file_name}"
110
+ else
111
+ puts "Re-encrypting entire file: #{file_name}"
112
+ re_encrypt_file(file_name)
113
+ end
114
+ else
115
+ count = re_encrypt_contents(file_name)
116
+ puts "Re-encrypted #{count} encrypted value(s) in: #{file_name}" if count > 0
117
+ end
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def regexp
124
+ @regexp ||= /#{SymmetricEncryption.cipher.encoded_magic_header}([A-Za-z0-9+\/]+=+[\\n]*)/
125
+ end
126
+
127
+ # Returns [Integer] encrypted file key version.
128
+ # Returns [nil] if the file is not encrypted or does not have a header.
129
+ def encrypted_file_version(file_name)
130
+ ::File.open(file_name, 'rb') do |file|
131
+ reader = SymmetricEncryption::Reader.new(file)
132
+ reader.version if reader.header_present?
133
+ end
134
+ rescue OpenSSL::Cipher::CipherError
135
+ nil
136
+ end
137
+
138
+ end
139
+ end
140
+ end
@@ -1,3 +1,3 @@
1
1
  module SymmetricEncryption #:nodoc
2
- VERSION = '3.9.1'
2
+ VERSION = '4.0.0.beta3'
3
3
  end
@@ -1,89 +1,38 @@
1
1
  require 'openssl'
2
2
 
3
3
  module SymmetricEncryption
4
- # Write to encrypted files and other IO streams
4
+ # Write to encrypted files and other IO streams.
5
5
  #
6
6
  # Features:
7
7
  # * Encryption on the fly whilst writing files.
8
- # * Large file support by only buffering small amounts of data in memory
8
+ # * Large file support by only buffering small amounts of data in memory.
9
9
  # * Underlying buffering to ensure that encrypted data fits
10
- # into the Symmetric Encryption Cipher block size
11
- # Only the last block in the file will be padded if it is less than the block size
10
+ # into the Symmetric Encryption Cipher block size.
11
+ # Only the last block in the file will be padded if it is less than the block size.
12
12
  class Writer
13
- # Open a file for writing, or use the supplied IO Stream
13
+ # Open a file for writing, or use the supplied IO Stream.
14
14
  #
15
15
  # Parameters:
16
- # filename_or_stream:
17
- # The filename to open if a string, otherwise the stream to use
16
+ # file_name_or_stream: [String|IO]
17
+ # The file_name to open if a string, otherwise the stream to use.
18
18
  # The file or stream will be closed on completion, use .initialize to
19
- # avoid having the stream closed automatically
20
- #
21
- # options:
22
- # :compress [true|false]
23
- # Uses Zlib to compress the data before it is encrypted and
24
- # written to the file
25
- # If true, it forces header to true.
26
- # Default: false
27
- #
28
- # :random_key [true|false]
29
- # Generates a new random key for every new file or stream
30
- # If true, it forces header to true. Version below then has no effect
31
- # The Random key will be written to the file/stream in encrypted
32
- # form as part of the header
33
- # The key is encrypted using the global key
34
- # Default: true
35
- # Recommended: true.
36
- # Setting to false will eventually expose the
37
- # encryption key since too much data will be encrypted using the
38
- # same encryption key
39
- #
40
- # :random_iv [true|false]
41
- # Generates a new random iv for every new file or stream
42
- # If true, it forces header to true.
43
- # The Random iv will be written to the file/stream in encrypted
44
- # form as part of the header
45
- # Default: Value supplied above for :random_key
46
- # Recommended: true. Setting to false will eventually expose the
47
- # encryption key since too much data will be encrypted using the
48
- # same encryption key
49
- #
50
- # :header [true|false]
51
- # Whether to include the magic header that indicates the file
52
- # is encrypted and whether its contents are compressed
53
- #
54
- # The header contains:
55
- # Version of the encryption key used to encrypt the file
56
- # Indicator if the data was compressed
57
- # Default: true
58
- #
59
- # :version
60
- # When random_key is true, the version of the encryption key to use
61
- # when encrypting the header portion of the file
62
- #
63
- # When random_key is false, the version of the encryption key to use
64
- # to encrypt the entire file
65
- # Default: SymmetricEncryption.cipher
66
- #
67
- # :mode
68
- # See File.open for open modes
69
- # Default: 'w'
70
- #
71
- # :cipher_name
72
- # The name of the cipher to use only if both :random_key and
73
- # :random_iv are true.
74
- # Default: SymmetricEncryption.cipher.cipher_name
19
+ # avoid having the stream closed automatically.
75
20
  #
76
- # Note: Compression occurs before encryption
21
+ # compress: [true|false]
22
+ # Uses Zlib to compress the data before it is encrypted and
23
+ # written to the file/stream.
24
+ # Default: false
77
25
  #
26
+ # Note: Compression occurs before encryption
78
27
  #
79
28
  # # Example: Encrypt and write data to a file
80
- # SymmetricEncryption::Writer.open('test_file') do |file|
29
+ # SymmetricEncryption::Writer.open('test_file.enc') do |file|
81
30
  # file.write "Hello World\n"
82
31
  # file.write 'Keep this secret'
83
32
  # end
84
33
  #
85
34
  # # Example: Compress, Encrypt and write data to a file
86
- # SymmetricEncryption::Writer.open('encrypted_compressed.zip', compress: true) do |file|
35
+ # SymmetricEncryption::Writer.open('encrypted_compressed.enc', compress: true) do |file|
87
36
  # file.write "Hello World\n"
88
37
  # file.write "Compress this\n"
89
38
  # file.write "Keep this safe and secure\n"
@@ -93,80 +42,115 @@ module SymmetricEncryption
93
42
  # require 'csv'
94
43
  # begin
95
44
  # # Must supply :row_sep for CSV otherwise it will attempt to read from and then rewind the file
96
- # csv = CSV.new(SymmetricEncryption::Writer.open('csv_encrypted'), row_sep: "\n")
45
+ # csv = CSV.new(SymmetricEncryption::Writer.open('csv.enc'), row_sep: "\n")
97
46
  # csv << [1,2,3,4,5]
98
47
  # ensure
99
48
  # csv.close if csv
100
49
  # end
101
- def self.open(filename_or_stream, options={}, &block)
102
- raise(ArgumentError, 'options must be a hash') unless options.respond_to?(:each_pair)
103
- mode = options.fetch(:mode, 'wb')
104
- compress = options.fetch(:compress, false)
105
- ios = filename_or_stream.is_a?(String) ? ::File.open(filename_or_stream, mode) : filename_or_stream
50
+ def self.open(file_name_or_stream, compress: false, **args)
51
+ ios = file_name_or_stream.is_a?(String) ? ::File.open(file_name_or_stream, 'wb') : file_name_or_stream
106
52
 
107
53
  begin
108
- file = self.new(ios, options)
54
+ file = self.new(ios, compress: compress, **args)
109
55
  file = Zlib::GzipWriter.new(file) if compress
110
- block ? block.call(file) : file
56
+ block_given? ? yield(file) : file
111
57
  ensure
112
- file.close if block && file && (file.respond_to?(:closed?) && !file.closed?)
58
+ file.close if block_given? && file && (file.respond_to?(:closed?) && !file.closed?)
59
+ end
60
+ end
61
+
62
+ # Write the contents of a string in memory to an encrypted file / stream.
63
+ #
64
+ # Notes:
65
+ # * Do not use this method for writing large files.
66
+ def self.write(file_name_or_stream, data, **args)
67
+ open(file_name_or_stream, **args) { |f| f.write(data) }
68
+ end
69
+
70
+ # Encrypt an entire file.
71
+ #
72
+ # Returns [Integer] the number of encrypted bytes written to the target file.
73
+ #
74
+ # Params:
75
+ # source: [String|IO]
76
+ # Source file_name or IOStream
77
+ #
78
+ # target: [String|IO]
79
+ # Target file_name or IOStream
80
+ #
81
+ # compress: [true|false]
82
+ # Whether to compress the target file prior to encryption.
83
+ # Default: false
84
+ #
85
+ # block_size: [Integer]
86
+ # Number of bytes to read into memory for each read.
87
+ # For very large files using a larger block size is faster.
88
+ # Default: 65535
89
+ #
90
+ # Notes:
91
+ # * The file contents are streamed so that the entire file is _not_ loaded into memory.
92
+ def self.encrypt(source:, target:, block_size: 65535, **args)
93
+ source_ios = source.is_a?(String) ? ::File.open(source, 'rb') : source
94
+ bytes_written = 0
95
+ open(target, **args) do |output_file|
96
+ while !source_ios.eof?
97
+ bytes_written += output_file.write(source_ios.read(block_size))
98
+ end
113
99
  end
100
+ bytes_written
101
+ ensure
102
+ source_ios.close if source_ios && source_ios.respond_to?(:closed?) && !source_ios.closed?
114
103
  end
115
104
 
116
105
  # Encrypt data before writing to the supplied stream
117
- def initialize(ios, options={})
118
- @ios = ios
119
- header = options.fetch(:header, true)
120
- random_key = options.fetch(:random_key, true)
121
- random_iv = options.fetch(:random_iv, random_key)
122
- raise(ArgumentError, 'When :random_key is true, :random_iv must also be true') if random_key && !random_iv
106
+ def initialize(ios, version: nil, cipher_name: nil, header: true, random_key: true, random_iv: true, compress: false)
123
107
  # Compress is only used at this point for setting the flag in the header
124
- compress = options.fetch(:compress, false)
125
- version = options[:version]
126
- cipher_name = options[:cipher_name]
108
+ @ios = ios
109
+ raise(ArgumentError, 'When :random_key is true, :random_iv must also be true') if random_key && !random_iv
127
110
  raise(ArgumentError, 'Cannot supply a :cipher_name unless both :random_key and :random_iv are true') if cipher_name && !random_key && !random_iv
128
111
 
129
- # Force header if compressed or using random iv, key
130
- header = true if compress || random_key || random_iv
131
-
132
112
  # Cipher to encrypt the random_key, or the entire file
133
113
  cipher = SymmetricEncryption.cipher(version)
134
114
  raise(SymmetricEncryption::CipherError, "Cipher with version:#{version} not found in any of the configured SymmetricEncryption ciphers") unless cipher
135
115
 
116
+ # Force header if compressed or using random iv, key
117
+ if (header == true) || compress || random_key || random_iv
118
+ header = Header.new(version: cipher.version, compress: compress, cipher_name: cipher_name)
119
+ end
120
+
136
121
  @stream_cipher = ::OpenSSL::Cipher.new(cipher_name || cipher.cipher_name)
137
122
  @stream_cipher.encrypt
138
123
 
139
- key = random_key ? @stream_cipher.random_key : cipher.send(:key)
140
- iv = random_iv ? @stream_cipher.random_iv : cipher.send(:iv)
141
-
142
- @stream_cipher.key = key
143
- @stream_cipher.iv = iv if iv
124
+ if random_key
125
+ header.key = @stream_cipher.key = @stream_cipher.random_key
126
+ else
127
+ @stream_cipher.key = cipher.send(:key)
128
+ end
144
129
 
145
- # Write the Encryption header including the random iv, key, and cipher
146
- if header
147
- @ios.write(Cipher.build_header(
148
- cipher.version,
149
- compress,
150
- random_iv ? iv : nil,
151
- random_key ? key : nil,
152
- cipher_name))
130
+ if random_iv
131
+ header.iv = @stream_cipher.iv = @stream_cipher.random_iv
132
+ else
133
+ @stream_cipher.iv = cipher.iv if cipher.iv
153
134
  end
135
+
136
+ @ios.write(header.to_s) if header
137
+
154
138
  @size = 0
155
139
  @closed = false
156
140
  end
157
141
 
158
- # Close the IO Stream
159
- # Flushes any unwritten data
142
+ # Close the IO Stream.
160
143
  #
161
- # Note: Once an EncryptionWriter has been closed a new instance must be
162
- # created before writing again
163
- #
164
- # Note: Also closes the passed in io stream or file
165
- # Note: This method must be called _before_ the supplied stream is closed
144
+ # Notes:
145
+ # * Flushes any unwritten data.
146
+ # * Once an EncryptionWriter has been closed a new instance must be
147
+ # created before writing again.
148
+ # * Closes the passed in io stream or file.
149
+ # * `close` must be called _before_ the supplied stream is closed.
166
150
  #
167
151
  # It is recommended to call Symmetric::EncryptedStream.open
168
- # rather than creating an instance of Symmetric::EncryptedStream directly to
169
- # ensure that the encrypted stream is closed before the stream itself is closed
152
+ # rather than creating an instance of Symmetric::Writer directly to
153
+ # ensure that the encrypted stream is closed before the stream itself is closed.
170
154
  def close(close_child_stream = true)
171
155
  return if closed?
172
156
  if size > 0
@@ -177,8 +161,9 @@ module SymmetricEncryption
177
161
  @closed = true
178
162
  end
179
163
 
180
- # Write to the IO Stream as encrypted data
181
- # Returns the number of bytes written
164
+ # Write to the IO Stream as encrypted data.
165
+ #
166
+ # Returns [Integer] the number of bytes written.
182
167
  def write(data)
183
168
  return unless data
184
169
 
@@ -189,8 +174,9 @@ module SymmetricEncryption
189
174
  data.length
190
175
  end
191
176
 
192
- # Write to the IO Stream as encrypted data
193
- # Returns self
177
+ # Write to the IO Stream as encrypted data.
178
+ #
179
+ # Returns [SymmetricEncryption::Writer] self
194
180
  #
195
181
  # Example:
196
182
  # file << "Hello.\n" << 'This is Jack'
@@ -199,20 +185,21 @@ module SymmetricEncryption
199
185
  self
200
186
  end
201
187
 
202
- # Flush the output stream
188
+ # Flush the output stream.
203
189
  # Does not flush internal buffers since encryption requires all data to
204
- # be written following the encryption block size
205
- # Needed by XLS gem
190
+ # be written following the encryption block size.
191
+ # Needed by XLS gem.
206
192
  def flush
207
193
  @ios.flush
208
194
  end
209
195
 
196
+ # Returns [true|false] whether this stream is closed.
210
197
  def closed?
211
198
  @closed || @ios.respond_to?(:closed?) && @ios.closed?
212
199
  end
213
200
 
214
201
  # Returns [Integer] the number of unencrypted and uncompressed bytes
215
- # written to the file so far
202
+ # written to the file so far.
216
203
  attr_reader :size
217
204
 
218
205
  end
@@ -13,12 +13,17 @@ module SymmetricEncryption
13
13
  autoload :Coerce, 'symmetric_encryption/coerce'
14
14
  autoload :Config, 'symmetric_encryption/config'
15
15
  autoload :Encoder, 'symmetric_encryption/encoder'
16
- autoload :KeyEncryptionKey, 'symmetric_encryption/key_encryption_key'
16
+ autoload :Generator, 'symmetric_encryption/generator'
17
+ autoload :Header, 'symmetric_encryption/header'
18
+ autoload :Key, 'symmetric_encryption/key'
17
19
  autoload :Reader, 'symmetric_encryption/reader'
20
+ autoload :RSAKey, 'symmetric_encryption/rsa_key'
18
21
  autoload :Writer, 'symmetric_encryption/writer'
19
- autoload :Generator, 'symmetric_encryption/generator'
22
+ autoload :CLI, 'symmetric_encryption/cli'
23
+ autoload :Keystore, 'symmetric_encryption/keystore'
20
24
  module Utils
21
- autoload :ReEncryptConfigFiles, 'symmetric_encryption/re_encrypt_config_files'
25
+ autoload :Generate, 'symmetric_encryption/utils/generate'
26
+ autoload :ReEncryptFiles, 'symmetric_encryption/utils/re_encrypt_files'
22
27
  end
23
28
  end
24
29
  #@formatter:on
@@ -31,6 +36,6 @@ end
31
36
  require 'symmetric_encryption/railties/symmetric_encryption_validator' if defined?(ActiveModel)
32
37
  require 'symmetric_encryption/extensions/mongoid/encrypted' if defined?(Mongoid)
33
38
  if defined?(MongoMapper)
34
- warn 'MongoMapper support is deprecated. Consider upgrading to Mongoid.'
39
+ warn 'MongoMapper support is deprecated. Upgrade to Mongoid.'
35
40
  require 'symmetric_encryption/extensions/mongo_mapper/plugins/encrypted_key'
36
41
  end
@@ -77,15 +77,6 @@ class UniqueUser < ActiveRecord::Base
77
77
  end
78
78
  #@formatter:on
79
79
 
80
- # Initialize the database connection
81
- config_file = File.join(File.dirname(__FILE__), 'config', 'database.yml')
82
- raise 'database config not found. Create a config file at: test/config/database.yml' unless File.exist? config_file
83
-
84
- cfg = YAML.load(ERB.new(File.new(config_file).read).result)['test']
85
- raise("Environment 'test' not defined in test/config/database.yml") unless cfg
86
-
87
- User.establish_connection(cfg)
88
-
89
80
  #
90
81
  # Unit Test for attr_encrypted extensions in ActiveRecord
91
82
  #
@@ -114,26 +105,26 @@ class ActiveRecordTest < Minitest::Test
114
105
 
115
106
  @user = User.new(
116
107
  # Encrypted Attribute
117
- bank_account_number: @bank_account_number,
108
+ bank_account_number: @bank_account_number,
118
109
  # Encrypted Attribute
119
110
  social_security_number: @social_security_number,
120
111
  name: @name,
121
112
  # data type specific fields
122
- string_value: STRING_VALUE,
123
- long_string_value: LONG_STRING_VALUE,
124
- binary_string_value: BINARY_STRING_VALUE,
125
- integer_value: INTEGER_VALUE,
126
- float_value: FLOAT_VALUE,
127
- decimal_value: DECIMAL_VALUE,
128
- datetime_value: DATETIME_VALUE,
129
- time_value: TIME_VALUE,
130
- date_value: DATE_VALUE,
131
- true_value: true,
132
- false_value: false,
133
- data_yaml: @h.dup,
134
- data_json: @h.dup,
135
- text: 'hello',
136
- number: '21'
113
+ string_value: STRING_VALUE,
114
+ long_string_value: LONG_STRING_VALUE,
115
+ binary_string_value: BINARY_STRING_VALUE,
116
+ integer_value: INTEGER_VALUE,
117
+ float_value: FLOAT_VALUE,
118
+ decimal_value: DECIMAL_VALUE,
119
+ datetime_value: DATETIME_VALUE,
120
+ time_value: TIME_VALUE,
121
+ date_value: DATE_VALUE,
122
+ true_value: true,
123
+ false_value: false,
124
+ data_yaml: @h.dup,
125
+ data_json: @h.dup,
126
+ text: 'hello',
127
+ number: '21'
137
128
  )
138
129
  end
139
130
 
@@ -171,9 +162,9 @@ class ActiveRecordTest < Minitest::Test
171
162
  it 'true' do
172
163
  @user.string_value = STRING_VALUE
173
164
  assert first_value = @user.encrypted_string_value
174
- # Assign the same value
175
- @user.string_value = STRING_VALUE.dup
176
- assert first_value != @user.encrypted_string_value
165
+ @user.string_value = 'blah'
166
+ @user.string_value = STRING_VALUE
167
+ refute_equal first_value, @user.encrypted_string_value
177
168
  end
178
169
 
179
170
  it 'true and compress: true' do
@@ -182,9 +173,48 @@ class ActiveRecordTest < Minitest::Test
182
173
 
183
174
  refute_equal @user.encrypted_long_string_value, @user.encrypted_string_value
184
175
  end
176
+
177
+ describe 'changed?' do
178
+ it 'true for a new instance' do
179
+ assert @user.string_value_changed?
180
+ end
181
+
182
+ it 'clears after save' do
183
+ @user.save!
184
+ refute @user.string_value_changed?
185
+ end
186
+
187
+ it 'does not change when equal' do
188
+ @user.save!
189
+ before = @user.encrypted_string_value
190
+ @user.string_value = STRING_VALUE
191
+ refute @user.string_value_changed?
192
+ assert_equal before, @user.encrypted_string_value
193
+ end
194
+ end
185
195
  end
186
196
 
187
197
  describe 'attribute=' do
198
+ it 'handles nil' do
199
+ @user.string_value = nil
200
+ assert_nil @user.string_value
201
+ assert_nil @user.encrypted_string_value
202
+ @user.save!
203
+ @user.reload
204
+ assert_nil @user.string_value
205
+ assert_nil @user.encrypted_string_value
206
+ end
207
+
208
+ it 'handles empty string' do
209
+ @user.string_value = ''
210
+ assert_equal '', @user.string_value
211
+ assert_equal '', @user.encrypted_string_value
212
+ @user.save!
213
+ @user.reload
214
+ assert_equal '', @user.string_value
215
+ assert_equal '', @user.encrypted_string_value
216
+ end
217
+
188
218
  it 'encrypt' do
189
219
  user = User.new
190
220
  user.bank_account_number = @bank_account_number
@@ -290,7 +320,7 @@ class ActiveRecordTest < Minitest::Test
290
320
  assert @user.valid?
291
321
  @user.number = ''
292
322
  assert_equal false, @user.valid?
293
- assert_nil @user.number
323
+ assert_equal '', @user.number
294
324
  assert_equal ["can't be blank"], @user.errors[:number]
295
325
  @user.number = nil
296
326
  assert_nil @user.number
@@ -413,17 +443,8 @@ class ActiveRecordTest < Minitest::Test
413
443
  @user_clone.save!
414
444
 
415
445
  @user.reload
416
- assert_nil @user.send(@attribute)
417
- assert_nil @user.send("encrypted_#{@attribute}".to_sym)
418
- end
419
-
420
- it 'permit replacing value with a blank string' do
421
- @user_clone.send("#{@attribute}=".to_sym, ' ')
422
- @user_clone.save!
423
-
424
- @user.reload
425
- assert_nil @user.send(@attribute)
426
- assert_nil @user.send("encrypted_#{@attribute}".to_sym)
446
+ assert_equal '', @user.send(@attribute)
447
+ assert_equal '', @user.send("encrypted_#{@attribute}".to_sym)
427
448
  end
428
449
 
429
450
  it 'permit replacing value' do