forked from reidmorrison/symmetric-encryption
/
base.rb
145 lines (131 loc) · 5.51 KB
/
base.rb
1
2
3
4
5
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
module ActiveRecord #:nodoc:
class Base
class << self # Class methods
# Much lighter weight encryption for Rails attributes matching the
# attr_encrypted interface using SymmetricEncryption
#
# The regular attr_encrypted gem uses Encryptor that adds encryption to
# every Ruby object which is a complete overkill for this simple use-case
#
# Params:
# * symbolic names of each method to create which has a corresponding
# method already defined in rails starting with: encrypted_
# * Followed by an option hash:
# :marshal => Whether this element should be converted to YAML before encryption
# true or false
# Default: false
#
def attr_encrypted(*params)
# Ensure ActiveRecord has created all its methods first
# Ignore failures since the table may not yet actually exist
define_attribute_methods rescue nil
options = params.last.is_a?(Hash) ? params.pop : {}
params.each do |attribute|
# Generate unencrypted attribute with getter and setter
class_eval(<<-UNENCRYPTED, __FILE__, __LINE__ + 1)
# Returns the decrypted value for the encrypted attribute
# The decrypted value is cached and is only decrypted if the encrypted value has changed
# If this method is not called, then the encrypted value is never decrypted
def #{attribute}
if @stored_encrypted_#{attribute} != self.encrypted_#{attribute}
@#{attribute} = ::SymmetricEncryption.decrypt(self.encrypted_#{attribute})
@stored_encrypted_#{attribute} = self.encrypted_#{attribute}
end
@#{attribute}
end
# Set the un-encrypted attribute
# Also updates the encrypted field with the encrypted value
def #{attribute}=(value)
self.encrypted_#{attribute} = @stored_encrypted_#{attribute} = ::SymmetricEncryption.encrypt(value#{".to_yaml" if options[:marshal]})
@#{attribute} = value
end
UNENCRYPTED
encrypted_attributes[attribute.to_sym] = "encrypted_#{attribute}".to_sym
end
end
# Contains a hash of encrypted attributes with virtual attribute names as keys and real attribute
# names as values
#
# Example
#
# class User < ActiveRecord::Base
# attr_encrypted :email
# end
#
# User.encrypted_attributes # { :email => :encrypted_email }
def encrypted_attributes
@encrypted_attributes ||= superclass.respond_to?(:encrypted_attributes) ? superclass.encrypted_attributes.dup : {}
end
# Return the name of all encrypted virtual attributes as an Array of symbols
# Example: [:email, :password]
def encrypted_keys
@encrypted_keys ||= encrypted_attributes.keys
end
# Return the name of all encrypted columns as an Array of symbols
# Example: [:encrypted_email, :encrypted_password]
def encrypted_columns
@encrypted_columns ||= encrypted_attributes.values
end
# Returns whether an attribute has been configured to be encrypted
#
# Example
#
# class User < ActiveRecord::Base
# attr_accessor :name
# attr_encrypted :email
# end
#
# User.encrypted_attribute?(:name) # false
# User.encrypted_attribute?(:email) # true
def encrypted_attribute?(attribute)
encrypted_keys.include?(attribute)
end
# Returns whether the attribute is the database column to hold the
# encrypted data for a matching encrypted attribute
#
# Example
#
# class User < ActiveRecord::Base
# attr_accessor :name
# attr_encrypted :email
# end
#
# User.encrypted_column?(:encrypted_name) # false
# User.encrypted_column?(:encrypted_email) # true
def encrypted_column?(attribute)
encrypted_columns.include?(attribute)
end
protected
# Allows you to use dynamic methods like <tt>find_by_email</tt> or <tt>scoped_by_email</tt> for
# encrypted attributes
#
# This is useful for encrypting fields like email addresses. Your user's email addresses
# are encrypted in the database, but you can still look up a user by email for logging in
#
# Example
#
# class User < ActiveRecord::Base
# attr_encrypted :email
# end
#
# User.find_by_email_and_password('test@example.com', 'testing')
# # results in a call to
# User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing')
def method_missing_with_attr_encrypted(method, *args, &block)
if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
attribute_names = match.captures.last.split('_and_')
attribute_names.each_with_index do |attribute, index|
encrypted_name = "encrypted_#{attribute}"
if method_defined? encrypted_name.to_sym
args[index] = ::SymmetricEncryption.encrypt(args[index])
attribute_names[index] = encrypted_name
end
end
method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
end
method_missing_without_attr_encrypted(method, *args, &block)
end
alias_method_chain :method_missing, :attr_encrypted
end
end
end