symmetric-encryption 1.1.1 → 2.0.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.
@@ -2,26 +2,49 @@ module ActiveRecord #:nodoc:
2
2
  class Base
3
3
 
4
4
  class << self # Class methods
5
- # Much lighter weight encryption for Rails attributes matching the
6
- # attr_encrypted interface using SymmetricEncryption
5
+ # Drop in replacement for attr_encrypted gem, except that it uses
6
+ # SymmetricEncryption for managing the encryption key
7
7
  #
8
- # The regular attr_encrypted gem uses Encryptor that adds encryption to
9
- # every Ruby object which is a complete overkill for this simple use-case
10
- #
11
- # Params:
12
- # * symbolic names of each method to create which has a corresponding
8
+ # Parameters:
9
+ # * Symbolic names of each method to create which has a corresponding
13
10
  # method already defined in rails starting with: encrypted_
14
- # * Followed by an option hash:
15
- # :marshal => Whether this element should be converted to YAML before encryption
16
- # true or false
17
- # Default: false
18
- #
11
+ # * Followed by an optional hash:
12
+ # :marshal [true|false]
13
+ # Whether this element should be converted to YAML before encryption
14
+ # Default: false
15
+ #
16
+ # :random_iv [true|false]
17
+ # Whether the encrypted value should use a random IV every time the
18
+ # field is encrypted.
19
+ # It is recommended to set this to true where feasible. If the encrypted
20
+ # value could be used as part of a SQL where clause, or as part
21
+ # of any lookup, then it must be false.
22
+ # Setting random_iv to true will result in a different encrypted output for
23
+ # the same input string.
24
+ # Note: Only set to true if the field will never be used as part of
25
+ # the where clause in an SQL query.
26
+ # Note: When random_iv is true it will add a 8 byte header, plus the bytes
27
+ # to store the random IV in every returned encrypted string, prior to the
28
+ # encoding if any.
29
+ # Default: false
30
+ # Highly Recommended where feasible: true
31
+ #
32
+ # :compress [true|false]
33
+ # Whether to compress str before encryption
34
+ # Should only be used for large strings since compression overhead and
35
+ # the overhead of adding the 'magic' header may exceed any benefits of
36
+ # compression
37
+ # Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
38
+ # Default: false
19
39
  def attr_encrypted(*params)
20
40
  # Ensure ActiveRecord has created all its methods first
21
41
  # Ignore failures since the table may not yet actually exist
22
42
  define_attribute_methods rescue nil
23
43
 
24
44
  options = params.last.is_a?(Hash) ? params.pop : {}
45
+ random_iv = options.fetch(:random_iv, false)
46
+ compress = options.fetch(:compress, false)
47
+ marshal = options.fetch(:marshal, false)
25
48
 
26
49
  params.each do |attribute|
27
50
  # Generate unencrypted attribute with getter and setter
@@ -40,7 +63,7 @@ module ActiveRecord #:nodoc:
40
63
  # Set the un-encrypted attribute
41
64
  # Also updates the encrypted field with the encrypted value
42
65
  def #{attribute}=(value)
43
- self.encrypted_#{attribute} = @stored_encrypted_#{attribute} = ::SymmetricEncryption.encrypt(value#{".to_yaml" if options[:marshal]})
66
+ self.encrypted_#{attribute} = @stored_encrypted_#{attribute} = ::SymmetricEncryption.encrypt(value#{".to_yaml" if marshal},#{random_iv},#{compress})
44
67
  @#{attribute} = value
45
68
  end
46
69
  UNENCRYPTED
@@ -25,6 +25,7 @@ module Mongoid
25
25
  # field :name, :type => String
26
26
  # field :encrypted_social_security_number, :type => String, :encrypted => true
27
27
  # field :age, :type => Integer
28
+ # field :life_history, :type => String, :encrypted => true, :compress => true, :random_iv => true
28
29
  #
29
30
  # end
30
31
  #
@@ -50,28 +51,35 @@ module Mongoid
50
51
  # person.social_security_number = "123456789"
51
52
  #
52
53
  # # Or, is equivalent to:
53
- # person.social_security_number = SymmetricEncryption.encrypt("123456789")
54
+ # person.encrypted_social_security_number = SymmetricEncryption.encrypt("123456789")
54
55
  #
55
56
  #
56
57
  # Note: Unlike attr_encrypted finders must use the encrypted field name
57
- # For Example this is NOT valid:
58
+ # Invalid Example, does not work:
58
59
  # person = Person.where(:social_security_number => '123456789').first
59
60
  #
