powerhome-attr_encrypted 1.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +67 -0
- data/CHANGELOG.md +98 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +465 -0
- data/Rakefile +25 -0
- data/attr_encrypted.gemspec +63 -0
- data/certs/saghaulor.pem +21 -0
- data/checksum/attr_encrypted-3.0.0.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.0.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.0.1.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.1.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.0.2.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.2.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.0.3.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.3.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.1.0.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.1.0.gem.sha512 +1 -0
- data/lib/attr_encrypted.rb +473 -0
- data/lib/attr_encrypted/adapters/active_record.rb +157 -0
- data/lib/attr_encrypted/adapters/data_mapper.rb +24 -0
- data/lib/attr_encrypted/adapters/sequel.rb +16 -0
- data/lib/attr_encrypted/version.rb +19 -0
- data/test/active_record_test.rb +365 -0
- data/test/attr_encrypted_test.rb +490 -0
- data/test/compatibility_test.rb +109 -0
- data/test/data_mapper_test.rb +59 -0
- data/test/legacy_active_record_test.rb +120 -0
- data/test/legacy_attr_encrypted_test.rb +300 -0
- data/test/legacy_compatibility_test.rb +95 -0
- data/test/legacy_data_mapper_test.rb +57 -0
- data/test/legacy_sequel_test.rb +54 -0
- data/test/run.sh +12 -0
- data/test/sequel_test.rb +55 -0
- data/test/test_helper.rb +61 -0
- metadata +294 -0
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rdoc/task'
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
|
7
|
+
desc 'Test the attr_encrypted gem.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.pattern = 'test/**/*_test.rb'
|
11
|
+
t.warning = false
|
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*')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Default: run unit tests.'
|
25
|
+
task :default => :test
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib/', __FILE__)
|
4
|
+
$:.unshift lib unless $:.include?(lib)
|
5
|
+
|
6
|
+
require 'attr_encrypted/version'
|
7
|
+
require 'date'
|
8
|
+
|
9
|
+
Gem::Specification.new do |s|
|
10
|
+
s.name = 'powerhome-attr_encrypted'
|
11
|
+
s.version = AttrEncrypted::Version.string
|
12
|
+
s.date = Date.today
|
13
|
+
|
14
|
+
s.summary = "Power's version of the attr_encrypted gem"
|
15
|
+
s.description = 'Generates attr_accessors that encrypt and decrypt attributes transparently'
|
16
|
+
|
17
|
+
s.authors = ['Wade Winningham', 'Ben Langfeld']
|
18
|
+
s.email = ['wwinningham@powerhrg.com', 'ben@langfeld.me']
|
19
|
+
s.homepage = 'https://github.com/powerhome/attr_encrypted'
|
20
|
+
s.license = 'MIT'
|
21
|
+
|
22
|
+
s.require_paths = ['lib']
|
23
|
+
|
24
|
+
s.files = `git ls-files`.split("\n")
|
25
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
26
|
+
|
27
|
+
s.required_ruby_version = '>= 2.0.0'
|
28
|
+
|
29
|
+
s.add_dependency('encryptor', ['~> 3.0.0'])
|
30
|
+
# support for testing with specific active record version
|
31
|
+
activerecord_version = if ENV.key?('ACTIVERECORD')
|
32
|
+
"~> #{ENV['ACTIVERECORD']}"
|
33
|
+
else
|
34
|
+
'>= 2.0.0'
|
35
|
+
end
|
36
|
+
s.add_development_dependency('activerecord', activerecord_version)
|
37
|
+
s.add_development_dependency('actionpack', activerecord_version)
|
38
|
+
s.add_development_dependency('datamapper')
|
39
|
+
s.add_development_dependency('rake')
|
40
|
+
s.add_development_dependency('minitest')
|
41
|
+
s.add_development_dependency('sequel')
|
42
|
+
s.add_development_dependency('pry-byebug')
|
43
|
+
if RUBY_VERSION < '2.1.0'
|
44
|
+
s.add_development_dependency('nokogiri', '< 1.7.0')
|
45
|
+
s.add_development_dependency('public_suffix', '< 3.0.0')
|
46
|
+
end
|
47
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby
|
48
|
+
s.add_development_dependency('activerecord-jdbcsqlite3-adapter')
|
49
|
+
s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke
|
50
|
+
else
|
51
|
+
s.add_development_dependency('sqlite3', '~> 1.3.0', '>= 1.3.6')
|
52
|
+
end
|
53
|
+
s.add_development_dependency('dm-sqlite-adapter')
|
54
|
+
s.add_development_dependency('simplecov')
|
55
|
+
s.add_development_dependency('simplecov-rcov')
|
56
|
+
s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0')
|
57
|
+
|
58
|
+
s.post_install_message = "\n\n\nWARNING: Several insecure default options and features were deprecated in attr_encrypted v2.0.0.\n
|
59
|
+
Additionally, there was a bug in Encryptor v2.0.0 that insecurely encrypted data when using an AES-*-GCM algorithm.\n
|
60
|
+
This bug was fixed but introduced breaking changes between v2.x and v3.x.\n
|
61
|
+
Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n"
|
62
|
+
|
63
|
+
end
|
data/certs/saghaulor.pem
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDdDCCAlygAwIBAgIBATANBgkqhkiG9w0BAQUFADBAMRIwEAYDVQQDDAlzYWdo
|
3
|
+
YXVsb3IxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
|
4
|
+
bTAeFw0xODAyMTIwMzMzMThaFw0xOTAyMTIwMzMzMThaMEAxEjAQBgNVBAMMCXNh
|
5
|
+
Z2hhdWxvcjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD
|
6
|
+
Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOLqbSmj5txfw39a
|
7
|
+
Ki0g3BJWGrfGBiSRq9aThUGzoiaqyDo/m1WMQdgPioZG+P923okChEWFjhSymBQU
|
8
|
+
eMdys6JRPm5ortp5sh9gesOWoozqb8R55d8rr1V7pY533cCut53Kb1wiitjkfXjR
|
9
|
+
efT2HPh6nV6rYjGMJek/URaCNzsZo7HCkRsKdezP+BKr4V4wOka69tfJX5pcvFvR
|
10
|
+
iiqfaiP4RK12hYdsFnSVKiKP7SAFTFiYcohbL8TUW6ezQQqJCK0M6fu74EWVCnBS
|
11
|
+
gFVjj931BuD8qhuxMiB6uC6FKxemB5TRGBLzn7RcrOMAo2inMAopjkGeQJUAyVCm
|
12
|
+
J5US3wIDAQABo3kwdzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
|
13
|
+
hJEuSZgvuuIhIsxQ/0pRQTBVzokwHgYDVR0RBBcwFYETc2FnaGF1bG9yQGdtYWls
|
14
|
+
LmNvbTAeBgNVHRIEFzAVgRNzYWdoYXVsb3JAZ21haWwuY29tMA0GCSqGSIb3DQEB
|
15
|
+
BQUAA4IBAQCsBS2cxqTmV4nXJEH/QbdgjVDAZbK6xf2gpM3vCRlYsy7Wz6GEoOpD
|
16
|
+
bzRkjxZwGNbhXShMUZwm6zahYQ/L1/HFztLoMBMkm8EIfPxH0PDrP4aWl0oyWxmU
|
17
|
+
OLm0/t9icSWRPPJ1tLJvuAaDdVpY5dEHd6VdnNJGQC5vHKRInt1kEyqEttIJ/xmJ
|
18
|
+
leSEFyMeoFsR+C/WPG9WSC+xN0eXqakCu6YUJoQzCn/7znv8WxpHEbeZjNIHq0qb
|
19
|
+
nbqZ/ZW1bwzj1T/NIbnMr37wqV29XwkI4+LbewMkb6/bDPYl0qZpAkCxKtGYCCJp
|
20
|
+
l6KPs9K/72yH00dxuAhiTXikTkcLXeQJ
|
21
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1 @@
|
|
1
|
+
845fc3cb09a19c3ac76192aba443788f92c880744617bca99b16fd31ce843e07
|
@@ -0,0 +1 @@
|
|
1
|
+
81a065442258cc3702aab62c7b2307a48ed3e0deb803600d11a7480cce0db7c43fd9929acd2755081042f8989236553fd694b6cb62776bbfc53f9165a22cbca1
|
@@ -0,0 +1 @@
|
|
1
|
+
33140af4b223177db7a19efb2fa38472a299a745b29ca1c5ba9d3fa947390b77
|
@@ -0,0 +1 @@
|
|
1
|
+
0c467cab98b9b2eb331f9818323a90ae01392d6cb03cf1f32faccc954d0fc54be65f0fc7bf751b0fce57925eef1c9e2af90181bc40d81ad93e21d15a001c53c6
|
@@ -0,0 +1 @@
|
|
1
|
+
c1256b459336d4a2012a0d0c70ce5cd3dac46acb5e78da6f77f6f104cb1e8b7b
|
@@ -0,0 +1 @@
|
|
1
|
+
dca0c8a729974c0e26fde4cd4216c7d0f66d9eca9f6cf0ccca64999f5180a00bf7c05b630c1d420ec1673141a2923946e8bd28b12e711faf64a4cd42c7a3ac9e
|
@@ -0,0 +1 @@
|
|
1
|
+
6d84c64852c4bbc0926b92fe7a93295671a9e69cb2939b96fb1e4b5e8a5b33b6
|
@@ -0,0 +1 @@
|
|
1
|
+
0f960e8a2f63c747c273241f7395dcceb0dd8a6f79349bee453db741fc7ea5ceb4342d7d5908e540e3b5acea2216ff38bef8c743e6e7c8559bacb4a731ab27c4
|
@@ -0,0 +1 @@
|
|
1
|
+
4f0682604714ed4599cf00771ad27e82f0b51b0ed8644af51a43d21fbe129b59
|
@@ -0,0 +1 @@
|
|
1
|
+
dc757a50c53175dc05adbfccb25b536f7f1a060c7bd626bfc72ff577479c6fe28d3b65747033276bd4bce3dad741b67235f3e01ea61409f6c67959da65df445b
|
@@ -0,0 +1,473 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'encryptor'
|
4
|
+
|
5
|
+
# Adds attr_accessors that encrypt and decrypt an object's attributes
|
6
|
+
module AttrEncrypted
|
7
|
+
autoload :Version, 'attr_encrypted/version'
|
8
|
+
|
9
|
+
def self.extended(base) # :nodoc:
|
10
|
+
base.class_eval do
|
11
|
+
include InstanceMethods
|
12
|
+
attr_writer :attr_encrypted_options
|
13
|
+
@attr_encrypted_options, @encrypted_attributes = {}, {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generates attr_accessors that encrypt and decrypt attributes transparently
|
18
|
+
#
|
19
|
+
# Options (any other options you specify are passed to the Encryptor's encrypt and decrypt methods)
|
20
|
+
#
|
21
|
+
# attribute: The name of the referenced encrypted attribute. For example
|
22
|
+
# <tt>attr_accessor :email, attribute: :ee</tt> would generate an
|
23
|
+
# attribute named 'ee' to store the encrypted email. This is useful when defining
|
24
|
+
# one attribute to encrypt at a time or when the :prefix and :suffix options
|
25
|
+
# aren't enough.
|
26
|
+
# Defaults to nil.
|
27
|
+
#
|
28
|
+
# prefix: A prefix used to generate the name of the referenced encrypted attributes.
|
29
|
+
# For example <tt>attr_accessor :email, prefix: 'crypted_'</tt> would
|
30
|
+
# generate attributes named 'crypted_email' to store the encrypted
|
31
|
+
# email and password.
|
32
|
+
# Defaults to 'encrypted_'.
|
33
|
+
#
|
34
|
+
# suffix: A suffix used to generate the name of the referenced encrypted attributes.
|
35
|
+
# For example <tt>attr_accessor :email, prefix: '', suffix: '_encrypted'</tt>
|
36
|
+
# would generate attributes named 'email_encrypted' to store the
|
37
|
+
# encrypted email.
|
38
|
+
# Defaults to ''.
|
39
|
+
#
|
40
|
+
# key: The encryption key. This option may not be required if
|
41
|
+
# you're using a custom encryptor. If you pass a symbol
|
42
|
+
# representing an instance method then the :key option
|
43
|
+
# will be replaced with the result of the method before
|
44
|
+
# being passed to the encryptor. Objects that respond
|
45
|
+
# to :call are evaluated as well (including procs).
|
46
|
+
# Any other key types will be passed directly to the encryptor.
|
47
|
+
# Defaults to nil.
|
48
|
+
#
|
49
|
+
# encode: If set to true, attributes will be encoded as well as
|
50
|
+
# encrypted. This is useful if you're planning on storing
|
51
|
+
# the encrypted attributes in a database. The default
|
52
|
+
# encoding is 'm' (base64), however this can be overwritten
|
53
|
+
# by setting the :encode option to some other encoding
|
54
|
+
# string instead of just 'true'. See
|
55
|
+
# http://www.ruby-doc.org/core/classes/Array.html#M002245
|
56
|
+
# for more encoding directives.
|
57
|
+
# Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
|
58
|
+
#
|
59
|
+
# encode_iv: Defaults to true.
|
60
|
+
|
61
|
+
# encode_salt: Defaults to true.
|
62
|
+
#
|
63
|
+
# default_encoding: Defaults to 'm' (base64).
|
64
|
+
#
|
65
|
+
# marshal: If set to true, attributes will be marshaled as well
|
66
|
+
# as encrypted. This is useful if you're planning on
|
67
|
+
# encrypting something other than a string.
|
68
|
+
# Defaults to false.
|
69
|
+
#
|
70
|
+
# marshaler: The object to use for marshaling.
|
71
|
+
# Defaults to Marshal.
|
72
|
+
#
|
73
|
+
# dump_method: The dump method name to call on the <tt>:marshaler</tt> object to.
|
74
|
+
# Defaults to 'dump'.
|
75
|
+
#
|
76
|
+
# load_method: The load method name to call on the <tt>:marshaler</tt> object.
|
77
|
+
# Defaults to 'load'.
|
78
|
+
#
|
79
|
+
# encryptor: The object to use for encrypting.
|
80
|
+
# Defaults to Encryptor.
|
81
|
+
#
|
82
|
+
# encrypt_method: The encrypt method name to call on the <tt>:encryptor</tt> object.
|
83
|
+
# Defaults to 'encrypt'.
|
84
|
+
#
|
85
|
+
# decrypt_method: The decrypt method name to call on the <tt>:encryptor</tt> object.
|
86
|
+
# Defaults to 'decrypt'.
|
87
|
+
#
|
88
|
+
# if: Attributes are only encrypted if this option evaluates
|
89
|
+
# to true. If you pass a symbol representing an instance
|
90
|
+
# method then the result of the method will be evaluated.
|
91
|
+
# Any objects that respond to <tt>:call</tt> are evaluated as well.
|
92
|
+
# Defaults to true.
|
93
|
+
#
|
94
|
+
# unless: Attributes are only encrypted if this option evaluates
|
95
|
+
# to false. If you pass a symbol representing an instance
|
96
|
+
# method then the result of the method will be evaluated.
|
97
|
+
# Any objects that respond to <tt>:call</tt> are evaluated as well.
|
98
|
+
# Defaults to false.
|
99
|
+
#
|
100
|
+
# mode: Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
|
101
|
+
# with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used.
|
102
|
+
# The <tt>:per_attribute_iv_and_salt</tt> mode uses a per-attribute IV and salt. The salt is used to derive a unique key per attribute.
|
103
|
+
# A <tt>:per_attribute_iv</default> mode derives a unique IV per attribute; salt is not used.
|
104
|
+
# Defaults to <tt>:per_attribute_iv</tt>.
|
105
|
+
#
|
106
|
+
# allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option
|
107
|
+
# has a truthy value.
|
108
|
+
#
|
109
|
+
# You can specify your own default options
|
110
|
+
#
|
111
|
+
# class User
|
112
|
+
# # Now all attributes will be encoded and marshaled by default
|
113
|
+
# attr_encrypted_options.merge!(encode: true, marshal: true, some_other_option: true)
|
114
|
+
# attr_encrypted :configuration, key: 'my secret key'
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
#
|
118
|
+
# Example
|
119
|
+
#
|
120
|
+
# class User
|
121
|
+
# attr_encrypted :email, key: 'some secret key'
|
122
|
+
# attr_encrypted :configuration, key: 'some other secret key', marshal: true
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# @user = User.new
|
126
|
+
# @user.encrypted_email # nil
|
127
|
+
# @user.email? # false
|
128
|
+
# @user.email = 'test@example.com'
|
129
|
+
# @user.email? # true
|
130
|
+
# @user.encrypted_email # returns the encrypted version of 'test@example.com'
|
131
|
+
#
|
132
|
+
# @user.configuration = { time_zone: 'UTC' }
|
133
|
+
# @user.encrypted_configuration # returns the encrypted version of configuration
|
134
|
+
#
|
135
|
+
# See README for more examples
|
136
|
+
def attr_encrypted(*attributes)
|
137
|
+
options = attributes.last.is_a?(Hash) ? attributes.pop : {}
|
138
|
+
options = attr_encrypted_default_options.dup.merge!(attr_encrypted_options).merge!(options)
|
139
|
+
|
140
|
+
options[:encode] = options[:default_encoding] if options[:encode] == true
|
141
|
+
options[:encode_iv] = options[:default_encoding] if options[:encode_iv] == true
|
142
|
+
options[:encode_salt] = options[:default_encoding] if options[:encode_salt] == true
|
143
|
+
|
144
|
+
attributes.each do |attribute|
|
145
|
+
encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
|
146
|
+
|
147
|
+
instance_methods_as_symbols = attribute_instance_methods_as_symbols
|
148
|
+
|
149
|
+
if attribute_instance_methods_as_symbols_available?
|
150
|
+
attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
|
151
|
+
attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
|
152
|
+
|
153
|
+
iv_name = "#{encrypted_attribute_name}_iv".to_sym
|
154
|
+
attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
|
155
|
+
attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
|
156
|
+
|
157
|
+
salt_name = "#{encrypted_attribute_name}_salt".to_sym
|
158
|
+
attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
|
159
|
+
attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
|
160
|
+
end
|
161
|
+
|
162
|
+
define_method(attribute) do
|
163
|
+
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
|
164
|
+
end
|
165
|
+
|
166
|
+
define_method("#{attribute}=") do |value|
|
167
|
+
send("#{encrypted_attribute_name}=", encrypt(attribute, value))
|
168
|
+
instance_variable_set("@#{attribute}", value)
|
169
|
+
end
|
170
|
+
|
171
|
+
define_method("#{attribute}?") do
|
172
|
+
value = send(attribute)
|
173
|
+
value.respond_to?(:empty?) ? !value.empty? : !!value
|
174
|
+
end
|
175
|
+
|
176
|
+
encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
alias_method :attr_encryptor, :attr_encrypted
|
181
|
+
|
182
|
+
# Default options to use with calls to <tt>attr_encrypted</tt>
|
183
|
+
#
|
184
|
+
# It will inherit existing options from its superclass
|
185
|
+
def attr_encrypted_options
|
186
|
+
@attr_encrypted_options ||= superclass.attr_encrypted_options.dup
|
187
|
+
end
|
188
|
+
|
189
|
+
def attr_encrypted_default_options
|
190
|
+
{
|
191
|
+
prefix: 'encrypted_',
|
192
|
+
suffix: '',
|
193
|
+
if: true,
|
194
|
+
unless: false,
|
195
|
+
encode: false,
|
196
|
+
encode_iv: true,
|
197
|
+
encode_salt: true,
|
198
|
+
default_encoding: 'm',
|
199
|
+
marshal: false,
|
200
|
+
marshaler: Marshal,
|
201
|
+
dump_method: 'dump',
|
202
|
+
load_method: 'load',
|
203
|
+
encryptor: Encryptor,
|
204
|
+
encrypt_method: 'encrypt',
|
205
|
+
decrypt_method: 'decrypt',
|
206
|
+
mode: :per_attribute_iv,
|
207
|
+
algorithm: 'aes-256-gcm',
|
208
|
+
allow_empty_value: false,
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
private :attr_encrypted_default_options
|
213
|
+
|
214
|
+
# Checks if an attribute is configured with <tt>attr_encrypted</tt>
|
215
|
+
#
|
216
|
+
# Example
|
217
|
+
#
|
218
|
+
# class User
|
219
|
+
# attr_accessor :name
|
220
|
+
# attr_encrypted :email
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# User.attr_encrypted?(:name) # false
|
224
|
+
# User.attr_encrypted?(:email) # true
|
225
|
+
def attr_encrypted?(attribute)
|
226
|
+
encrypted_attributes.has_key?(attribute.to_sym)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Decrypts a value for the attribute specified
|
230
|
+
#
|
231
|
+
# Example
|
232
|
+
#
|
233
|
+
# class User
|
234
|
+
# attr_encrypted :email
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
238
|
+
def decrypt(attribute, encrypted_value, options = {})
|
239
|
+
options = encrypted_attributes[attribute.to_sym].merge(options)
|
240
|
+
if options[:if] && !options[:unless] && not_empty?(encrypted_value)
|
241
|
+
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
|
242
|
+
value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
|
243
|
+
if options[:marshal]
|
244
|
+
value = options[:marshaler].send(options[:load_method], value)
|
245
|
+
elsif defined?(Encoding)
|
246
|
+
encoding = Encoding.default_internal || Encoding.default_external
|
247
|
+
value = value.force_encoding(encoding.name)
|
248
|
+
end
|
249
|
+
value
|
250
|
+
else
|
251
|
+
encrypted_value
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Encrypts a value for the attribute specified
|
256
|
+
#
|
257
|
+
# Example
|
258
|
+
#
|
259
|
+
# class User
|
260
|
+
# attr_encrypted :email
|
261
|
+
# end
|
262
|
+
#
|
263
|
+
# encrypted_email = User.encrypt(:email, 'test@example.com')
|
264
|
+
def encrypt(attribute, value, options = {})
|
265
|
+
options = encrypted_attributes[attribute.to_sym].merge(options)
|
266
|
+
if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
|
267
|
+
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
|
268
|
+
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
|
269
|
+
encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
|
270
|
+
encrypted_value
|
271
|
+
else
|
272
|
+
value
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def not_empty?(value)
|
277
|
+
!value.nil? && !(value.is_a?(String) && value.empty?)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Contains a hash of encrypted attributes with virtual attribute names as keys
|
281
|
+
# and their corresponding options as values
|
282
|
+
#
|
283
|
+
# Example
|
284
|
+
#
|
285
|
+
# class User
|
286
|
+
# attr_encrypted :email, key: 'my secret key'
|
287
|
+
# end
|
288
|
+
#
|
289
|
+
# User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
|
290
|
+
def encrypted_attributes
|
291
|
+
@encrypted_attributes ||= superclass.encrypted_attributes.dup
|
292
|
+
end
|
293
|
+
|
294
|
+
# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
|
295
|
+
# if attribute was configured with attr_encrypted
|
296
|
+
#
|
297
|
+
# Example
|
298
|
+
#
|
299
|
+
# class User
|
300
|
+
# attr_encrypted :email, key: 'my secret key'
|
301
|
+
# end
|
302
|
+
#
|
303
|
+
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
|
304
|
+
def method_missing(method, *arguments, &block)
|
305
|
+
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
|
306
|
+
send($1, $3, *arguments)
|
307
|
+
else
|
308
|
+
super
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
module InstanceMethods
|
313
|
+
# Decrypts a value for the attribute specified using options evaluated in the current object's scope
|
314
|
+
#
|
315
|
+
# Example
|
316
|
+
#
|
317
|
+
# class User
|
318
|
+
# attr_accessor :secret_key
|
319
|
+
# attr_encrypted :email, key: :secret_key
|
320
|
+
#
|
321
|
+
# def initialize(secret_key)
|
322
|
+
# self.secret_key = secret_key
|
323
|
+
# end
|
324
|
+
# end
|
325
|
+
#
|
326
|
+
# @user = User.new('some-secret-key')
|
327
|
+
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
328
|
+
def decrypt(attribute, encrypted_value)
|
329
|
+
encrypted_attributes[attribute.to_sym][:operation] = :decrypting
|
330
|
+
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
|
331
|
+
self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
|
332
|
+
end
|
333
|
+
|
334
|
+
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
|
335
|
+
#
|
336
|
+
# Example
|
337
|
+
#
|
338
|
+
# class User
|
339
|
+
# attr_accessor :secret_key
|
340
|
+
# attr_encrypted :email, key: :secret_key
|
341
|
+
#
|
342
|
+
# def initialize(secret_key)
|
343
|
+
# self.secret_key = secret_key
|
344
|
+
# end
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
# @user = User.new('some-secret-key')
|
348
|
+
# @user.encrypt(:email, 'test@example.com')
|
349
|
+
def encrypt(attribute, value)
|
350
|
+
encrypted_attributes[attribute.to_sym][:operation] = :encrypting
|
351
|
+
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
|
352
|
+
self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
|
353
|
+
end
|
354
|
+
|
355
|
+
# Copies the class level hash of encrypted attributes with virtual attribute names as keys
|
356
|
+
# and their corresponding options as values to the instance
|
357
|
+
#
|
358
|
+
def encrypted_attributes
|
359
|
+
@encrypted_attributes ||= begin
|
360
|
+
duplicated= {}
|
361
|
+
self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
|
362
|
+
duplicated
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
protected
|
367
|
+
|
368
|
+
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
|
369
|
+
def evaluated_attr_encrypted_options_for(attribute)
|
370
|
+
evaluated_options = Hash.new
|
371
|
+
attributes = encrypted_attributes[attribute.to_sym]
|
372
|
+
attribute_option_value = attributes[:attribute]
|
373
|
+
|
374
|
+
[:if, :unless, :value_present, :allow_empty_value].each do |option|
|
375
|
+
evaluated_options[option] = evaluate_attr_encrypted_option(attributes[option])
|
376
|
+
end
|
377
|
+
|
378
|
+
evaluated_options[:attribute] = attribute_option_value
|
379
|
+
|
380
|
+
evaluated_options.tap do |options|
|
381
|
+
if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
|
382
|
+
(attributes.keys - evaluated_options.keys).each do |option|
|
383
|
+
options[option] = evaluate_attr_encrypted_option(attributes[option])
|
384
|
+
end
|
385
|
+
|
386
|
+
unless options[:mode] == :single_iv_and_salt
|
387
|
+
load_iv_for_attribute(attribute, options)
|
388
|
+
end
|
389
|
+
|
390
|
+
if options[:mode] == :per_attribute_iv_and_salt
|
391
|
+
load_salt_for_attribute(attribute, options)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Evaluates symbol (method reference) or proc (responds to call) options
|
398
|
+
#
|
399
|
+
# If the option is not a symbol or proc then the original option is returned
|
400
|
+
def evaluate_attr_encrypted_option(option)
|
401
|
+
if option.is_a?(Symbol) && respond_to?(option, true)
|
402
|
+
send(option)
|
403
|
+
elsif option.respond_to?(:call)
|
404
|
+
option.call(self)
|
405
|
+
else
|
406
|
+
option
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def load_iv_for_attribute(attribute, options)
|
411
|
+
encrypted_attribute_name = options[:attribute]
|
412
|
+
encode_iv = options[:encode_iv]
|
413
|
+
iv = options[:iv] || send("#{encrypted_attribute_name}_iv")
|
414
|
+
if options[:operation] == :encrypting
|
415
|
+
begin
|
416
|
+
iv = generate_iv(options[:algorithm])
|
417
|
+
iv = [iv].pack(encode_iv) if encode_iv
|
418
|
+
send("#{encrypted_attribute_name}_iv=", iv)
|
419
|
+
rescue RuntimeError
|
420
|
+
end
|
421
|
+
end
|
422
|
+
if iv && !iv.empty?
|
423
|
+
iv = iv.unpack(encode_iv).first if encode_iv
|
424
|
+
options[:iv] = iv
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def generate_iv(algorithm)
|
429
|
+
algo = OpenSSL::Cipher.new(algorithm)
|
430
|
+
algo.encrypt
|
431
|
+
algo.random_iv
|
432
|
+
end
|
433
|
+
|
434
|
+
def load_salt_for_attribute(attribute, options)
|
435
|
+
encrypted_attribute_name = options[:attribute]
|
436
|
+
encode_salt = options[:encode_salt]
|
437
|
+
salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
|
438
|
+
if options[:operation] == :encrypting
|
439
|
+
salt = SecureRandom.random_bytes
|
440
|
+
salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
|
441
|
+
send("#{encrypted_attribute_name}_salt=", salt)
|
442
|
+
end
|
443
|
+
if salt && !salt.empty?
|
444
|
+
salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt
|
445
|
+
options[:salt] = salt
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def prefix_and_encode_salt(salt, encoding)
|
450
|
+
prefix = '_'
|
451
|
+
prefix + [salt].pack(encoding)
|
452
|
+
end
|
453
|
+
|
454
|
+
def decode_salt_if_encoded(salt, encoding)
|
455
|
+
prefix = '_'
|
456
|
+
salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
protected
|
461
|
+
|
462
|
+
def attribute_instance_methods_as_symbols
|
463
|
+
instance_methods.collect { |method| method.to_sym }
|
464
|
+
end
|
465
|
+
|
466
|
+
def attribute_instance_methods_as_symbols_available?
|
467
|
+
true
|
468
|
+
end
|
469
|
+
|
470
|
+
end
|
471
|
+
|
472
|
+
|
473
|
+
Dir[File.join(File.dirname(__FILE__), 'attr_encrypted', 'adapters', '*.rb')].each { |adapter| require adapter }
|