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 +10 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +278 -0
- data/Rakefile +22 -0
- data/lib/attr_encrypted.rb +10 -0
- data/lib/huberry/active_record.rb +47 -0
- data/lib/huberry/class.rb +141 -0
- data/lib/huberry/object.rb +49 -0
- data/test/active_record_test.rb +74 -0
- data/test/attr_encrypted_test.rb +167 -0
- data/test/test_helper.rb +13 -0
- metadata +82 -0
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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|