61
+ # Valid Example:
62
+ # person = Person.where(:encrypted_social_security_number => SymmetricEncryption.encrypt('123456789')).first
63
+ #
60
64
  # Defines all the fields that are accessible on the Document
61
65
  # For each field that is defined, a getter and setter will be
62
66
  # added as an instance method to the Document.
63
67
  #
64
68
  # @example Define a field.
65
- # field :score, :type => Integer, :default => 0
69
+ # field :social_security_number, :type => String, :encrypted => true, :compress => false, :random_iv => false
70
+ # field :sensitive_text, :type => String, :encrypted => true, :compress => true, :random_iv => true
66
71
  #
67
72
  # @param [ Symbol ] name The name of the field.
68
73
  # @param [ Hash ] options The options to pass to the field.
69
74
  #
70
- # @option options [ Boolean ] :encryption If the field contains encrypted data.
71
- # @option options [ Symbol ] :decrypt_as Name of the getters and setters to generate to access the decrypted value of this field.
72
- # @option options [ Class ] :type The type of the field.
73
- # @option options [ String ] :label The label for the field.
74
- # @option options [ Object, Proc ] :default The field's default
75
+ # @option options [ Boolean ] :encrypted If the field contains encrypted data.
76
+ # @option options [ Symbol ] :decrypt_as Name of the getters and setters to generate to access the decrypted value of this field.
77
+ # @option options [ Boolean ] :compress Whether to compress this encrypted field
78
+ # @option options [ Boolean ] :random_iv Whether the encrypted value should use a random IV every time the field is encrypted.
79
+ #
80
+ # @option options [ Class ] :type The type of the field.
81
+ # @option options [ String ] :label The label for the field.
82
+ # @option options [ Object, Proc ] :default The fields default
75
83
  #
76
84
  # @return [ Field ] The generated field
77
85
  def field_with_symmetric_encryption(field_name, options={})
@@ -82,20 +90,23 @@ module Mongoid
82
90
  decrypt_as = field_name.to_s['encrypted_'.length..-1]
83
91
  end
84
92
 
93
+ random_iv = options.delete(:random_iv) || false
94
+ compress = options.delete(:compress) || false
95
+
85
96
  # Store Intended data type for this field, but we store it as a String
86
97
  underlying_type = options[:type]
87
98
  options[:type] = String
88
99
 
89
100
  raise "SymmetricEncryption for Mongoid currently only supports :type => String" unless underlying_type == String
90
101
 
91
- # #TODO Need to do type conversions. Currently only support String
102
+ # #TODO Need to do type conversions. Currently only supports String
92
103
 
93
104
  # Generate getter and setter methods
94
105
  class_eval(<<-EOS, __FILE__, __LINE__ + 1)
95
- # Set the un-encrypted bank account number
106
+ # Set the un-encrypted field
96
107
  # Also updates the encrypted field with the encrypted value
97
108
  def #{decrypt_as}=(value)
98
- @stored_#{field_name} = SymmetricEncryption.encrypt(value)
109
+ @stored_#{field_name} = ::SymmetricEncryption.encrypt(value,#{random_iv},#{compress})
99
110
  self.#{field_name} = @stored_#{field_name}
100
111
  @#{decrypt_as} = value
101
112
  end
@@ -105,7 +116,7 @@ module Mongoid
105
116
  # If this method is not called, then the encrypted value is never decrypted
106
117
  def #{decrypt_as}
107
118
  if @stored_#{field_name} != self.#{field_name}
108
- @#{decrypt_as} = SymmetricEncryption.decrypt(self.#{field_name})
119
+ @#{decrypt_as} = ::SymmetricEncryption.decrypt(self.#{field_name})
109
120
  @stored_#{field_name} = self.#{field_name}
110
121
  end
111
122
  @#{decrypt_as}
@@ -10,7 +10,7 @@ module SymmetricEncryption #:nodoc:
10
10
  # config.symmetric_encryption.cipher = SymmetricEncryption::Cipher.new(
11
11
  # :key => '1234567890ABCDEF1234567890ABCDEF',
12
12
  # :iv => '1234567890ABCDEF',
13
- # :cipher => 'aes-128-cbc'
13
+ # :cipher_name => 'aes-128-cbc'
14
14
  # )
15
15
  # end
16
16
  # end
@@ -26,9 +26,9 @@ module SymmetricEncryption #:nodoc:
26
26
  # @example symmetric-encryption.yml
27
27
  #
28
28
  # development:
