shuber-attr_encrypted 1.0.0

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