encrypted_attributes 0.2.0 → 0.3.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.rdoc +4 -0
- data/Rakefile +2 -2
- data/lib/encrypted_attributes/sha_cipher.rb +43 -45
- data/lib/encrypted_attributes.rb +140 -142
- data/test/unit/encrypted_attributes_test.rb +4 -4
- data/test/unit/sha_cipher_test.rb +7 -7
- metadata +3 -3
data/CHANGELOG.rdoc
CHANGED
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
|
|
5
5
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = 'encrypted_attributes'
|
8
|
-
s.version = '0.
|
8
|
+
s.version = '0.3.0'
|
9
9
|
s.platform = Gem::Platform::RUBY
|
10
10
|
s.summary = 'Adds support for automatically encrypting ActiveRecord attributes'
|
11
11
|
|
@@ -13,7 +13,7 @@ spec = Gem::Specification.new do |s|
|
|
13
13
|
s.require_path = 'lib'
|
14
14
|
s.has_rdoc = true
|
15
15
|
s.test_files = Dir['test/**/*_test.rb']
|
16
|
-
s.add_dependency 'encrypted_strings', '>= 0.
|
16
|
+
s.add_dependency 'encrypted_strings', '>= 0.3.0'
|
17
17
|
|
18
18
|
s.author = 'Aaron Pfeifer'
|
19
19
|
s.email = 'aaron@pluginaweek.org'
|
@@ -1,50 +1,48 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# Track whether or not the salt was generated dynamically
|
27
|
-
@dynamic_salt = salt != options[:salt]
|
28
|
-
|
29
|
-
super(options)
|
30
|
-
else
|
31
|
-
# The salt is at the end of the value if it's dynamic
|
32
|
-
salt = value[40..-1]
|
33
|
-
if @dynamic_salt = !salt.blank?
|
34
|
-
options[:salt] = salt
|
35
|
-
end
|
36
|
-
|
37
|
-
super(options)
|
1
|
+
module EncryptedAttributes
|
2
|
+
# Adds support for dynamically generated salts
|
3
|
+
class ShaCipher < EncryptedStrings::ShaCipher
|
4
|
+
# Encrypts a string using a Secure Hash Algorithm (SHA), specifically SHA-1.
|
5
|
+
#
|
6
|
+
# The <tt>:salt</tt> configuration option can be any one of the following types:
|
7
|
+
# * +symbol+ - Calls the method on the object whose value is being encrypted
|
8
|
+
# * +proc+ - A block that will be invoked, providing it with the object whose value is being encrypted
|
9
|
+
# * +string+ - The actual salt value to use
|
10
|
+
def initialize(object, value, operation, options = {}) #:nodoc:
|
11
|
+
if operation == :write
|
12
|
+
# Figure out the actual salt value
|
13
|
+
if salt = options[:salt]
|
14
|
+
options[:salt] =
|
15
|
+
case salt
|
16
|
+
when Symbol
|
17
|
+
object.send(salt)
|
18
|
+
when Proc
|
19
|
+
salt.call(object)
|
20
|
+
else
|
21
|
+
salt
|
22
|
+
end
|
38
23
|
end
|
24
|
+
|
25
|
+
# Track whether or not the salt was generated dynamically
|
26
|
+
@dynamic_salt = salt != options[:salt]
|
27
|
+
|
28
|
+
super(options)
|
29
|
+
else
|
30
|
+
# The salt is at the end of the value if it's dynamic
|
31
|
+
salt = value[40..-1]
|
32
|
+
if @dynamic_salt = !salt.blank?
|
33
|
+
options[:salt] = salt
|
34
|
+
end
|
35
|
+
|
36
|
+
super(options)
|
39
37
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
38
|
+
end
|
39
|
+
|
40
|
+
# Encrypts the data, appending the salt to the end of the string if it
|
41
|
+
# was created dynamically
|
42
|
+
def encrypt(data)
|
43
|
+
encrypted_data = super
|
44
|
+
encrypted_data << salt if @dynamic_salt
|
45
|
+
encrypted_data
|
48
46
|
end
|
49
47
|
end
|
50
48
|
end
|
data/lib/encrypted_attributes.rb
CHANGED
@@ -1,158 +1,156 @@
|
|
1
1
|
require 'encrypted_strings'
|
2
2
|
require 'encrypted_attributes/sha_cipher'
|
3
3
|
|
4
|
-
module
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
include PluginAWeek::EncryptedAttributes::InstanceMethods
|
96
|
-
end
|
4
|
+
module EncryptedAttributes
|
5
|
+
module MacroMethods
|
6
|
+
# Encrypts the specified attribute.
|
7
|
+
#
|
8
|
+
# Configuration options:
|
9
|
+
# * +mode+ - The mode of encryption to use. Default is sha. See EncryptedStrings for other possible modes.
|
10
|
+
# * +to+ - The attribute to write the encrypted value to. Default is the same attribute being encrypted.
|
11
|
+
# * +if+ - Specifies a method, proc or string to call to determine if the encryption should occur. The method, proc or string should return or evaluate to a true or false value.
|
12
|
+
# * +unless+ - Specifies a method, proc or string to call to determine if the encryption should not occur. The method, proc or string should return or evaluate to a true or false value.
|
13
|
+
#
|
14
|
+
# For additional configuration options used during the actual encryption,
|
15
|
+
# see the individual cipher class for the specified mode.
|
16
|
+
#
|
17
|
+
# == Encryption timeline
|
18
|
+
#
|
19
|
+
# Attributes are encrypted immediately before a record is validated.
|
20
|
+
# This means that you can still validate the presence of the encrypted
|
21
|
+
# attribute, but other things like password length cannot be validated
|
22
|
+
# without either (a) decrypting the value first or (b) using a different
|
23
|
+
# encryption target. For example,
|
24
|
+
#
|
25
|
+
# class User < ActiveRecord::Base
|
26
|
+
# encrypts :password, :to => :crypted_password
|
27
|
+
#
|
28
|
+
# validates_presence_of :password, :crypted_password
|
29
|
+
# validates_length_of :password, :maximum => 16
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# In the above example, the actual encrypted password will be stored in
|
33
|
+
# the +crypted_password+ attribute. This means that validations can
|
34
|
+
# still run against the model for the original password value.
|
35
|
+
#
|
36
|
+
# user = User.new(:password => 'secret')
|
37
|
+
# user.password # => "secret"
|
38
|
+
# user.crypted_password # => nil
|
39
|
+
# user.valid? # => true
|
40
|
+
# user.crypted_password # => "8152bc582f58c854f580cb101d3182813dec4afe"
|
41
|
+
#
|
42
|
+
# user = User.new(:password => 'longer_than_the_maximum_allowed')
|
43
|
+
# user.valid? # => false
|
44
|
+
# user.crypted_password # => "e80a709f25798f87d9ca8005a7f64a645964d7c2"
|
45
|
+
# user.errors[:password] # => "is too long (maximum is 16 characters)"
|
46
|
+
#
|
47
|
+
# == Encryption mode examples
|
48
|
+
#
|
49
|
+
# SHA encryption:
|
50
|
+
#
|
51
|
+
# class User < ActiveRecord::Base
|
52
|
+
# encrypts :password
|
53
|
+
# # encrypts :password, :salt => :create_salt
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Symmetric encryption:
|
57
|
+
#
|
58
|
+
# class User < ActiveRecord::Base
|
59
|
+
# encrypts :password, :mode => :symmetric
|
60
|
+
# # encrypts :password, :mode => :symmetric, :key => 'custom'
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# Asymmetric encryption:
|
64
|
+
#
|
65
|
+
# class User < ActiveRecord::Base
|
66
|
+
# encrypts :password, :mode => :asymmetric
|
67
|
+
# # encrypts :password, :mode => :asymmetric, :public_key_file => '/keys/public', :private_key_file => '/keys/private'
|
68
|
+
# end
|
69
|
+
def encrypts(attr_name, options = {})
|
70
|
+
attr_name = attr_name.to_s
|
71
|
+
to_attr_name = options.delete(:to) || attr_name
|
72
|
+
|
73
|
+
# Figure out what cipher is being configured for the attribute
|
74
|
+
mode = options.delete(:mode) || :sha
|
75
|
+
class_name = "#{mode.to_s.classify}Cipher"
|
76
|
+
if EncryptedAttributes.const_defined?(class_name)
|
77
|
+
cipher_class = EncryptedAttributes.const_get(class_name)
|
78
|
+
else
|
79
|
+
cipher_class = EncryptedStrings.const_get(class_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Set the encrypted value right before validation takes place
|
83
|
+
before_validation(:if => options.delete(:if), :unless => options.delete(:unless)) do |record|
|
84
|
+
record.send(:write_encrypted_attribute, attr_name, to_attr_name, cipher_class, options)
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Define the reader when reading the encrypted attribute from the database
|
89
|
+
define_method(to_attr_name) do
|
90
|
+
read_encrypted_attribute(to_attr_name, cipher_class, options)
|
91
|
+
end
|
92
|
+
|
93
|
+
unless included_modules.include?(EncryptedAttributes::InstanceMethods)
|
94
|
+
include EncryptedAttributes::InstanceMethods
|
97
95
|
end
|
98
96
|
end
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
# Only encrypt values that actually have content and have not already
|
108
|
-
# been encrypted
|
109
|
-
unless value.blank? || value.encrypted?
|
110
|
-
# Create the cipher configured for this attribute
|
111
|
-
cipher = create_cipher(cipher_class, options, :write, value)
|
112
|
-
|
113
|
-
# Encrypt the value
|
114
|
-
value = cipher.encrypt(value)
|
115
|
-
value.cipher = cipher
|
116
|
-
|
117
|
-
# Update the value based on the target attribute
|
118
|
-
send("#{to_attr_name}=", value)
|
119
|
-
end
|
120
|
-
end
|
97
|
+
end
|
98
|
+
|
99
|
+
module InstanceMethods #:nodoc:
|
100
|
+
private
|
101
|
+
# Encrypts the given attribute to a target location using the encryption
|
102
|
+
# options configured for that attribute
|
103
|
+
def write_encrypted_attribute(attr_name, to_attr_name, cipher_class, options)
|
104
|
+
value = send(attr_name)
|
121
105
|
|
122
|
-
#
|
123
|
-
#
|
124
|
-
|
125
|
-
|
126
|
-
|
106
|
+
# Only encrypt values that actually have content and have not already
|
107
|
+
# been encrypted
|
108
|
+
unless value.blank? || value.encrypted?
|
109
|
+
# Create the cipher configured for this attribute
|
110
|
+
cipher = create_cipher(cipher_class, options, :write, value)
|
127
111
|
|
128
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
# the database. The dirty checking is important when the encypted value
|
132
|
-
# is written to the same attribute as the unencrypted value (i.e. you
|
133
|
-
# don't want to encrypt when a new value has been set)
|
134
|
-
unless value.blank? || value.encrypted? || attribute_changed?(to_attr_name)
|
135
|
-
# Create the cipher configured for this attribute
|
136
|
-
value.cipher = create_cipher(cipher_class, options, :read, value)
|
137
|
-
end
|
112
|
+
# Encrypt the value
|
113
|
+
value = cipher.encrypt(value)
|
114
|
+
value.cipher = cipher
|
138
115
|
|
139
|
-
value
|
116
|
+
# Update the value based on the target attribute
|
117
|
+
send("#{to_attr_name}=", value)
|
140
118
|
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Reads the given attribute from the database, adding contextual
|
122
|
+
# information about how it was encrypted so that equality comparisons
|
123
|
+
# can be used
|
124
|
+
def read_encrypted_attribute(to_attr_name, cipher_class, options)
|
125
|
+
value = read_attribute(to_attr_name)
|
141
126
|
|
142
|
-
#
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
127
|
+
# Make sure we set the cipher for equality comparison when reading
|
128
|
+
# from the database. This should only be done if the value is *not*
|
129
|
+
# blank, is *not* encrypted, and hasn't changed since it was read from
|
130
|
+
# the database. The dirty checking is important when the encypted value
|
131
|
+
# is written to the same attribute as the unencrypted value (i.e. you
|
132
|
+
# don't want to encrypt when a new value has been set)
|
133
|
+
unless value.blank? || value.encrypted? || attribute_changed?(to_attr_name)
|
134
|
+
# Create the cipher configured for this attribute
|
135
|
+
value.cipher = create_cipher(cipher_class, options, :read, value)
|
151
136
|
end
|
152
|
-
|
137
|
+
|
138
|
+
value
|
139
|
+
end
|
140
|
+
|
141
|
+
# Creates a new cipher with the given configuration options. The
|
142
|
+
# operator defines the context in which the cipher will be used.
|
143
|
+
def create_cipher(klass, options, operator, value)
|
144
|
+
if klass.parent == EncryptedAttributes
|
145
|
+
# Only use the contextual information for ciphers defined in this plugin
|
146
|
+
klass.new(self, value, operator, options.dup)
|
147
|
+
else
|
148
|
+
klass.new(options.dup)
|
149
|
+
end
|
150
|
+
end
|
153
151
|
end
|
154
152
|
end
|
155
153
|
|
156
154
|
ActiveRecord::Base.class_eval do
|
157
|
-
extend
|
155
|
+
extend EncryptedAttributes::MacroMethods
|
158
156
|
end
|
@@ -160,7 +160,7 @@ class ShaEncryptionTest < Test::Unit::TestCase
|
|
160
160
|
end
|
161
161
|
|
162
162
|
def test_should_use_sha_cipher
|
163
|
-
assert_instance_of
|
163
|
+
assert_instance_of EncryptedAttributes::ShaCipher, @user.password.cipher
|
164
164
|
end
|
165
165
|
|
166
166
|
def test_should_use_default_salt
|
@@ -193,7 +193,7 @@ class ShaWithCustomSaltEncryptionTest < Test::Unit::TestCase
|
|
193
193
|
end
|
194
194
|
|
195
195
|
def test_should_use_sha_cipher
|
196
|
-
assert_instance_of
|
196
|
+
assert_instance_of EncryptedAttributes::ShaCipher, @user.password.cipher
|
197
197
|
end
|
198
198
|
|
199
199
|
def test_should_use_custom_salt
|
@@ -226,7 +226,7 @@ class SymmetricEncryptionTest < Test::Unit::TestCase
|
|
226
226
|
end
|
227
227
|
|
228
228
|
def test_should_use_sha_cipher
|
229
|
-
assert_instance_of
|
229
|
+
assert_instance_of EncryptedStrings::SymmetricCipher, @user.password.cipher
|
230
230
|
end
|
231
231
|
|
232
232
|
def test_should_use_custom_password
|
@@ -262,7 +262,7 @@ class AsymmetricEncryptionTest < Test::Unit::TestCase
|
|
262
262
|
end
|
263
263
|
|
264
264
|
def test_should_use_sha_cipher
|
265
|
-
assert_instance_of
|
265
|
+
assert_instance_of EncryptedStrings::AsymmetricCipher, @user.password.cipher
|
266
266
|
end
|
267
267
|
|
268
268
|
def test_should_be_able_to_check_password
|
@@ -6,34 +6,34 @@ class ShaCipherOnWriteTest < Test::Unit::TestCase
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def test_should_allow_symbolic_salt
|
9
|
-
cipher =
|
9
|
+
cipher = EncryptedAttributes::ShaCipher.new(@user, 'password', :write, :salt => :login)
|
10
10
|
assert_equal 'admin', cipher.salt
|
11
11
|
end
|
12
12
|
|
13
13
|
def test_should_allow_stringified_salt
|
14
|
-
cipher =
|
14
|
+
cipher = EncryptedAttributes::ShaCipher.new(@user, 'password', :write, :salt => 'custom_salt')
|
15
15
|
assert_equal 'custom_salt', cipher.salt
|
16
16
|
end
|
17
17
|
|
18
18
|
def test_should_allow_block_salt
|
19
19
|
dynamic_salt = lambda {|user| user.login}
|
20
|
-
cipher =
|
20
|
+
cipher = EncryptedAttributes::ShaCipher.new(@user, 'password', :write, :salt => dynamic_salt)
|
21
21
|
assert_equal 'admin', cipher.salt
|
22
22
|
end
|
23
23
|
|
24
24
|
def test_should_allow_dynamic_nil_salt
|
25
25
|
dynamic_salt = lambda {|user| nil}
|
26
|
-
cipher =
|
26
|
+
cipher = EncryptedAttributes::ShaCipher.new(@user, 'password', :write, :salt => dynamic_salt)
|
27
27
|
assert_equal '', cipher.salt
|
28
28
|
end
|
29
29
|
|
30
30
|
def test_should_append_salt_to_encrypted_value_if_dynamic
|
31
|
-
cipher =
|
31
|
+
cipher = EncryptedAttributes::ShaCipher.new(@user, 'password', :write, :salt => :login)
|
32
32
|
assert_equal 'a55d037f385cad22efe7862e07b805938d150154admin', cipher.encrypt('secret')
|
33
33
|
end
|
34
34
|
|
35
35
|
def test_should_not_append_salt_to_encrypted_value_if_static
|
36
|
-
cipher =
|
36
|
+
cipher = EncryptedAttributes::ShaCipher.new(@user, 'password', :write, :salt => 'custom_salt')
|
37
37
|
assert_equal 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0e', cipher.encrypt('secret')
|
38
38
|
end
|
39
39
|
end
|
@@ -41,7 +41,7 @@ end
|
|
41
41
|
class ShaCipherOnReadTest < Test::Unit::TestCase
|
42
42
|
def setup
|
43
43
|
@user = create_user(:login => 'admin')
|
44
|
-
@cipher =
|
44
|
+
@cipher = EncryptedAttributes::ShaCipher.new(@user, 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt', :read)
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_should_should_use_remaining_characters_after_password_for_salt
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: encrypted_attributes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Pfeifer
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-12-
|
12
|
+
date: 2008-12-14 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 0.
|
23
|
+
version: 0.3.0
|
24
24
|
version:
|
25
25
|
description:
|
26
26
|
email: aaron@pluginaweek.org
|