29
- # cipher: aes-256-cbc
30
- # symmetric_key: 1234567890ABCDEF1234567890ABCDEF
31
- # symmetric_iv: 1234567890ABCDEF
29
+ # cipher_name: aes-256-cbc
30
+ # key: 1234567890ABCDEF1234567890ABCDEF
31
+ # iv: 1234567890ABCDEF
32
32
  #
33
33
  # Loaded before Active Record initializes since database.yml can have encrypted
34
34
  # passwords in it
@@ -1,3 +1,5 @@
1
+ require 'openssl'
2
+
1
3
  module SymmetricEncryption
2
4
  # Read from encrypted files and other IO streams
3
5
  #
@@ -293,12 +295,12 @@ module SymmetricEncryption
293
295
  buf = @ios.read(@buffer_size)
294
296
 
295
297
  # Use cipher specified in header, or global cipher if it has no header
296
- @cipher, @compressed = SymmetricEncryption::Cipher.parse_magic_header!(buf, @version)
297
-
298
- # Use supplied version if cipher could not be detected due to missing header
299
- @cipher ||= SymmetricEncryption.cipher(@version)
298
+ @compressed, iv, key, cipher_name, decryption_cipher = SymmetricEncryption::Cipher.parse_magic_header!(buf, @version)
300
299
 
301
- @stream_cipher = @cipher.send(:openssl_cipher, :decrypt)
300
+ @stream_cipher = ::OpenSSL::Cipher.new(cipher_name || decryption_cipher.cipher_name)
301
+ @stream_cipher.decrypt
302
+ @stream_cipher.key = key || decryption_cipher.send(:key)
303
+ @stream_cipher.iv = iv || decryption_cipher.send(:iv)
302
304
 
303
305
  # First call to #update should return an empty string anyway
304
306
  @read_buffer = @stream_cipher.update(buf)
@@ -85,11 +85,40 @@ module SymmetricEncryption
85
85
  # Returns result as a Base64 encoded string
86
86
  # Returns nil if the supplied str is nil
87
87
  # Returns "" if it is a string and it is empty
88
- def self.encrypt(str)
88
+ #
89
+ # Parameters
90
+ # str [String]
91
+ # String to be encrypted. If str is not a string, #to_s will be called on it
92
+ # to convert it to a string
93
+ #
94
+ # random_iv [true|false]
95
+ # Whether the encypted value should use a random IV every time the
96
+ # field is encrypted.
97
+ # It is recommended to set this to true where feasible. If the encrypted
98
+ # value could be used as part of a SQL where clause, or as part
99
+ # of any lookup, then it must be false.
100
+ # Setting random_iv to true will result in a different encrypted output for
101
+ # the same input string.
102
+ # Note: Only set to true if the field will never be used as part of
103
+ # the where clause in an SQL query.
104
+ # Note: When random_iv is true it will add a 8 byte header, plus the bytes
105
+ # to store the random IV in every returned encrypted string, prior to the
106
+ # encoding if any.
107
+ # Default: false
108
+ # Highly Recommended where feasible: true
109
+ #
110
+ # compress [true|false]
111
+ # Whether to compress str before encryption
112
+ # Should only be used for large strings since compression overhead and
113
+ # the overhead of adding the 'magic' header may exceed any benefits of
114
+ # compression
115
+ # Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
116
+ # Default: false
117
+ def self.encrypt(str, random_iv=false, compress=false)
89
118
  raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher
90
119
 
91
120
  # Encrypt and then encode the supplied string
92
- @@cipher.encrypt(str)
121
+ @@cipher.encrypt(str, random_iv, compress)
93
122
  end
94
123
 
95
124
  # Invokes decrypt
@@ -159,13 +188,13 @@ module SymmetricEncryption
159
188
  cipher_cfg = config[:ciphers].first
160
189
  key_filename = cipher_cfg[:key_filename]
161
190
  iv_filename = cipher_cfg[:iv_filename]
162
- cipher = cipher_cfg[:cipher]
191
+ cipher_name = cipher_cfg[:cipher_name] || cipher_cfg[:cipher]
163
192
 
164
193
  raise "The configuration file must contain a 'private_rsa_key' parameter to generate symmetric keys" unless config[:private_rsa_key]
165
194
  rsa_key = OpenSSL::PKey::RSA.new(config[:private_rsa_key])
166
195
 
167
196
  # Generate a new Symmetric Key pair
168
- key_pair = SymmetricEncryption::Cipher.random_key_pair(cipher || 'aes-256-cbc', !iv_filename.nil?)
197
+ key_pair = SymmetricEncryption::Cipher.random_key_pair(cipher_name || 'aes-256-cbc', !iv_filename.nil?)
169
198
 
