symmetric-encryption 1.1.1 → 2.0.0

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