attr_encryptor 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/.gitignore +16 -0
- data/.rvmrc +47 -0
- data/MIT-LICENSE +20 -0
- data/README.md +291 -0
- data/Rakefile +22 -0
- data/attr_encryptor.gemspec +36 -0
- data/lib/attr_encryptor.rb +326 -0
- data/lib/attr_encryptor/adapters/active_record.rb +25 -0
- data/lib/attr_encryptor/adapters/data_mapper.rb +21 -0
- data/lib/attr_encryptor/adapters/sequel.rb +14 -0
- data/lib/attr_encryptor/version.rb +17 -0
- data/test/active_record_test.rb +98 -0
- data/test/attr_encrypted_test.rb +290 -0
- data/test/data_mapper_test.rb +52 -0
- data/test/debug_order.rb +41 -0
- data/test/sequel_test.rb +50 -0
- data/test/test_helper.rb +14 -0
- metadata +149 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
|
7
|
+
environment_id="ruby-1.9.2-p290@attr_encrypted"
|
8
|
+
|
9
|
+
#
|
10
|
+
# Uncomment following line if you want options to be set only for given project.
|
11
|
+
#
|
12
|
+
# PROJECT_JRUBY_OPTS=( --1.9 )
|
13
|
+
|
14
|
+
#
|
15
|
+
# First we attempt to load the desired environment directly from the environment
|
16
|
+
# file. This is very fast and efficient compared to running through the entire
|
17
|
+
# CLI and selector. If you want feedback on which environment was used then
|
18
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
19
|
+
#
|
20
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
|
21
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
22
|
+
then
|
23
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
24
|
+
|
25
|
+
if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
|
26
|
+
then
|
27
|
+
. "${rvm_path:-$HOME/.rvm}/hooks/after_use"
|
28
|
+
fi
|
29
|
+
else
|
30
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
31
|
+
if ! rvm --create "$environment_id"
|
32
|
+
then
|
33
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
34
|
+
exit 1
|
35
|
+
fi
|
36
|
+
fi
|
37
|
+
|
38
|
+
#
|
39
|
+
# If you use an RVM gemset file to install a list of gems (*.gems), you can have
|
40
|
+
# it be automatically loaded. Uncomment the following and adjust the filename if
|
41
|
+
# necessary.
|
42
|
+
#
|
43
|
+
# filename=".gems"
|
44
|
+
# if [[ -s "$filename" ]] ; then
|
45
|
+
# rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
|
46
|
+
# fi
|
47
|
+
|
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.md
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
# attr_encryptor
|
2
|
+
|
3
|
+
Generates attr_accessors that encrypt and decrypt attributes transparently
|
4
|
+
|
5
|
+
It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord`, `DataMapper`, or `Sequel`
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
gem install attr_encryptor
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
### Basic
|
15
|
+
|
16
|
+
Encrypting attributes has never been easier:
|
17
|
+
|
18
|
+
### You database
|
19
|
+
|
20
|
+
add a `encrypted_ssn`, `encrypted_ssn_salt`, `encrypted_ssn_iv`. All of
|
21
|
+
them will be populated automatically
|
22
|
+
|
23
|
+
create_table :google_apps_admins do |t|
|
24
|
+
t.string :username
|
25
|
+
t.string :encrypted_password
|
26
|
+
t.string :encrypted_password_iv
|
27
|
+
t.string :domain
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
### Your model
|
32
|
+
|
33
|
+
class User
|
34
|
+
attr_accessor :name
|
35
|
+
attr_encrypted :ssn, :key => 'a secret key'
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
### Controllers/views
|
40
|
+
|
41
|
+
|
42
|
+
@user = User.new
|
43
|
+
@user.ssn = '123-45-6789'
|
44
|
+
@user.ssn # returns the unencrypted version of :ssn
|
45
|
+
@user.save
|
46
|
+
|
47
|
+
@user = User.load
|
48
|
+
@user.ssn # decrypts :encrypted_ssn and returns '123-45-6789'
|
49
|
+
|
50
|
+
The `attr_encrypted` method is also aliased as `attr_encryptor` to conform to Ruby's `attr_` naming conventions.
|
51
|
+
|
52
|
+
|
53
|
+
### Specifying the encrypted attribute name
|
54
|
+
|
55
|
+
By default, the encrypted attribute name is `encrypted_#{attribute}` (e.g. `attr_encrypted :email` would create an attribute named `encrypted_email`). So, if you're storing the encrypted attribute in the database, you need to make sure the `encrypted_#{attribute}` field exists in your table(as well as `encrypted_#{attribute}_iv` and `encrypted_#{attribute}_salt`). You have a couple of options if you want to name your attribute something else.
|
56
|
+
|
57
|
+
#### The `:attribute` option
|
58
|
+
|
59
|
+
You can simply pass the name of the encrypted attribute as the `:attribute` option:
|
60
|
+
|
61
|
+
class User
|
62
|
+
attr_encrypted :email, :key => 'a secret key', :attribute => 'email_encrypted'
|
63
|
+
end
|
64
|
+
|
65
|
+
This would generate an attribute named `email_encrypted`
|
66
|
+
|
67
|
+
#### The `:prefix` and `:suffix` options
|
68
|
+
|
69
|
+
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:
|
70
|
+
|
71
|
+
class User
|
72
|
+
attr_encrypted :email, :credit_card, :ssn, :key => 'a secret key', :prefix => 'secret_', :suffix => '_crypted'
|
73
|
+
end
|
74
|
+
|
75
|
+
This would generate the following attributes: `secret_email_crypted`, `secret_credit_card_crypted`, and `secret_ssn_crypted`.
|
76
|
+
|
77
|
+
|
78
|
+
### Encryption keys
|
79
|
+
|
80
|
+
Although a `:key` option may not be required (see custom encryptor below), it has a few special features
|
81
|
+
|
82
|
+
#### Unique keys for each attribute
|
83
|
+
|
84
|
+
You can specify unique keys for each attribute if you'd like:
|
85
|
+
|
86
|
+
class User
|
87
|
+
attr_encrypted :email, :key => 'a secret key'
|
88
|
+
attr_encrypted :ssn, :key => 'a different secret key'
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
#### Symbols representing instance methods as keys
|
93
|
+
|
94
|
+
If your class has an instance method that determines the encryption key to use, simply pass a symbol representing it like so:
|
95
|
+
|
96
|
+
class User
|
97
|
+
attr_encrypted :email, :key => :encryption_key
|
98
|
+
|
99
|
+
def encryption_key
|
100
|
+
# does some fancy logic and returns an encryption key
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
#### Procs as keys
|
106
|
+
|
107
|
+
You can pass a proc/lambda object as the `:key` option as well:
|
108
|
+
|
109
|
+
class User
|
110
|
+
attr_accessor :key
|
111
|
+
attr_encrypted :email, :key => proc { |user| user.key }
|
112
|
+
end
|
113
|
+
|
114
|
+
However when calling `User.new`, `User.create`, `User.update_attributes` the `:key` attribute has to precede the `:email` or key will be nil and you'll get an Exception,
|
115
|
+
|
116
|
+
### Conditional encrypting
|
117
|
+
|
118
|
+
There may be times that you want to only encrypt when certain conditions are met. For example maybe you're using rails and you don't want to encrypt
|
119
|
+
attributes when you're in development mode. You can specify conditions like this:
|
120
|
+
|
121
|
+
class User < ActiveRecord::Base
|
122
|
+
attr_encrypted :email, :key => 'a secret key', :unless => Rails.env.development?
|
123
|
+
end
|
124
|
+
|
125
|
+
You can specify both `:if` and `:unless` options. If you pass a symbol representing an instance method then the result of the method will be evaluated. Any objects that respond to `:call` are evaluated as well.
|
126
|
+
|
127
|
+
|
128
|
+
### Custom encryptor
|
129
|
+
|
130
|
+
The `Encryptor` (see http://github.com/shuber/encryptor) class is used by default. You may use your own custom encryptor by specifying
|
131
|
+
the `:encryptor`, `:encrypt_method`, and `:decrypt_method` options
|
132
|
+
|
133
|
+
Lets suppose you'd like to use this custom encryptor class:
|
134
|
+
|
135
|
+
class SillyEncryptor
|
136
|
+
def self.silly_encrypt(options)
|
137
|
+
(options[:value] + options[:secret_key]).reverse
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.silly_decrypt(options)
|
141
|
+
options[:value].reverse.gsub(/#{options[:secret_key]}$/, '')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
Simply set up your class like so:
|
146
|
+
|
147
|
+
class User
|
148
|
+
attr_encrypted :email, :secret_key => 'a secret key', :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt
|
149
|
+
end
|
150
|
+
|
151
|
+
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. Notice it uses `:secret_key` instead of `:key`.
|
152
|
+
|
153
|
+
|
154
|
+
### Custom algorithms
|
155
|
+
|
156
|
+
The default `Encryptor` uses the standard ruby OpenSSL library. It's default algorithm is `aes-256-cbc`. You can modify this by passing the `:algorithm` option to the `attr_encrypted` call like so:
|
157
|
+
|
158
|
+
class User
|
159
|
+
attr_encrypted :email, :key => 'a secret key', :algorithm => 'bf'
|
160
|
+
end
|
161
|
+
|
162
|
+
Run `openssl list-cipher-commands` to view a list of algorithms supported on your platform. See http://github.com/danpal/encryptor for more information.
|
163
|
+
|
164
|
+
aes-128-cbc
|
165
|
+
aes-128-ecb
|
166
|
+
aes-192-cbc
|
167
|
+
aes-192-ecb
|
168
|
+
aes-256-cbc
|
169
|
+
aes-256-ecb
|
170
|
+
base64
|
171
|
+
bf
|
172
|
+
bf-cbc
|
173
|
+
bf-cfb
|
174
|
+
bf-ecb
|
175
|
+
bf-ofb
|
176
|
+
cast
|
177
|
+
cast-cbc
|
178
|
+
cast5-cbc
|
179
|
+
cast5-cfb
|
180
|
+
cast5-ecb
|
181
|
+
cast5-ofb
|
182
|
+
des
|
183
|
+
des-cbc
|
184
|
+
des-cfb
|
185
|
+
des-ecb
|
186
|
+
des-ede
|
187
|
+
des-ede-cbc
|
188
|
+
des-ede-cfb
|
189
|
+
des-ede-ofb
|
190
|
+
des-ede3
|
191
|
+
des-ede3-cbc
|
192
|
+
des-ede3-cfb
|
193
|
+
des-ede3-ofb
|
194
|
+
des-ofb
|
195
|
+
des3
|
196
|
+
desx
|
197
|
+
idea
|
198
|
+
idea-cbc
|
199
|
+
idea-cfb
|
200
|
+
idea-ecb
|
201
|
+
idea-ofb
|
202
|
+
rc2
|
203
|
+
rc2-40-cbc
|
204
|
+
rc2-64-cbc
|
205
|
+
rc2-cbc
|
206
|
+
rc2-cfb
|
207
|
+
rc2-ecb
|
208
|
+
rc2-ofb
|
209
|
+
rc4
|
210
|
+
rc4-40
|
211
|
+
|
212
|
+
|
213
|
+
### Default options
|
214
|
+
|
215
|
+
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. Instead of having to define your class like this:
|
216
|
+
|
217
|
+
class User
|
218
|
+
attr_encrypted :email, :key => 'a secret key', :prefix => '', :suffix => '_crypted'
|
219
|
+
attr_encrypted :ssn, :key => 'a different secret key', :prefix => '', :suffix => '_crypted'
|
220
|
+
attr_encrypted :credit_card, :key => 'another secret key', :prefix => '', :suffix => '_crypted'
|
221
|
+
end
|
222
|
+
|
223
|
+
You can simply define some default options like so:
|
224
|
+
|
225
|
+
class User
|
226
|
+
attr_encrypted_options.merge!(:prefix => '', :suffix => '_crypted')
|
227
|
+
attr_encrypted :email, :key => 'a secret key'
|
228
|
+
attr_encrypted :ssn, :key => 'a different secret key'
|
229
|
+
attr_encrypted :credit_card, :key => 'another secret key'
|
230
|
+
end
|
231
|
+
|
232
|
+
This should help keep your classes clean and DRY.
|
233
|
+
|
234
|
+
|
235
|
+
### Encoding
|
236
|
+
|
237
|
+
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
|
238
|
+
encrypted string. I've had this problem myself using MySQL. You can simply pass the `:encode` option to automatically encode/decode when encrypting/decrypting.
|
239
|
+
|
240
|
+
class User
|
241
|
+
attr_encrypted :email, :key => 'some secret key', :encode => true
|
242
|
+
end
|
243
|
+
|
244
|
+
The default encoding is `m*` (base64). You can change this by setting `:encode => 'some encoding'`. See the `Array#pack` method at http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding options.
|
245
|
+
|
246
|
+
|
247
|
+
### Marshaling
|
248
|
+
|
249
|
+
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 when encrypting/decrypting.
|
250
|
+
|
251
|
+
class User
|
252
|
+
attr_encrypted :credentials, :key => 'some secret key', :marshal => true
|
253
|
+
end
|
254
|
+
|
255
|
+
You may also optionally specify `:marshaler`, `:dump_method`, and `:load_method` if you want to use something other than the default `Marshal` object.
|
256
|
+
|
257
|
+
|
258
|
+
### Encrypt/decrypt attribute methods
|
259
|
+
|
260
|
+
If you use the same key to encrypt every record (per attribute) like this:
|
261
|
+
|
262
|
+
class User
|
263
|
+
attr_encrypted :email, :key => 'a secret key'
|
264
|
+
end
|
265
|
+
|
266
|
+
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 be useful when you're using `ActiveRecord` (see below).
|
267
|
+
|
268
|
+
|
269
|
+
### ActiveRecord
|
270
|
+
|
271
|
+
If you're using this gem with `ActiveRecord`, you get a few extra features:
|
272
|
+
|
273
|
+
|
274
|
+
#### Default options
|
275
|
+
|
276
|
+
For your convenience, the `:encode` option is set to true by default since you'll be storing everything in a database.
|
277
|
+
|
278
|
+
|
279
|
+
### DataMapper and Sequel
|
280
|
+
|
281
|
+
Just like the default options for `ActiveRecord`, the `:encode` option is set to true by default since you'll be storing everything in a database.
|
282
|
+
|
283
|
+
|
284
|
+
## Note on Patches/Pull Requests
|
285
|
+
|
286
|
+
* Fork the project.
|
287
|
+
* Make your feature addition or bug fix.
|
288
|
+
* Add tests for it. This is important so I don't break it in a
|
289
|
+
future version unintentionally.
|
290
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
291
|
+
* Send me a pull request. Bonus points for topic branches.
|
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_encryptor 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_encryptor gem.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'attr_encryptor'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README*')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib/', __FILE__)
|
4
|
+
$:.unshift lib unless $:.include?(lib)
|
5
|
+
|
6
|
+
require 'attr_encryptor/version'
|
7
|
+
require 'date'
|
8
|
+
|
9
|
+
Gem::Specification.new do |s|
|
10
|
+
s.name = 'attr_encryptor'
|
11
|
+
s.version = AttrEncryptor::Version.string
|
12
|
+
s.date = Date.today
|
13
|
+
|
14
|
+
s.summary = 'Encrypt and decrypt attributes'
|
15
|
+
s.description = 'Generates attr_accessors that encrypt and decrypt attributes transparently'
|
16
|
+
|
17
|
+
s.author = 'Daniel Palacio'
|
18
|
+
s.email = 'danpal@gmail.com'
|
19
|
+
s.homepage = 'http://github.com/danpal/attr_encrypted'
|
20
|
+
|
21
|
+
s.has_rdoc = false
|
22
|
+
s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.rdoc']
|
23
|
+
|
24
|
+
s.require_paths = ['lib']
|
25
|
+
|
26
|
+
s.files = `git ls-files`.split("\n")
|
27
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
28
|
+
|
29
|
+
s.add_dependency('encryptor2', ['>= 1.1.1'])
|
30
|
+
s.add_development_dependency('activerecord', ['>= 2.0.0'])
|
31
|
+
s.add_development_dependency('datamapper')
|
32
|
+
s.add_development_dependency('mocha')
|
33
|
+
s.add_development_dependency('sequel')
|
34
|
+
s.add_development_dependency('dm-sqlite-adapter')
|
35
|
+
s.add_development_dependency('sqlite3')
|
36
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'encryptor'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
# Adds attr_accessors that encrypt and decrypt an object's attributes
|
5
|
+
module AttrEncryptor
|
6
|
+
autoload :Version, 'attr_encryptor/version'
|
7
|
+
|
8
|
+
def self.extended(base) # :nodoc:
|
9
|
+
base.class_eval do
|
10
|
+
include InstanceMethods
|
11
|
+
attr_writer :attr_encrypted_options
|
12
|
+
@attr_encrypted_options, @encrypted_attributes = {}, {}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generates attr_accessors that encrypt and decrypt attributes transparently
|
17
|
+
#
|
18
|
+
# Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)
|
19
|
+
#
|
20
|
+
# :attribute => The name of the referenced encrypted attribute. For example
|
21
|
+
# <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
|
22
|
+
# attribute named 'ee' to store the encrypted email. This is useful when defining
|
23
|
+
# one attribute to encrypt at a time or when the :prefix and :suffix options
|
24
|
+
# aren't enough. Defaults to nil.
|
25
|
+
#
|
26
|
+
# :prefix => A prefix used to generate the name of the referenced encrypted attributes.
|
27
|
+
# For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
|
28
|
+
# generate attributes named 'crypted_email' and 'crypted_password' to store the
|
29
|
+
# encrypted email and password. Defaults to 'encrypted_'.
|
30
|
+
#
|
31
|
+
# :suffix => A suffix used to generate the name of the referenced encrypted attributes.
|
32
|
+
# For example <tt>attr_accessor :email, :password, :prefix => '', :suffix => '_encrypted'</tt>
|
33
|
+
# would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
|
34
|
+
# encrypted email. Defaults to ''.
|
35
|
+
#
|
36
|
+
# :key => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
|
37
|
+
# a symbol representing an instance method then the :key option will be replaced with the result of the
|
38
|
+
# method before being passed to the encryptor. Objects that respond to :call are evaluated as well (including procs).
|
39
|
+
# Any other key types will be passed directly to the encryptor.
|
40
|
+
#
|
41
|
+
# :encode => If set to true, attributes will be encoded as well as encrypted. This is useful if you're
|
42
|
+
# planning on storing the encrypted attributes in a database. The default encoding is 'm' (base64),
|
43
|
+
# however this can be overwritten by setting the :encode option to some other encoding string instead of
|
44
|
+
# just 'true'. See http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding directives.
|
45
|
+
# Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
|
46
|
+
#
|
47
|
+
# :default_encoding => Defaults to 'm' (base64).
|
48
|
+
#
|
49
|
+
# :marshal => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
|
50
|
+
# on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord
|
51
|
+
# or DataMapper.
|
52
|
+
#
|
53
|
+
# :marshaler => The object to use for marshaling. Defaults to Marshal.
|
54
|
+
#
|
55
|
+
# :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
|
56
|
+
#
|
57
|
+
# :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
|
58
|
+
#
|
59
|
+
# :encryptor => The object to use for encrypting. Defaults to Encryptor.
|
60
|
+
#
|
61
|
+
# :encrypt_method => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'encrypt'.
|
62
|
+
#
|
63
|
+
# :decrypt_method => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'decrypt'.
|
64
|
+
#
|
65
|
+
# :if => Attributes are only encrypted if this option evaluates to true. If you pass a symbol representing an instance
|
66
|
+
# method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
|
67
|
+
# Defaults to true.
|
68
|
+
#
|
69
|
+
# :unless => Attributes are only encrypted if this option evaluates to false. If you pass a symbol representing an instance
|
70
|
+
# method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
|
71
|
+
# Defaults to false.
|
72
|
+
#
|
73
|
+
# You can specify your own default options
|
74
|
+
#
|
75
|
+
# class User
|
76
|
+
# # now all attributes will be encoded and marshaled by default
|
77
|
+
# attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
|
78
|
+
# attr_encrypted :configuration, :key => 'my secret key'
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
#
|
82
|
+
# Example
|
83
|
+
#
|
84
|
+
# class User
|
85
|
+
# attr_encrypted :email, :credit_card, :key => 'some secret key'
|
86
|
+
# attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# @user = User.new
|
90
|
+
# @user.encrypted_email # nil
|
91
|
+
# @user.email? # false
|
92
|
+
# @user.email = 'test@example.com'
|
93
|
+
# @user.email? # true
|
94
|
+
# @user.encrypted_email # returns the encrypted version of 'test@example.com'
|
95
|
+
#
|
96
|
+
# @user.configuration = { :time_zone => 'UTC' }
|
97
|
+
# @user.encrypted_configuration # returns the encrypted version of configuration
|
98
|
+
#
|
99
|
+
# See README for more examples
|
100
|
+
def attr_encrypted(*attributes)
|
101
|
+
options = {
|
102
|
+
:prefix => 'encrypted_',
|
103
|
+
:suffix => '',
|
104
|
+
:if => true,
|
105
|
+
:unless => false,
|
106
|
+
:encode => false,
|
107
|
+
:default_encoding => 'm',
|
108
|
+
:marshal => false,
|
109
|
+
:marshaler => Marshal,
|
110
|
+
:dump_method => 'dump',
|
111
|
+
:load_method => 'load',
|
112
|
+
:encryptor => Encryptor,
|
113
|
+
:encrypt_method => 'encrypt',
|
114
|
+
:decrypt_method => 'decrypt'
|
115
|
+
}.merge!(attr_encrypted_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {})
|
116
|
+
|
117
|
+
options[:encode] = options[:default_encoding] if options[:encode] == true
|
118
|
+
|
119
|
+
attributes.each do |attribute|
|
120
|
+
encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
|
121
|
+
|
122
|
+
instance_methods_as_symbols = instance_methods.collect { |method| method.to_sym }
|
123
|
+
attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
|
124
|
+
attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
|
125
|
+
|
126
|
+
attr_reader (encrypted_attribute_name.to_s + "_iv").to_sym unless instance_methods_as_symbols.include?((encrypted_attribute_name.to_s + "_iv").to_sym )
|
127
|
+
attr_writer (encrypted_attribute_name.to_s + "_iv").to_sym unless instance_methods_as_symbols.include?((encrypted_attribute_name.to_s + "_iv").to_sym )
|
128
|
+
|
129
|
+
attr_reader (encrypted_attribute_name.to_s + "_salt").to_sym unless instance_methods_as_symbols.include?((encrypted_attribute_name.to_s + "_salt").to_sym )
|
130
|
+
attr_writer (encrypted_attribute_name.to_s + "_salt").to_sym unless instance_methods_as_symbols.include?((encrypted_attribute_name.to_s + "_salt").to_sym )
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
define_method(attribute) do
|
135
|
+
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
|
136
|
+
end
|
137
|
+
|
138
|
+
define_method("#{attribute}=") do |value|
|
139
|
+
iv = send("#{encrypted_attribute_name.to_s + "_iv"}")
|
140
|
+
if(iv == nil)
|
141
|
+
begin
|
142
|
+
algorithm = options[:algorithm] || "aes-256-cbc"
|
143
|
+
algo = OpenSSL::Cipher::Cipher.new(algorithm)
|
144
|
+
iv = [algo.random_iv].pack("m")
|
145
|
+
send("#{encrypted_attribute_name.to_s + "_iv"}=", iv)
|
146
|
+
rescue RuntimeError
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
salt = send("#{encrypted_attribute_name.to_s + "_salt"}") || send("#{encrypted_attribute_name.to_s + "_salt"}=", Time.now.to_i.to_s)
|
151
|
+
#this add's the iv and salt on the options for this instance
|
152
|
+
self.class.encrypted_attributes[attribute.to_sym] = self.class.encrypted_attributes[attribute.to_sym].merge(:iv => iv.unpack("m").first) if (iv && !iv.empty?)
|
153
|
+
self.class.encrypted_attributes[attribute.to_sym] = self.class.encrypted_attributes[attribute.to_sym].merge(:salt => salt)
|
154
|
+
send("#{encrypted_attribute_name}=", encrypt(attribute, value))
|
155
|
+
instance_variable_set("@#{attribute}", value)
|
156
|
+
end
|
157
|
+
|
158
|
+
define_method("#{attribute}?") do
|
159
|
+
value = send(attribute)
|
160
|
+
value.respond_to?(:empty?) ? !value.empty? : !!value
|
161
|
+
end
|
162
|
+
encrypted_attributes[attribute.to_sym] = options.merge(:attribute => encrypted_attribute_name)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
alias_method :attr_encryptor, :attr_encrypted
|
166
|
+
|
167
|
+
# Default options to use with calls to <tt>attr_encrypted</tt>
|
168
|
+
#
|
169
|
+
# It will inherit existing options from its superclass
|
170
|
+
def attr_encrypted_options
|
171
|
+
@attr_encrypted_options ||= superclass.attr_encrypted_options.dup
|
172
|
+
end
|
173
|
+
|
174
|
+
# Checks if an attribute is configured with <tt>attr_encrypted</tt>
|
175
|
+
#
|
176
|
+
# Example
|
177
|
+
#
|
178
|
+
# class User
|
179
|
+
# attr_accessor :name
|
180
|
+
# attr_encrypted :email
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# User.attr_encrypted?(:name) # false
|
184
|
+
# User.attr_encrypted?(:email) # true
|
185
|
+
def attr_encrypted?(attribute)
|
186
|
+
encrypted_attributes.has_key?(attribute.to_sym)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Decrypts a value for the attribute specified
|
190
|
+
#
|
191
|
+
# Example
|
192
|
+
#
|
193
|
+
# class User
|
194
|
+
# attr_encrypted :email
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
198
|
+
def decrypt(attribute, encrypted_value, options = {})
|
199
|
+
options = encrypted_attributes[attribute.to_sym].merge(options)
|
200
|
+
if options[:if] && !options[:unless] && !encrypted_value.nil? && !(encrypted_value.is_a?(String) && encrypted_value.empty?)
|
201
|
+
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
|
202
|
+
value = options[:encryptor].send(options[:decrypt_method], options.merge!(:value => encrypted_value))
|
203
|
+
value = options[:marshaler].send(options[:load_method], value) if options[:marshal]
|
204
|
+
value
|
205
|
+
else
|
206
|
+
encrypted_value
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Encrypts a value for the attribute specified
|
211
|
+
#
|
212
|
+
# Example
|
213
|
+
#
|
214
|
+
# class User
|
215
|
+
# attr_encrypted :email
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
# encrypted_email = User.encrypt(:email, 'test@example.com')
|
219
|
+
def encrypt(attribute, value, options = {})
|
220
|
+
options = encrypted_attributes[attribute.to_sym].merge(options)
|
221
|
+
if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
|
222
|
+
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
|
223
|
+
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(:value => value))
|
224
|
+
encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
|
225
|
+
encrypted_value
|
226
|
+
else
|
227
|
+
value
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Contains a hash of encrypted attributes with virtual attribute names as keys
|
232
|
+
# and their corresponding options as values
|
233
|
+
#
|
234
|
+
# Example
|
235
|
+
#
|
236
|
+
# class User
|
237
|
+
# attr_encrypted :email, :key => 'my secret key'
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# User.encrypted_attributes # { :email => { :attribute => 'encrypted_email', :key => 'my secret key' } }
|
241
|
+
def encrypted_attributes
|
242
|
+
@encrypted_attributes ||= superclass.encrypted_attributes.dup
|
243
|
+
end
|
244
|
+
|
245
|
+
# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
|
246
|
+
# if attribute was configured with attr_encrypted
|
247
|
+
#
|
248
|
+
# Example
|
249
|
+
#
|
250
|
+
# class User
|
251
|
+
# attr_encrypted :email, :key => 'my secret key'
|
252
|
+
# end
|
253
|
+
#
|
254
|
+
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
|
255
|
+
def method_missing(method, *arguments, &block)
|
256
|
+
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
|
257
|
+
send($1, $3, *arguments)
|
258
|
+
else
|
259
|
+
super
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
module InstanceMethods
|
264
|
+
# Decrypts a value for the attribute specified using options evaluated in the current object's scope
|
265
|
+
#
|
266
|
+
# Example
|
267
|
+
#
|
268
|
+
# class User
|
269
|
+
# attr_accessor :secret_key
|
270
|
+
# attr_encrypted :email, :key => :secret_key
|
271
|
+
#
|
272
|
+
# def initialize(secret_key)
|
273
|
+
# self.secret_key = secret_key
|
274
|
+
# end
|
275
|
+
# end
|
276
|
+
#
|
277
|
+
# @user = User.new('some-secret-key')
|
278
|
+
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
279
|
+
def decrypt(attribute, encrypted_value)
|
280
|
+
self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
|
281
|
+
end
|
282
|
+
|
283
|
+
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
|
284
|
+
#
|
285
|
+
# Example
|
286
|
+
#
|
287
|
+
# class User
|
288
|
+
# attr_accessor :secret_key
|
289
|
+
# attr_encrypted :email, :key => :secret_key
|
290
|
+
#
|
291
|
+
# def initialize(secret_key)
|
292
|
+
# self.secret_key = secret_key
|
293
|
+
# end
|
294
|
+
# end
|
295
|
+
#
|
296
|
+
# @user = User.new('some-secret-key')
|
297
|
+
# @user.encrypt(:email, 'test@example.com')
|
298
|
+
def encrypt(attribute, value)
|
299
|
+
self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
|
300
|
+
end
|
301
|
+
|
302
|
+
protected
|
303
|
+
|
304
|
+
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
|
305
|
+
def evaluated_attr_encrypted_options_for(attribute)
|
306
|
+
self.class.encrypted_attributes[attribute.to_sym].inject({}) { |hash, (option, value)| hash.merge!(option => evaluate_attr_encrypted_option(value)) }
|
307
|
+
end
|
308
|
+
|
309
|
+
# Evaluates symbol (method reference) or proc (responds to call) options
|
310
|
+
#
|
311
|
+
# If the option is not a symbol or proc then the original option is returned
|
312
|
+
def evaluate_attr_encrypted_option(option)
|
313
|
+
if option.is_a?(Symbol) && respond_to?(option)
|
314
|
+
send(option)
|
315
|
+
elsif option.respond_to?(:call)
|
316
|
+
option.call(self)
|
317
|
+
else
|
318
|
+
option
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
Object.extend AttrEncryptor
|
325
|
+
|
326
|
+
Dir[File.join(File.dirname(__FILE__), 'attr_encryptor', 'adapters', '*.rb')].each { |adapter| require adapter }
|