170
199
  # Save symmetric key after encrypting it with the private RSA key, backing up existing files if present
171
200
  File.rename(key_filename, "#{key_filename}.#{Time.now.to_i}") if File.exist?(key_filename)
@@ -207,15 +236,15 @@ module SymmetricEncryption
207
236
  config = YAML.load_file(filename || File.join(Rails.root, "config", "symmetric-encryption.yml"))[environment || Rails.env]
208
237
 
209
238
  # Default cipher
210
- default_cipher = config['cipher'] || 'aes-256-cbc'
239
+ default_cipher = config['cipher_name'] || config['cipher'] || 'aes-256-cbc'
211
240
  cfg = {}
212
241
 
213
242
  # Hard coded symmetric_key? - Dev / Testing use only!
214
243
  if symmetric_key = (config['key'] || config['symmetric_key'])
215
244
  raise "SymmetricEncryption Cannot hard code Production encryption keys in #{filename}" if (environment || Rails.env) == 'production'
216
- cfg[:key] = symmetric_key
217
- cfg[:iv] = config['iv'] || config['symmetric_iv']
218
- cfg[:cipher] = default_cipher
245
+ cfg[:key] = symmetric_key
246
+ cfg[:iv] = config['iv'] || config['symmetric_iv']
247
+ cfg[:cipher_name] = default_cipher
219
248
 
220
249
  elsif ciphers = config['ciphers']
221
250
  raise "Missing mandatory config parameter 'private_rsa_key'" unless cfg[:private_rsa_key] = config['private_rsa_key']
@@ -225,7 +254,7 @@ module SymmetricEncryption
225
254
  raise "Missing mandatory 'key_filename' for environment:#{environment} in #{filename}" unless key_filename
226
255
  iv_filename = cipher_cfg['iv_filename'] || cipher_cfg['symmetric_iv_filename']
