shuber-attr_encrypted 1.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.
data/CHANGELOG ADDED
@@ -0,0 +1,10 @@
1
+ 2009-01-08 - Sean Huber (shuber@huberry.com)
2
+ * Update comments and documentation
3
+ * Update README
4
+ * Add gemspec
5
+
6
+ 2009-01-07 - Sean Huber (shuber@huberry.com)
7
+ * Initial commit
8
+ * Add comments/documentation to the active record related logic
9
+ * Add more attr_encrypted tests
10
+ * Update tests and comments
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Sean Huber - shuber@huberry.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,278 @@
1
+ attr\_encrypted
2
+ ===============
3
+
4
+ Generates attr\_accessors that encrypt and decrypt attributes transparently
5
+
6
+
7
+ Installation
8
+ ------------
9
+
10
+ gem install shuber-attr_encrypted --source http://gems.github.com
11
+
12
+
13
+ Usage
14
+ -----
15
+
16
+ ### Basic ###
17
+
18
+ Encrypting attributes has never been easier:
19
+
20
+ class User
21
+ attr_accessor :name
22
+ attr_encrypted :ssn, :key => 'a secret key'
23
+
24
+ def load
25
+ # loads the stored data
26
+ end
27
+
28
+ def save
29
+ # saves the :name and :encrypted_ssn attributes somewhere (e.g. filesystem, database, etc)
30
+ end
31
+ end
32
+
33
+ @user = User.new
34
+ @user.ssn = '123-45-6789'
35
+ @user.encrypted_ssn # returns the encrypted version of :ssn
36
+ @user.save
37
+
38
+ @user = User.load
39
+ @user.ssn # decrypts :encrypted_ssn and returns '123-45-6789'
40
+
41
+
42
+ ### Specifying the encrypted attribute name ###
43
+
44
+ By default, the encrypted attribute name is `encrypted_#{attribute}` (e.g. `attr_encrypted :email` would create an attribute named `encrypted_email`).
45
+ You have a couple of options if you want to name your attribute something else.
46
+
47
+ #### The `:attribute` option ####
48
+
49
+ You can simply pass the name of the encrypted attribute as the `:attribute` option:
50
+
51
+ class User
52
+ attr_encrypted :email, :key => 'a secret key', :attribute => 'email_encrypted'
53
+ end
54
+
55
+ This would generate an attribute named `email_encrypted`
56
+
57
+
58
+ #### The `:prefix` and `:suffix` options ####
59
+
60
+ If you're planning on encrypting a few different attributes and you don't like the `encrypted_#{attribute}` naming convention then you can specify your own:
61
+
62
+ class User
63
+ attr_encrypted :email, :credit_card, :ssn, :key => 'a secret key', :prefix => 'secret_', :suffix => '_crypted'
64
+ end
65
+
66
+ This would generate the following attributes: `secret_email_crypted`, `secret_credit_card_crypted`, and `secret_ssn_crypted`.
67
+
68
+
69
+ ### Encryption keys ###
70
+
71
+ Although a `:key` option may not be required (see custom encryptor below), it has a few special features
72
+
73
+ #### Unique keys for each attribute ####
74
+
75
+ You can specify unique keys for each attribute if you'd like:
76
+
77
+ class User
78
+ attr_encrypted :email, :key => 'a secret key'
79
+ attr_encrypted :ssn, :key => 'a different secret key'
80
+ end
81
+
82
+
83
+ #### Symbols representing instance methods as keys ####
84
+
85
+ If your class has an instance method that determines the encryption key to use, simply pass a symbol representing it like so:
86
+
87
+ class User
88
+ attr_encrypted :email, :key => :encryption_key
89
+
90
+ def encryption_key
91
+ # does some fancy logic and returns an encryption key
92
+ end
93
+ end
94
+
95
+
96
+ #### Procs as keys ####
97
+
98
+ You can pass a proc object as the `:key` option as well:
99
+
100
+ class User
101
+ attr_encrypted :email, :key => proc { |user| ... }
102
+ end
103
+
104
+
105
+ ### Custom encryptor ###
106
+
107
+ The [Huberry::Encryptor](http://github.com/shuber/encryptor) class is used by default. You may use your own custom encryptor by specifying
108
+ the `:encryptor`, `:encrypt_method`, and `:decrypt_method` options
109
+
110
+ Lets suppose you'd like to use this custom encryptor class:
111
+
112
+ class SillyEncryptor
113
+ def self.silly_encrypt(options)
114
+ (options[:value] + options[:secret_key]).reverse
115
+ end
116
+
117
+ def self.silly_decrypt(options)
118
+ options[:value].reverse.gsub(/#{options[:secret_key]}$/, '')
119
+ end
120
+ end
121
+
122
+ Simply set up your class like so:
123
+
124
+ class User
125
+ attr_encrypted :email, :secret_key => 'a secret key', :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt
126
+ end
127
+
128
+ Any options that you pass to `attr_encrypted` will be passed to the encryptor along with the `:value` option which contains the string to encrypt/decrypt.
129
+ Notice it uses `:secret_key` instead of `:key`.
130
+
131
+
132
+ ### Custom algorithms ###
133
+
134
+ The default [Huberry::Encryptor](http://github.com/shuber/encryptor) uses the standard ruby OpenSSL library. It's default algorithm is `aes-256-cbc`. You can
135
+ modify this by passing the `:algorithm` option to the `attr_encrypted` call like so:
136
+
137
+ class User
138
+ attr_encrypted :email, :key => 'a secret key', :algorithm => 'bf'
139
+ end
140
+
141
+ Run `openssl list-cipher-commands` to view a list of algorithms supported on your platform. See [http://github.com/shuber/encryptor](http://github.com/shuber/encryptor) for more information.
142
+
143
+ aes-128-cbc
144
+ aes-128-ecb
145
+ aes-192-cbc
146
+ aes-192-ecb
147
+ aes-256-cbc
148
+ aes-256-ecb
149
+ base64
150
+ bf
151
+ bf-cbc
152
+ bf-cfb
153
+ bf-ecb
154
+ bf-ofb
155
+ cast
156
+ cast-cbc
157
+ cast5-cbc
158
+ cast5-cfb
159
+ cast5-ecb
160
+ cast5-ofb
161
+ des
162
+ des-cbc
163
+ des-cfb
164
+ des-ecb
165
+ des-ede
166
+ des-ede-cbc
167
+ des-ede-cfb
168
+ des-ede-ofb
169
+ des-ede3
170
+ des-ede3-cbc
171
+ des-ede3-cfb
172
+ des-ede3-ofb
173
+ des-ofb
174
+ des3
175
+ desx
176
+ idea
177
+ idea-cbc
178
+ idea-cfb
179
+ idea-ecb
180
+ idea-ofb
181
+ rc2
182
+ rc2-40-cbc
183
+ rc2-64-cbc
184
+ rc2-cbc
185
+ rc2-cfb
186
+ rc2-ecb
187
+ rc2-ofb
188
+ rc4
189
+ rc4-40
190
+
191
+
192
+ ### Default options ###
193
+
194
+ Let's imagine that you have a few attributes that you want to encrypt with different keys, but you don't like the `encrypted_#{attribute}` naming convention.
195
+ Instead of having to define your class like this:
196
+
197
+ class User
198
+ attr_encrypted :email, :key => 'a secret key', :prefix => '', :suffix => '_crypted'
199
+ attr_encrypted :ssn, :key => 'a different secret key', :prefix => '', :suffix => '_crypted'
200
+ attr_encrypted :credit_card, :key => 'another secret key', :prefix => '', :suffix => '_crypted'
201
+ end
202
+
203
+ You can simply define some default options like so:
204
+
205
+ class User
206
+ attr_encrypted_options.merge!(:prefix => '', :suffix => '_crypted')
207
+ attr_encrypted :email, :key => 'a secret key'
208
+ attr_encrypted :ssn, :key => 'a different secret key'
209
+ attr_encrypted :credit_card, :key => 'another secret key'
210
+ end
211
+
212
+ This should help keep your classes clean and DRY.
213
+
214
+
215
+ ### Encoding ###
216
+
217
+ You're probably going to be storing your encrypted attributes somehow (e.g. filesystem, database, etc) and may run into some issues trying to store a weird
218
+ encrypted string. I've had this problem myself using MySQL. You can simply pass the `:encode` option to automatically base64 encode/decode when encrypting/decrypting.
219
+
220
+ class User
221
+ attr_encrypted :email, :key => 'some secret key', :encode => true
222
+ end
223
+
224
+
225
+ ### Marshaling ###
226
+
227
+ You may want to encrypt objects other than strings (e.g. hashes, arrays, etc). If this is the case, simply pass the `:marshal` option to automatically marshal
228
+ when encrypting/decrypting.
229
+
230
+ class User
231
+ attr_encrypted :credentials, :key => 'some secret key', :marshal => true
232
+ end
233
+
234
+
235
+ ### Encrypt/decrypt attribute methods ###
236
+
237
+ If you use the same key to encrypt every record (per attribute) like this:
238
+
239
+ class User
240
+ attr_encrypted :email, :key => 'a secret key'
241
+ end
242
+
243
+ Then you'll have these two class methods available for each attribute: `User.encrypt_email(email_to_encrypt)` and `User.decrypt_email(email_to_decrypt)`. This can
244
+ be useful when you're using ActiveRecord (see below).
245
+
246
+
247
+ ### ActiveRecord ###
248
+
249
+ If you're using this gem with ActiveRecord, you get a few extra features:
250
+
251
+ #### Default options ####
252
+
253
+ For your convenience, the `:encode` and `:marshal` options are set to true by default since you'll be storing everything in a database.
254
+
255
+
256
+ #### Dynamic find\_by\_ and scoped\_by\_ methods ####
257
+
258
+ Let's say you'd like to encrypt your user's email addresses, but you also need a way for them to login. Simply set up your class like so:
259
+
260
+ class User < ActiveRecord::Base
261
+ attr_encrypted :email, :key => 'a secret key'
262
+ attr_encrypted :password, :key => 'some other secret key'
263
+ end
264
+
265
+ You can now lookup and login users like so:
266
+
267
+ User.find_by_email_and_password('test@example.com', 'testing')
268
+
269
+ The call to `find_by_email_and_password` is intercepted and modified to `find_by_encrypted_email_and_encrypted_password('ENCRYPTED EMAIL', 'ENCRYPTED PASSWORD')`.
270
+ The dynamic scope methods like `scoped_by_email_and_password` work the same way.
271
+
272
+ NOTE: This only works if all records are encrypted with the same encryption key (per attribute).
273
+
274
+
275
+ Contact
276
+ -------
277
+
278
+ Problems, comments, and suggestions all welcome: [shuber@huberry.com](mailto:shuber@huberry.com)
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the attr_encrypted gem.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the attr_encrypted gem.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'attr_encrypted'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README.markdown')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,10 @@
1
+ require 'huberry/class'
2
+ Class.send :include, Huberry::Class
3
+
4
+ require 'huberry/object'
5
+ Object.send :include, Huberry::Object
6
+
7
+ if defined?(ActiveRecord)
8
+ require 'huberry/active_record'
9
+ ActiveRecord::Base.extend Huberry::ActiveRecord
10
+ end
@@ -0,0 +1,47 @@
1
+ module Huberry
2
+ module ActiveRecord
3
+ def self.extended(base)
4
+ base.eigenclass_eval { alias_method_chain :method_missing, :attr_encrypted }
5
+ end
6
+
7
+ protected
8
+
9
+ # Calls attr_encrypted with the options <tt>:encode</tt> and <tt>:marshal</tt> set to true
10
+ # unless they've already been specified
11
+ def attr_encrypted(*attrs)
12
+ options = { :encode => true, :marshal => true }.merge(attrs.last.is_a?(Hash) ? attrs.pop : {})
13
+ super *(attrs << options)
14
+ end
15
+
16
+ # Allows you to use dynamic methods like <tt>find_by_email</tt> or <tt>scoped_by_email</tt> for
17
+ # encrypted attributes
18
+ #
19
+ # NOTE: This only works when the <tt>:key</tt> option is specified as a string (see the README)
20
+ #
21
+ # This is useful for encrypting fields like email addresses. Your user's email addresses
22
+ # are encrypted in the database, but you can still look up a user by email for logging in
23
+ #
24
+ # Example
25
+ #
26
+ # class User < ActiveRecord::Base
27
+ # attr_encrypted :email, :key => 'secret key'
28
+ # end
29
+ #
30
+ # User.find_by_email_and_password('test@example.com', 'testing')
31
+ # # results in a call to
32
+ # User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing')
33
+ def method_missing_with_attr_encrypted(method, *args, &block)
34
+ if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
35
+ attribute_names = match.captures.last.split('_and_')
36
+ attribute_names.each_with_index do |attribute, index|
37
+ if attr_encrypted?(attribute)
38
+ args[index] = send("encrypt_#{attribute}", args[index])
39
+ attribute_names[index] = encrypted_attributes[attribute]
40
+ end
41
+ end
42
+ method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
43
+ end
44
+ method_missing_without_attr_encrypted(method, *args, &block)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,141 @@
1
+ module Huberry
2
+ module Class
3
+ protected
4
+ # Generates attr_accessors that encrypt and decrypt attributes transparently
5
+ #
6
+ # Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)
7
+ #
8
+ # :attribute => The name of the referenced encrypted attribute. For example
9
+ # <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
10
+ # attribute named 'ee' to store the encrypted email. This is useful when defining
11
+ # one attribute to encrypt at a time or when the :prefix and :suffix options
12
+ # aren't enough. Defaults to nil.
13
+ #
14
+ # :prefix => A prefix used to generate the name of the referenced encrypted attributes.
15
+ # For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
16
+ # generate attributes named 'crypted_email' and 'crypted_password' to store the
17
+ # encrypted email and password. Defaults to 'encrypted_'.
18
+ #
19
+ # :suffix => A suffix used to generate the name of the referenced encrypted attributes.
20
+ # For example <tt>attr_accessor :email, :password, :prefix => '', :suffix => '_encrypted'</tt>
21
+ # would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
22
+ # encrypted email. Defaults to ''.
23
+ #
24
+ # :key => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
25
+ # a symbol representing an instance method then the :key option will be replaced with the result of the
26
+ # method before being passed to the encryptor. Proc objects are evaluated as well. Any other key types
27
+ # will be passed directly to the encryptor.
28
+ #
29
+ # :encode => If set to true, attributes will be base64 encoded as well as encrypted. This is useful if you're
30
+ # planning on storing the encrypted attributes in a database. Defaults to false unless you're using
31
+ # it with ActiveRecord.
32
+ #
33
+ # :marshal => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
34
+ # on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord.
35
+ #
36
+ # :encryptor => The object to use for encrypting. Defaults to Huberry::Encryptor.
37
+ #
38
+ # :encrypt_method => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to :encrypt.
39
+ #
40
+ # :decrypt_method => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to :decrypt.
41
+ #
42
+ #
43
+ # You can specify your own default options
44
+ #
45
+ # class User
46
+ # # now all attributes will be encoded and marshaled by default
47
+ # attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
48
+ # attr_encrypted :configuration
49
+ # end
50
+ #
51
+ #
52
+ # Example
53
+ #
54
+ # class User
55
+ # attr_encrypted :email, :credit_card, :key => 'some secret key'
56
+ # attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
57
+ # end
58
+ #
59
+ # @user = User.new
60
+ # @user.encrypted_email # returns nil
61
+ # @user.email = 'test@example.com'
62
+ # @user.encrypted_email # returns the encrypted version of 'test@example.com'
63
+ #
64
+ # @user.configuration = { :time_zone => 'UTC' }
65
+ # @user.encrypted_configuration # returns the encrypted version of configuration
66
+ #
67
+ # See README for more examples
68
+ def attr_encrypted(*attrs)
69
+ options = {
70
+ :prefix => 'encrypted_',
71
+ :suffix => '',
72
+ :encryptor => Huberry::Encryptor,
73
+ :encrypt_method => :encrypt,
74
+ :decrypt_method => :decrypt,
75
+ :encode => false,
76
+ :marshal => false
77
+ }.merge(attr_encrypted_options).merge(attrs.last.is_a?(Hash) ? attrs.pop : {})
78
+
79
+ attrs.each do |attribute|
80
+ encrypted_attribute_name = options[:attribute].nil? ? options[:prefix].to_s + attribute.to_s + options[:suffix].to_s : options[:attribute].to_s
81
+
82
+ encrypted_attributes[attribute.to_s] = encrypted_attribute_name
83
+
84
+ attr_accessor encrypted_attribute_name.to_sym unless self.new.respond_to?(encrypted_attribute_name)
85
+
86
+ define_class_method "encrypt_#{attribute}" do |value|
87
+ if value.nil?
88
+ encrypted_value = nil
89
+ else
90
+ value = Marshal.dump(value) if options[:marshal]
91
+ encrypted_value = options[:encryptor].send options[:encrypt_method], options.merge(:value => value)
92
+ encrypted_value = [encrypted_value].pack('m*') if options[:encode]
93
+ end
94
+ encrypted_value
95
+ end
96
+
97
+ define_class_method "decrypt_#{attribute}" do |encrypted_value|
98
+ if encrypted_value.nil?
99
+ decrypted_value = nil
100
+ else
101
+ encrypted_value = encrypted_value.unpack('m*').to_s if options[:encode]
102
+ decrypted_value = options[:encryptor].send(options[:decrypt_method], options.merge(:value => encrypted_value))
103
+ decrypted_value = Marshal.load(decrypted_value) if options[:marshal]
104
+ end
105
+ decrypted_value
106
+ end
107
+
108
+ define_method "#{attribute}" do
109
+ value = instance_variable_get("@#{attribute}")
110
+ encrypted_value = read_attribute(encrypted_attribute_name)
111
+ original_key = options[:key]
112
+ options[:key] = self.class.send :evaluate_attr_encrypted_key, options[:key], self
113
+ value = write_attribute(attribute, self.class.send("decrypt_#{attribute}".to_sym, encrypted_value)) if value.nil? && !encrypted_value.nil?
114
+ options[:key] = original_key
115
+ value
116
+ end
117
+
118
+ define_method "#{attribute}=" do |value|
119
+ original_key = options[:key]
120
+ options[:key] = self.class.send :evaluate_attr_encrypted_key, options[:key], self
121
+ write_attribute(encrypted_attribute_name, self.class.send("encrypt_#{attribute}".to_sym, value))
122
+ options[:key] = original_key
123
+ instance_variable_set("@#{attribute}", value)
124
+ end
125
+ end
126
+ end
127
+
128
+ # Evaluates encryption keys specified as symbols (representing instance methods) or procs
129
+ # If the key is not a symbol or proc then the original key is returned
130
+ def evaluate_attr_encrypted_key(key, object)
131
+ case key
132
+ when Symbol
133
+ object.send(key)
134
+ when Proc
135
+ key.call(object)
136
+ else
137
+ key
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,49 @@
1
+ module Huberry
2
+ module Object
3
+ def self.included(base)
4
+ base.class_eval do
5
+ extend ClassMethods
6
+ eattr_accessor :attr_encrypted_options, :encrypted_attributes
7
+ attr_encrypted_options = {}
8
+ encrypted_attributes = {}
9
+ end
10
+ end
11
+
12
+ # Wraps instance_variable_get
13
+ #
14
+ # ActiveRecord overwrites this (if you're using it)
15
+ def read_attribute(attribute)
16
+ instance_variable_get("@#{attribute}")
17
+ end
18
+
19
+ # Wraps instance_variable_set
20
+ #
21
+ # ActiveRecord overwrites this (if you're using it)
22
+ def write_attribute(attribute, value)
23
+ instance_variable_set("@#{attribute}", value)
24
+ end
25
+
26
+ module ClassMethods
27
+ # Checks if an attribute has been configured to be encrypted
28
+ #
29
+ # Example
30
+ #
31
+ # class User
32
+ # attr_accessor :name
33
+ # attr_encrypted :email
34
+ # end
35
+ #
36
+ # User.attr_encrypted?(:name) # false
37
+ # User.attr_encrypted?(:email) # true
38
+ def attr_encrypted?(attribute)
39
+ encrypted_attributes.keys.include?(attribute.to_s)
40
+ end
41
+
42
+ # Copies existing encrypted attributes and options to the derived class
43
+ def inherited(base)
44
+ base.attr_encrypted_options = self.attr_encrypted_options.nil? ? {} : self.attr_encrypted_options.dup
45
+ base.encrypted_attributes = self.encrypted_attributes.nil? ? {} : self.encrypted_attributes.dup
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,74 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
4
+
5
+ def create_people_table
6
+ silence_stream(STDOUT) do
7
+ ActiveRecord::Schema.define(:version => 1) do
8
+ create_table :people do |t|
9
+ t.string :encrypted_email
10
+ t.string :password
11
+ t.string :encrypted_credentials
12
+ t.string :salt
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ # The table needs to exist before defining the class
19
+ create_people_table
20
+
21
+ class Person < ActiveRecord::Base
22
+ attr_encrypted :email, :key => 'a secret key'
23
+ attr_encrypted :credentials, :key => Proc.new { |user| Huberry::Encryptor.encrypt(:value => user.salt, :key => 'some private key') }
24
+
25
+ def after_initialize
26
+ self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(5)).to_s)
27
+ self.credentials ||= { :username => 'example', :password => 'test' }
28
+ end
29
+ end
30
+
31
+ class ActiveRecordTest < Test::Unit::TestCase
32
+
33
+ def setup
34
+ ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
35
+ create_people_table
36
+ end
37
+
38
+ def test_should_encrypt_email
39
+ @person = Person.create :email => 'test@example.com'
40
+ assert_not_nil @person.encrypted_email
41
+ assert_not_equal @person.email, @person.encrypted_email
42
+ assert_equal @person.email, Person.find(:first).email
43
+ end
44
+
45
+ def test_should_marshal_and_encrypt_credentials
46
+ @person = Person.create
47
+ assert_not_nil @person.encrypted_credentials
48
+ assert_not_equal @person.credentials, @person.encrypted_credentials
49
+ assert_equal @person.credentials, Person.find(:first).credentials
50
+ end
51
+
52
+ def test_should_find_by_email
53
+ @person = Person.create(:email => 'test@example.com')
54
+ assert_equal @person, Person.find_by_email('test@example.com')
55
+ end
56
+
57
+ def test_should_find_by_email_and_password
58
+ Person.create(:email => 'test@example.com', :password => 'invalid')
59
+ @person = Person.create(:email => 'test@example.com', :password => 'test')
60
+ assert_equal @person, Person.find_by_email_and_password('test@example.com', 'test')
61
+ end
62
+
63
+ def test_should_scope_by_email
64
+ @person = Person.create(:email => 'test@example.com')
65
+ assert_equal @person, Person.scoped_by_email('test@example.com').find(:first) rescue NoMethodError
66
+ end
67
+
68
+ def test_should_scope_by_email_and_password
69
+ Person.create(:email => 'test@example.com', :password => 'invalid')
70
+ @person = Person.create(:email => 'test@example.com', :password => 'test')
71
+ assert_equal @person, Person.scoped_by_email_and_password('test@example.com', 'test').find(:first) rescue NoMethodError
72
+ end
73
+
74
+ end
@@ -0,0 +1,167 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class SillyEncryptor
4
+ def self.silly_encrypt(options)
5
+ (options[:value] + options[:some_arg]).reverse
6
+ end
7
+
8
+ def self.silly_decrypt(options)
9
+ options[:value].reverse.gsub(/#{options[:some_arg]}$/, '')
10
+ end
11
+ end
12
+
13
+ class User
14
+ self.attr_encrypted_options[:key] = Proc.new { |user| user.class.to_s } # default key
15
+
16
+ attr_encrypted :email, :without_encoding, :key => 'secret key'
17
+ attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test'
18
+ attr_encrypted :ssn, :key => :salt, :attribute => 'ssn_encrypted'
19
+ attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test'
20
+ attr_encrypted :with_encoding, :key => 'secret key', :encode => true
21
+ attr_encrypted :with_marshaling, :key => 'secret key', :marshal => true
22
+ attr_accessor :salt
23
+
24
+ def initialize
25
+ self.salt = Time.now.to_i.to_s
26
+ end
27
+ end
28
+
29
+ class Admin < User
30
+ attr_encrypted :testing
31
+ end
32
+
33
+ class SomeOtherClass
34
+ end
35
+
36
+ class AttrEncryptedTest < Test::Unit::TestCase
37
+
38
+ def test_should_store_email_in_encrypted_attributes
39
+ assert User.encrypted_attributes.include?('email')
40
+ end
41
+
42
+ def test_should_not_store_salt_in_encrypted_attributes
43
+ assert !User.encrypted_attributes.include?('salt')
44
+ end
45
+
46
+ def test_attr_encrypted_should_return_true_for_email
47
+ assert User.attr_encrypted?('email')
48
+ end
49
+
50
+ def test_attr_encrypted_should_return_false_for_salt
51
+ assert !User.attr_encrypted?('salt')
52
+ end
53
+
54
+ def test_should_generate_an_encrypted_attribute
55
+ assert User.new.respond_to?(:encrypted_email)
56
+ end
57
+
58
+ def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix
59
+ assert User.new.respond_to?(:crypted_password_test)
60
+ end
61
+
62
+ def test_should_generate_an_encrypted_attribute_with_the_attribute_option
63
+ assert User.new.respond_to?(:ssn_encrypted)
64
+ end
65
+
66
+ def test_should_not_encrypt_nil_value
67
+ assert_nil User.encrypt_email(nil)
68
+ end
69
+
70
+ def test_should_encrypt_email
71
+ assert_not_nil User.encrypt_email('test@example.com')
72
+ assert_not_equal 'test@example.com', User.encrypt_email('test@example.com')
73
+ end
74
+
75
+ def test_should_encrypt_email_when_modifying_the_attr_writer
76
+ @user = User.new
77
+ assert_nil @user.encrypted_email
78
+ @user.email = 'test@example.com'
79
+ assert_not_nil @user.encrypted_email
80
+ assert_equal User.encrypt_email('test@example.com'), @user.encrypted_email
81
+ end
82
+
83
+ def test_should_not_decrypt_nil_value
84
+ assert_nil User.decrypt_email(nil)
85
+ end
86
+
87
+ def test_should_decrypt_email
88
+ encrypted_email = User.encrypt_email('test@example.com')
89
+ assert_not_equal 'test@test.com', encrypted_email
90
+ assert_equal 'test@example.com', User.decrypt_email(encrypted_email)
91
+ end
92
+
93
+ def test_should_decrypt_email_when_reading
94
+ @user = User.new
95
+ assert_nil @user.email
96
+ @user.encrypted_email = User.encrypt_email('test@example.com')
97
+ assert_equal 'test@example.com', @user.email
98
+ end
99
+
100
+ def test_should_encrypt_with_encoding
101
+ assert_equal User.encrypt_with_encoding('test'), [User.encrypt_without_encoding('test')].pack('m*')
102
+ end
103
+
104
+ def test_should_decrypt_with_encoding
105
+ encrypted = User.encrypt_with_encoding('test')
106
+ assert_equal 'test', User.decrypt_with_encoding(encrypted)
107
+ assert_equal User.decrypt_with_encoding(encrypted), User.decrypt_without_encoding(encrypted.unpack('m*').to_s)
108
+ end
109
+
110
+ def test_should_encrypt_with_marshaling
111
+ @user = User.new
112
+ @user.with_marshaling = [1, 2, 3]
113
+ assert_not_nil @user.encrypted_with_marshaling
114
+ assert_equal User.encrypt_with_marshaling([1, 2, 3]), @user.encrypted_with_marshaling
115
+ end
116
+
117
+ def test_should_decrypt_with_marshaling
118
+ encrypted = User.encrypt_with_marshaling([1, 2, 3])
119
+ @user = User.new
120
+ assert_nil @user.with_marshaling
121
+ @user.encrypted_with_marshaling = encrypted
122
+ assert_equal [1, 2, 3], @user.with_marshaling
123
+ end
124
+
125
+ def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments
126
+ assert_equal SillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), User.encrypt_credit_card('testing')
127
+ end
128
+
129
+ def test_should_evaluate_a_key_passed_as_a_symbol
130
+ @user = User.new
131
+ assert_nil @user.ssn_encrypted
132
+ @user.ssn = 'testing'
133
+ assert_not_nil @user.ssn_encrypted
134
+ assert_equal Huberry::Encryptor.encrypt(:value => 'testing', :key => @user.salt), @user.ssn_encrypted
135
+ end
136
+
137
+ def test_should_evaluate_a_key_passed_as_a_proc
138
+ @user = User.new
139
+ assert_nil @user.crypted_password_test
140
+ @user.password = 'testing'
141
+ assert_not_nil @user.crypted_password_test
142
+ assert_equal Huberry::Encryptor.encrypt(:value => 'testing', :key => 'User'), @user.crypted_password_test
143
+ end
144
+
145
+ def test_should_use_options_found_in_the_attr_encrypted_options_attribute
146
+ @user = User.new
147
+ assert_nil @user.crypted_password_test
148
+ @user.password = 'testing'
149
+ assert_not_nil @user.crypted_password_test
150
+ assert_equal Huberry::Encryptor.encrypt(:value => 'testing', :key => 'User'), @user.crypted_password_test
151
+ end
152
+
153
+ def test_should_inherit_encrypted_attributes
154
+ assert_equal User.encrypted_attributes.merge('testing' => 'encrypted_testing'), Admin.encrypted_attributes
155
+ end
156
+
157
+ def test_should_inherit_attr_encrypted_options
158
+ assert !User.attr_encrypted_options.empty?
159
+ assert_equal User.attr_encrypted_options, Admin.attr_encrypted_options
160
+ end
161
+
162
+ def test_should_not_inherit_unrelated_attributes
163
+ assert SomeOtherClass.attr_encrypted_options.empty?
164
+ assert SomeOtherClass.encrypted_attributes.empty?
165
+ end
166
+
167
+ end
@@ -0,0 +1,13 @@
1
+ require 'test/unit'
2
+ require 'digest/sha2'
3
+
4
+ require 'rubygems'
5
+ gem 'shuber-eigenclass', '>= 1.0.1'
6
+ gem 'shuber-encryptor'
7
+ gem 'activerecord'
8
+
9
+ require 'eigenclass'
10
+ require 'encryptor'
11
+ require 'active_record'
12
+
13
+ require File.dirname(__FILE__) + '/../lib/attr_encrypted'
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shuber-attr_encrypted
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Huber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-08 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shuber-eigenclass
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.1
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: shuber-encryptor
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.0.0
32
+ version:
33
+ description: Generates attr_accessors that encrypt and decrypt attributes transparently
34
+ email: shuber@huberry.com
35
+ executables: []
36
+
37
+ extensions: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ files:
42
+ - CHANGELOG
43
+ - lib/attr_encrypted.rb
44
+ - lib/huberry/active_record.rb
45
+ - lib/huberry/class.rb
46
+ - lib/huberry/object.rb
47
+ - MIT-LICENSE
48
+ - Rakefile
49
+ - README.markdown
50
+ - test/test_helper.rb
51
+ has_rdoc: false
52
+ homepage: http://github.com/shuber/attr_encrypted
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --line-numbers
56
+ - --inline-source
57
+ - --main
58
+ - README.markdown
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project:
76
+ rubygems_version: 1.2.0
77
+ signing_key:
78
+ specification_version: 2
79
+ summary: Generates attr_accessors that encrypt and decrypt attributes transparently
80
+ test_files:
81
+ - test/active_record_test.rb
82
+ - test/attr_encrypted_test.rb