symmetric-encryption 3.9.1 → 4.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
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