227
256
  {
228
- :cipher => cipher_cfg['cipher'] || default_cipher,
257
+ :cipher_name => cipher_cfg['cipher_name'] || cipher_cfg['cipher'] || default_cipher,
229
258
  :key_filename => key_filename,
230
259
  :iv_filename => iv_filename,
231
260
  :encoding => cipher_cfg['encoding'],
@@ -237,7 +266,7 @@ module SymmetricEncryption
237
266
  # Migrate old format config
238
267
  raise "Missing mandatory config parameter 'private_rsa_key'" unless cfg[:private_rsa_key] = config['private_rsa_key']
239
268
  cfg[:ciphers] = [ {
240
- :cipher => default_cipher,
269
+ :cipher_name => default_cipher,
241
270
  :key_filename => config['symmetric_key_filename'],
242
271
  :iv_filename => config['symmetric_iv_filename'],
243
272
  } ]
@@ -252,16 +281,17 @@ module SymmetricEncryption
252
281
  # Raises an Exception on failure
253
282
  #
254
283
  # Parameters:
255
- # cipher
256
- # Encryption cipher for the symmetric encryption key
257
- # private_key
284
+ # private_rsa_key
258
285
  # Key used to unlock file containing the actual symmetric key
259
- # key_filename
260
- # Name of file containing symmetric key encrypted using the public
261
- # key matching the supplied private_key
262
- # iv_filename
263
- # Optional. Name of file containing symmetric key initialization vector
264
- # encrypted using the public key matching the supplied private_key
286
+ # cipher_conf Hash:
287
+ # cipher_name
288
+ # Encryption cipher name for the symmetric encryption key
289
+ # key_filename
290
+ # Name of file containing symmetric key encrypted using the public
291
+ # key matching the supplied private_key
292
+ # iv_filename
293
+ # Optional. Name of file containing symmetric key initialization vector
294
+ # encrypted using the public key matching the supplied private_key
265
295
  def self.cipher_from_encrypted_files(private_rsa_key, cipher_conf)
266
296
  # Load Encrypted Symmetric keys
267
297
  key_filename = cipher_conf[:key_filename]
@@ -286,11 +316,11 @@ module SymmetricEncryption
286
316
  rsa = OpenSSL::PKey::RSA.new(private_rsa_key)
287
317
  iv = rsa.private_decrypt(encrypted_iv) if iv_filename
288
318
  Cipher.new(
289
- :key => rsa.private_decrypt(encrypted_key),
290
- :iv => iv,
291
- :cipher => cipher_conf[:cipher],
292
- :encoding => cipher_conf[:encoding],
293
- :version => cipher_conf[:version]
319
+ :key => rsa.private_decrypt(encrypted_key),
320
+ :iv => iv,
321
+ :cipher_name => cipher_conf[:cipher_name],
322
+ :encoding => cipher_conf[:encoding],
323
+ :version => cipher_conf[:version]
294
324
  )
295
325
  end
296
326
 
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module SymmetricEncryption #:nodoc
3
- VERSION = "1.1.1"
3
+ VERSION = "2.0.0"
4
4
  end
@@ -1,3 +1,5 @@
1
+ require 'openssl'
2
+
1
3
  module SymmetricEncryption
2
4
  # Write to encrypted files and other IO streams
3
5
  #
@@ -24,12 +26,23 @@ module SymmetricEncryption
24
26
  # Default: false
25
27
  #
26
28
  # :random_key [true|false]
27
- # Generates a new random key and iv for every new file or stream
29
+ # Generates a new random key for every new file or stream
28
30
  # If true, it forces header to true. Version below then has no effect
29
- # The Random key and iv will be written to the file/stream in encrypted
31
+ # The Random key will be written to the file/stream in encrypted
30
32
  # form as part of the header
31
- # The key and iv are both encrypted using the global key
33
+ # The key is encrypted using the global key
32
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
33
46
  # Recommended: true. Setting to false will eventually expose the
34
47
  # encryption key since too much data will be encrypted using the
35
48
  # same encryption key
@@ -49,12 +62,17 @@ module SymmetricEncryption
49
62
  #
50
63
  # When random_key is false, the version of the encryption key to use
51
64
  # to encrypt the entire file
52
- # Default: Current primary key
65
+ # Default: SymmetricEncryption.cipher
53
66
  #
54
67
  # :mode
55
68
  # See File.open for open modes
56
69
  # Default: 'w'
57
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
75
+ #
58
76
  # Note: Compression occurs before encryption
59
77
  #
60
78
  #
@@ -97,22 +115,50 @@ module SymmetricEncryption
97
115
 
98
116
  # Encrypt data before writing to the supplied stream
99
117
  def initialize(ios,options={})
100
- @ios = ios
101
- header = options.fetch(:header, true)
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 "When :random_key is true, :random_iv must also be true" if random_key && !random_iv
102
123
  # Compress is only used at this point for setting the flag in the header
103
- random_key = options.fetch(:random_key, true)
104
- compress = options.fetch(:compress, false)
105
- # Force header if compressed or using random iv, key pair
106
- header = true if compress || random_key
124
+ compress = options.fetch(:compress, false)
125
+ version = options[:version]
126
+ cipher_name = options[:cipher_name]
127
+ raise "Cannot supply a :cipher_name unless both :random_key and :random_iv are true" if cipher_name && !random_key && !random_iv
128
+
129
+ # Force header if compressed or using random iv, key
130
+ header = true if compress || random_key || random_iv
131
+
132
+ cipher = nil
133
+ if random_key
134
+ # Version of key used to encrypt the random key
135
+ version = SymmetricEncryption.cipher.version
136
+ else
137
+ # Use global key if a new random one is not being generated
138
+ cipher = SymmetricEncryption.cipher(version)
139
+ raise "Cipher with version:#{version} not found in any of the configured SymmetricEncryption ciphers" unless cipher
140
+ # Version of key used to encrypt the data
141
+ version = cipher.version
142
+ end
143
+
144
+ @stream_cipher = ::OpenSSL::Cipher.new(cipher_name || SymmetricEncryption.cipher.cipher_name)
145
+ @stream_cipher.encrypt
107
146
 
108
- # Create random cipher or use global primary cipher
109
- @cipher = random_key ? SymmetricEncryption::Cipher.random_cipher : SymmetricEncryption.cipher(options[:version])
110
- raise "Cipher with version:#{options[:version]} not found in any of the configured SymmetricEncryption ciphers" unless @cipher
147
+ key = random_key ? @stream_cipher.random_key : cipher.send(:key)
148
+ iv = random_iv ? @stream_cipher.random_iv : cipher.send(:iv)
111
149
 
112
- @stream_cipher = @cipher.send(:openssl_cipher, :encrypt)
150
+ @stream_cipher.key = key
151
+ @stream_cipher.iv = iv if iv
113
152
 
114
153
  # Write the Encryption header including the random iv, key, and cipher
115
- @ios.write(@cipher.magic_header(compress, random_key, random_key, random_key)) if header
154
+ if header
155
+ @ios.write(Cipher.magic_header(
156
+ version,
157
+ compress,
158
+ random_iv ? iv : nil,
159
+ random_key ? key : nil,
160
+ cipher_name))
161
+ end
116
162
  end
117
163
 
118
164
  # Close the IO Stream