├── Gemfile
├── .gitignore
├── checksum
├── attr_encrypted-3.0.0.gem.sha256
├── attr_encrypted-3.0.1.gem.sha256
├── attr_encrypted-3.0.2.gem.sha256
├── attr_encrypted-3.0.3.gem.sha256
├── attr_encrypted-3.0.0.gem.sha512
├── attr_encrypted-3.0.1.gem.sha512
├── attr_encrypted-3.0.2.gem.sha512
└── attr_encrypted-3.0.3.gem.sha512
├── test
├── run.sh
├── sequel_test.rb
├── data_mapper_test.rb
├── test_helper.rb
├── legacy_sequel_test.rb
├── legacy_data_mapper_test.rb
├── legacy_compatibility_test.rb
├── legacy_active_record_test.rb
├── compatibility_test.rb
├── legacy_attr_encrypted_test.rb
├── active_record_test.rb
└── attr_encrypted_test.rb
├── lib
├── attr_encrypted
│ ├── adapters
│ │ ├── sequel.rb
│ │ ├── data_mapper.rb
│ │ └── active_record.rb
│ └── version.rb
└── attr_encrypted.rb
├── Rakefile
├── MIT-LICENSE
├── .travis.yml
├── certs
└── saghaulor.pem
├── attr_encrypted.gemspec
├── CHANGELOG.md
└── README.md
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle
2 | .DS_Store
3 | .ruby-version
4 | pkg
5 | Gemfile.lock
6 | coverage
7 |
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.0.gem.sha256:
--------------------------------------------------------------------------------
1 | 845fc3cb09a19c3ac76192aba443788f92c880744617bca99b16fd31ce843e07
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.1.gem.sha256:
--------------------------------------------------------------------------------
1 | 33140af4b223177db7a19efb2fa38472a299a745b29ca1c5ba9d3fa947390b77
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.2.gem.sha256:
--------------------------------------------------------------------------------
1 | c1256b459336d4a2012a0d0c70ce5cd3dac46acb5e78da6f77f6f104cb1e8b7b
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.3.gem.sha256:
--------------------------------------------------------------------------------
1 | 6d84c64852c4bbc0926b92fe7a93295671a9e69cb2939b96fb1e4b5e8a5b33b6
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.0.gem.sha512:
--------------------------------------------------------------------------------
1 | 81a065442258cc3702aab62c7b2307a48ed3e0deb803600d11a7480cce0db7c43fd9929acd2755081042f8989236553fd694b6cb62776bbfc53f9165a22cbca1
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.1.gem.sha512:
--------------------------------------------------------------------------------
1 | 0c467cab98b9b2eb331f9818323a90ae01392d6cb03cf1f32faccc954d0fc54be65f0fc7bf751b0fce57925eef1c9e2af90181bc40d81ad93e21d15a001c53c6
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.2.gem.sha512:
--------------------------------------------------------------------------------
1 | dca0c8a729974c0e26fde4cd4216c7d0f66d9eca9f6cf0ccca64999f5180a00bf7c05b630c1d420ec1673141a2923946e8bd28b12e711faf64a4cd42c7a3ac9e
--------------------------------------------------------------------------------
/checksum/attr_encrypted-3.0.3.gem.sha512:
--------------------------------------------------------------------------------
1 | 0f960e8a2f63c747c273241f7395dcceb0dd8a6f79349bee453db741fc7ea5ceb4342d7d5908e540e3b5acea2216ff38bef8c743e6e7c8559bacb4a731ab27c4
--------------------------------------------------------------------------------
/test/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh -e
2 |
3 | for RUBY in 1.9.3 2.0.0 2.1 2.2
4 | do
5 | for RAILS in 2.3.8 3.0.0 3.1.0 3.2.0 4.0.0 4.1.0 4.2.0
6 | do
7 | if [[ $RUBY -gt 1.9.3 && $RAILS -lt 4.0.0 ]]; then
8 | continue
9 | fi
10 | RBENV_VERSION=$RUBY ACTIVERECORD=$RAILS bundle && bundle exec rake
11 | done
12 | done
13 |
--------------------------------------------------------------------------------
/lib/attr_encrypted/adapters/sequel.rb:
--------------------------------------------------------------------------------
1 | if defined?(Sequel)
2 | module AttrEncrypted
3 | module Adapters
4 | module Sequel
5 | def self.extended(base) # :nodoc:
6 | base.attr_encrypted_options[:encode] = true
7 | end
8 | end
9 | end
10 | end
11 |
12 | Sequel::Model.extend AttrEncrypted
13 | Sequel::Model.extend AttrEncrypted::Adapters::Sequel
14 | end
--------------------------------------------------------------------------------
/lib/attr_encrypted/version.rb:
--------------------------------------------------------------------------------
1 | module AttrEncrypted
2 | # Contains information about this gem's version
3 | module Version
4 | MAJOR = 3
5 | MINOR = 0
6 | PATCH = 3
7 |
8 | # Returns a version string by joining MAJOR, MINOR, and PATCH with '.'
9 | #
10 | # Example
11 | #
12 | # Version.string # '1.0.2'
13 | def self.string
14 | [MAJOR, MINOR, PATCH].join('.')
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 | require 'rdoc/task'
3 | require "bundler/gem_tasks"
4 |
5 | desc 'Test the attr_encrypted gem.'
6 | Rake::TestTask.new(:test) do |t|
7 | t.libs << 'lib'
8 | t.pattern = 'test/**/*_test.rb'
9 | t.warning = false
10 | t.verbose = true
11 | end
12 |
13 | desc 'Generate documentation for the attr_encrypted gem.'
14 | Rake::RDocTask.new(:rdoc) do |rdoc|
15 | rdoc.rdoc_dir = 'rdoc'
16 | rdoc.title = 'attr_encrypted'
17 | rdoc.options << '--line-numbers' << '--inline-source'
18 | rdoc.rdoc_files.include('README*')
19 | rdoc.rdoc_files.include('lib/**/*.rb')
20 | end
21 |
22 | desc 'Default: run unit tests.'
23 | task :default => :test
24 |
--------------------------------------------------------------------------------
/lib/attr_encrypted/adapters/data_mapper.rb:
--------------------------------------------------------------------------------
1 | if defined?(DataMapper)
2 | module AttrEncrypted
3 | module Adapters
4 | module DataMapper
5 | def self.extended(base) # :nodoc:
6 | class << base
7 | alias_method :included_without_attr_encrypted, :included
8 | alias_method :included, :included_with_attr_encrypted
9 | end
10 | end
11 |
12 | def included_with_attr_encrypted(base)
13 | included_without_attr_encrypted(base)
14 | base.extend AttrEncrypted
15 | base.attr_encrypted_options[:encode] = true
16 | end
17 | end
18 | end
19 | end
20 |
21 | DataMapper::Resource.extend AttrEncrypted::Adapters::DataMapper
22 | end
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
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.
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: ruby
3 | cache: bundler
4 | rvm:
5 | - 2.0
6 | - 2.1
7 | - 2.2.2
8 | - 2.3.0
9 | - 2.4.0
10 | - rbx
11 | env:
12 | - ACTIVERECORD=3.0.0
13 | - ACTIVERECORD=3.1.0
14 | - ACTIVERECORD=3.2.0
15 | - ACTIVERECORD=4.0.0
16 | - ACTIVERECORD=4.1.0
17 | - ACTIVERECORD=4.2.0
18 | - ACTIVERECORD=5.0.0
19 | - ACTIVERECORD=5.1.1
20 | matrix:
21 | exclude:
22 | - rvm: 2.0
23 | env: ACTIVERECORD=5.0.0
24 | - rvm: 2.0
25 | env: ACTIVERECORD=5.1.1
26 | - rvm: 2.1
27 | env: ACTIVERECORD=5.0.0
28 | - rvm: 2.1
29 | env: ACTIVERECORD=5.1.1
30 | - rvm: 2.4.0
31 | env: ACTIVERECORD=3.0.0
32 | - rvm: 2.4.0
33 | env: ACTIVERECORD=3.1.0
34 | - rvm: 2.4.0
35 | env: ACTIVERECORD=3.2.0
36 | - rvm: 2.4.0
37 | env: ACTIVERECORD=4.0.0
38 | - rvm: 2.4.0
39 | env: ACTIVERECORD=4.1.0
40 | - rvm: rbx
41 | env: ACTIVERECORD=5.0.0
42 | - rvm: rbx
43 | env: ACTIVERECORD=5.1.1
44 | allow_failures:
45 | - rvm: rbx
46 | fast_finish: true
47 | addons:
48 | code_climate:
49 | repo_token: a90435ed4954dd6e9f3697a20c5bc3754f67d94703f870e8fc7b00f69f5b2d06
50 |
--------------------------------------------------------------------------------
/certs/saghaulor.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDdDCCAlygAwIBAgIBATANBgkqhkiG9w0BAQUFADBAMRIwEAYDVQQDDAlzYWdo
3 | YXVsb3IxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
4 | bTAeFw0xNjAxMTEyMjQyMDFaFw0xNzAxMTAyMjQyMDFaMEAxEjAQBgNVBAMMCXNh
5 | Z2hhdWxvcjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD
6 | Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx0xdQYk2GwCpQ1n/
7 | n2mPVYHLYqU5TAn/82t5kbqBUWjbcj8tHAi41tJ19+fT/hH0dog8JHvho1zmOr71
8 | ZIqreJQo60TqP6oE9a5HncUpjqbRp7tOmHo9E+mOW1yT4NiXqFf1YINExQKy2XND
9 | WPQ+T50ZNUsGMfHFWB4NAymejRWXlOEY3bvKW0UHFeNmouP5he51TjoP8uCc9536
10 | 4AIWVP/zzzjwrFtC7av7nRw4Y+gX2bQjrkK2k2JS0ejiGzKBIEMJejcI2B+t79zT
11 | kUQq9SFwp2BrKSIy+4kh4CiF20RT/Hfc1MbvTxSIl/bbIxCYEOhmtHExHi0CoCWs
12 | YCGCXQIDAQABo3kwdzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
13 | SCpVzSBvYbO6B3oT3n3RCZmurG8wHgYDVR0RBBcwFYETc2FnaGF1bG9yQGdtYWls
14 | LmNvbTAeBgNVHRIEFzAVgRNzYWdoYXVsb3JAZ21haWwuY29tMA0GCSqGSIb3DQEB
15 | BQUAA4IBAQAeiGdC3e0WiZpm0cF/b7JC6hJYXC9Yv9VsRAWD9ROsLjFKwOhmonnc
16 | +l/QrmoTjMakYXBCai/Ca3L+k5eRrKilgyITILsmmFxK8sqPJXUw2Jmwk/dAky6x
17 | hHKVZAofT1OrOOPJ2USoZyhR/VI8epLaD5wUmkVDNqtZWviW+dtRa55aPYjRw5Pj
18 | wuj9nybhZr+BbEbmZE//2nbfkM4hCuMtxxxilPrJ22aYNmeWU0wsPpDyhPYxOUgU
19 | ZjeLmnSDiwL6doiP5IiwALH/dcHU67ck3NGf6XyqNwQrrmtPY0mv1WVVL4Uh+vYE
20 | kHoFzE2no0BfBg78Re8fY69P5yES5ncC
21 | -----END CERTIFICATE-----
22 |
--------------------------------------------------------------------------------
/test/sequel_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | DB.create_table :humans do
4 | primary_key :id
5 | column :encrypted_email, :string
6 | column :encrypted_email_salt, String
7 | column :encrypted_email_iv, :string
8 | column :password, :string
9 | column :encrypted_credentials, :string
10 | column :encrypted_credentials_iv, :string
11 | column :encrypted_credentials_salt, String
12 | end
13 |
14 | class Human < Sequel::Model(:humans)
15 | self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
16 |
17 | attr_encrypted :email, :key => SECRET_KEY
18 | attr_encrypted :credentials, :key => SECRET_KEY, :marshal => true
19 |
20 | def after_initialize(attrs = {})
21 | self.credentials ||= { :username => 'example', :password => 'test' }
22 | end
23 | end
24 |
25 | class SequelTest < Minitest::Test
26 |
27 | def setup
28 | Human.all.each(&:destroy)
29 | end
30 |
31 | def test_should_encrypt_email
32 | @human = Human.new :email => 'test@example.com'
33 | assert @human.save
34 | refute_nil @human.encrypted_email
35 | refute_equal @human.email, @human.encrypted_email
36 | assert_equal @human.email, Human.first.email
37 | end
38 |
39 | def test_should_marshal_and_encrypt_credentials
40 |
41 | @human = Human.new :credentials => { :username => 'example', :password => 'test' }
42 | assert @human.save
43 | refute_nil @human.encrypted_credentials
44 | refute_equal @human.credentials, @human.encrypted_credentials
45 | assert_equal @human.credentials, Human.first.credentials
46 | assert Human.first.credentials.is_a?(Hash)
47 | end
48 |
49 | def test_should_encode_by_default
50 | assert Human.attr_encrypted_options[:encode]
51 | end
52 |
53 | end
54 |
--------------------------------------------------------------------------------
/test/data_mapper_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | DataMapper.setup(:default, 'sqlite3::memory:')
4 |
5 | class Client
6 | include DataMapper::Resource
7 |
8 | property :id, Serial
9 | property :encrypted_email, String
10 | property :encrypted_email_iv, String
11 | property :encrypted_email_salt, String
12 |
13 | property :encrypted_credentials, Text
14 | property :encrypted_credentials_iv, Text
15 | property :encrypted_credentials_salt, Text
16 |
17 | self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
18 |
19 | attr_encrypted :email, :key => SECRET_KEY
20 | attr_encrypted :credentials, :key => SECRET_KEY, :marshal => true
21 |
22 | def initialize(attrs = {})
23 | super attrs
24 | self.credentials ||= { :username => 'example', :password => 'test' }
25 | end
26 | end
27 |
28 | DataMapper.auto_migrate!
29 |
30 | class DataMapperTest < Minitest::Test
31 |
32 | def setup
33 | Client.all.each(&:destroy)
34 | end
35 |
36 | def test_should_encrypt_email
37 | @client = Client.new :email => 'test@example.com'
38 | assert @client.save
39 | refute_nil @client.encrypted_email
40 | refute_equal @client.email, @client.encrypted_email
41 | assert_equal @client.email, Client.first.email
42 | end
43 |
44 | def test_should_marshal_and_encrypt_credentials
45 | @client = Client.new
46 | assert @client.save
47 | refute_nil @client.encrypted_credentials
48 | refute_equal @client.credentials, @client.encrypted_credentials
49 | assert_equal @client.credentials, Client.first.credentials
50 | assert Client.first.credentials.is_a?(Hash)
51 | end
52 |
53 | def test_should_encode_by_default
54 | assert Client.attr_encrypted_options[:encode]
55 | end
56 |
57 | end
58 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | require 'simplecov-rcov'
3 | require "codeclimate-test-reporter"
4 |
5 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
6 | [
7 | SimpleCov::Formatter::HTMLFormatter,
8 | SimpleCov::Formatter::RcovFormatter,
9 | CodeClimate::TestReporter::Formatter
10 | ]
11 | )
12 |
13 | SimpleCov.start do
14 | add_filter 'test'
15 | end
16 |
17 | CodeClimate::TestReporter.start
18 |
19 | require 'minitest/autorun'
20 |
21 | # Rails 4.0.x pins to an old minitest
22 | unless defined?(MiniTest::Test)
23 | MiniTest::Test = MiniTest::Unit::TestCase
24 | end
25 |
26 | require 'active_record'
27 | require 'data_mapper'
28 | require 'digest/sha2'
29 | require 'sequel'
30 | ActiveSupport::Deprecation.behavior = :raise
31 |
32 | $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
33 | $:.unshift(File.dirname(__FILE__))
34 | require 'attr_encrypted'
35 |
36 | DB = if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby
37 | Sequel.jdbc('jdbc:sqlite::memory:')
38 | else
39 | Sequel.sqlite
40 | end
41 |
42 | # The :after_initialize hook was removed in Sequel 4.0
43 | # and had been deprecated for a while before that:
44 | # http://sequel.rubyforge.org/rdoc-plugins/classes/Sequel/Plugins/AfterInitialize.html
45 | # This plugin re-enables it.
46 | Sequel::Model.plugin :after_initialize
47 |
48 | SECRET_KEY = SecureRandom.random_bytes(32)
49 |
50 | def base64_encoding_regex
51 | /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/
52 | end
53 |
54 | def drop_all_tables
55 | connection = ActiveRecord::Base.connection
56 | tables = (ActiveRecord::VERSION::MAJOR >= 5 ? connection.data_sources : connection.tables)
57 | tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
58 | end
59 |
--------------------------------------------------------------------------------
/test/legacy_sequel_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | DB.create_table :legacy_humans do
4 | primary_key :id
5 | column :encrypted_email, :string
6 | column :password, :string
7 | column :encrypted_credentials, :string
8 | column :salt, :string
9 | end
10 |
11 | class LegacyHuman < Sequel::Model(:legacy_humans)
12 | self.attr_encrypted_options[:insecure_mode] = true
13 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
14 | self.attr_encrypted_options[:mode] = :single_iv_and_salt
15 |
16 | attr_encrypted :email, :key => 'a secret key', mode: :single_iv_and_salt
17 | attr_encrypted :credentials, :key => Proc.new { |human| Encryptor.encrypt(:value => human.salt, :key => 'some private key', insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true, mode: :single_iv_and_salt
18 |
19 | def after_initialize(attrs = {})
20 | self.salt ||= Digest::SHA1.hexdigest((Time.now.to_i * rand(5)).to_s)
21 | self.credentials ||= { :username => 'example', :password => 'test' }
22 | end
23 | end
24 |
25 | class LegacySequelTest < Minitest::Test
26 |
27 | def setup
28 | LegacyHuman.all.each(&:destroy)
29 | end
30 |
31 | def test_should_encrypt_email
32 | @human = LegacyHuman.new :email => 'test@example.com'
33 | assert @human.save
34 | refute_nil @human.encrypted_email
35 | refute_equal @human.email, @human.encrypted_email
36 | assert_equal @human.email, LegacyHuman.first.email
37 | end
38 |
39 | def test_should_marshal_and_encrypt_credentials
40 | @human = LegacyHuman.new
41 | assert @human.save
42 | refute_nil @human.encrypted_credentials
43 | refute_equal @human.credentials, @human.encrypted_credentials
44 | assert_equal @human.credentials, LegacyHuman.first.credentials
45 | assert LegacyHuman.first.credentials.is_a?(Hash)
46 | end
47 |
48 | def test_should_encode_by_default
49 | assert LegacyHuman.attr_encrypted_options[:encode]
50 | end
51 |
52 | end
53 |
--------------------------------------------------------------------------------
/test/legacy_data_mapper_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | DataMapper.setup(:default, 'sqlite3::memory:')
4 |
5 | class LegacyClient
6 | include DataMapper::Resource
7 | self.attr_encrypted_options[:insecure_mode] = true
8 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
9 | self.attr_encrypted_options[:mode] = :single_iv_and_salt
10 |
11 | property :id, Serial
12 | property :encrypted_email, String
13 | property :encrypted_credentials, Text
14 | property :salt, String
15 |
16 | attr_encrypted :email, :key => 'a secret key', mode: :single_iv_and_salt
17 | attr_encrypted :credentials, :key => Proc.new { |client| Encryptor.encrypt(:value => client.salt, :key => 'some private key', insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true, mode: :single_iv_and_salt
18 |
19 | def initialize(attrs = {})
20 | super attrs
21 | self.salt ||= Digest::SHA1.hexdigest((Time.now.to_i * rand(5)).to_s)
22 | self.credentials ||= { :username => 'example', :password => 'test' }
23 | end
24 | end
25 |
26 | DataMapper.auto_migrate!
27 |
28 | class LegacyDataMapperTest < Minitest::Test
29 |
30 | def setup
31 | LegacyClient.all.each(&:destroy)
32 | end
33 |
34 | def test_should_encrypt_email
35 | @client = LegacyClient.new :email => 'test@example.com'
36 | assert @client.save
37 | refute_nil @client.encrypted_email
38 | refute_equal @client.email, @client.encrypted_email
39 | assert_equal @client.email, LegacyClient.first.email
40 | end
41 |
42 | def test_should_marshal_and_encrypt_credentials
43 | @client = LegacyClient.new
44 | assert @client.save
45 | refute_nil @client.encrypted_credentials
46 | refute_equal @client.credentials, @client.encrypted_credentials
47 | assert_equal @client.credentials, LegacyClient.first.credentials
48 | assert LegacyClient.first.credentials.is_a?(Hash)
49 | end
50 |
51 | def test_should_encode_by_default
52 | assert LegacyClient.attr_encrypted_options[:encode]
53 | end
54 |
55 | end
56 |
--------------------------------------------------------------------------------
/attr_encrypted.gemspec:
--------------------------------------------------------------------------------
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 = 'attr_encrypted'
11 | s.version = AttrEncrypted::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.authors = ['Sean Huber', 'S. Brent Faulkner', 'William Monk', 'Stephen Aghaulor']
18 | s.email = ['seah@shuber.io', 'sbfaulkner@gmail.com', 'billy.monk@gmail.com', 'saghaulor@gmail.com']
19 | s.homepage = 'http://github.com/attr-encrypted/attr_encrypted'
20 | s.license = 'MIT'
21 |
22 | s.has_rdoc = false
23 | s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.rdoc']
24 |
25 | s.require_paths = ['lib']
26 |
27 | s.files = `git ls-files`.split("\n")
28 | s.test_files = `git ls-files -- test/*`.split("\n")
29 |
30 | s.required_ruby_version = '>= 2.0.0'
31 |
32 | s.add_dependency('encryptor', ['~> 3.0.0'])
33 | # support for testing with specific active record version
34 | activerecord_version = if ENV.key?('ACTIVERECORD')
35 | "~> #{ENV['ACTIVERECORD']}"
36 | else
37 | '>= 2.0.0'
38 | end
39 | s.add_development_dependency('activerecord', activerecord_version)
40 | s.add_development_dependency('actionpack', activerecord_version)
41 | s.add_development_dependency('datamapper')
42 | s.add_development_dependency('rake')
43 | s.add_development_dependency('minitest')
44 | s.add_development_dependency('sequel')
45 | if RUBY_VERSION < '2.1.0'
46 | s.add_development_dependency('nokogiri', '< 1.7.0')
47 | end
48 | if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby
49 | s.add_development_dependency('activerecord-jdbcsqlite3-adapter')
50 | s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke
51 | else
52 | s.add_development_dependency('sqlite3')
53 | end
54 | s.add_development_dependency('dm-sqlite-adapter')
55 | s.add_development_dependency('simplecov')
56 | s.add_development_dependency('simplecov-rcov')
57 | s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0')
58 |
59 | s.cert_chain = ['certs/saghaulor.pem']
60 | s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/
61 |
62 | s.post_install_message = "\n\n\nWARNING: Several insecure default options and features were deprecated in attr_encrypted v2.0.0.\n
63 | Additionally, there was a bug in Encryptor v2.0.0 that insecurely encrypted data when using an AES-*-GCM algorithm.\n
64 | This bug was fixed but introduced breaking changes between v2.x and v3.x.\n
65 | Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n"
66 |
67 | end
68 |
--------------------------------------------------------------------------------
/test/legacy_compatibility_test.rb:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require_relative 'test_helper'
3 |
4 | # Test to ensure that existing representations in database do not break on
5 | # migrating to new versions of this gem. This ensures that future versions of
6 | # this gem will retain backwards compatibility with data generated by earlier
7 | # versions.
8 | class LegacyCompatibilityTest < Minitest::Test
9 | class LegacyNonmarshallingPet < ActiveRecord::Base
10 | PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt')
11 | PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key'
12 | PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt')
13 | PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key'
14 |
15 | self.attr_encrypted_options[:insecure_mode] = true
16 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
17 | self.attr_encrypted_options[:mode] = :single_iv_and_salt
18 |
19 | attr_encrypted :nickname,
20 | :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }
21 | attr_encrypted :birthdate,
22 | :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }
23 | end
24 |
25 | class LegacyMarshallingPet < ActiveRecord::Base
26 | PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt')
27 | PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key'
28 | PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt')
29 | PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key'
30 |
31 | self.attr_encrypted_options[:insecure_mode] = true
32 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
33 | self.attr_encrypted_options[:mode] = :single_iv_and_salt
34 |
35 | attr_encrypted :nickname,
36 | :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') },
37 | :marshal => true
38 | attr_encrypted :birthdate,
39 | :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') },
40 | :marshal => true
41 | end
42 |
43 | def setup
44 | drop_all_tables
45 | create_tables
46 | end
47 |
48 | def test_nonmarshalling_backwards_compatibility
49 | pet = LegacyNonmarshallingPet.create!(
50 | :name => 'Fido',
51 | :encrypted_nickname => 'uSUB6KGzta87yxesyVc3DA==',
52 | :encrypted_birthdate => 'I3d691B2PtFXLx15kO067g=='
53 | )
54 |
55 | assert_equal 'Fido', pet.name
56 | assert_equal 'Fido the Dog', pet.nickname
57 | assert_equal '2011-07-09', pet.birthdate
58 | end
59 |
60 | def test_marshalling_backwards_compatibility
61 | pet = LegacyMarshallingPet.create!(
62 | :name => 'Fido',
63 | :encrypted_nickname => '7RwoT64in4H+fGVBPYtRcN0K4RtriIy1EP4nDojUa8g=',
64 | :encrypted_birthdate => 'bSp9sJhXQSp2QlNZHiujtcK4lRVBE8HQhn1y7moQ63bGJR20hvRSZ73ePAmm+wc5'
65 | )
66 |
67 | assert_equal 'Fido', pet.name
68 | assert_equal 'Mummy\'s little helper', pet.nickname
69 |
70 | assert_equal Date.new(2011, 7, 9), pet.birthdate
71 | end
72 |
73 | private
74 |
75 | def create_tables
76 | ActiveRecord::Schema.define(:version => 1) do
77 | create_table :legacy_nonmarshalling_pets do |t|
78 | t.string :name
79 | t.string :encrypted_nickname
80 | t.string :encrypted_birthdate
81 | t.string :salt
82 | end
83 | create_table :legacy_marshalling_pets do |t|
84 | t.string :name
85 | t.string :encrypted_nickname
86 | t.string :encrypted_birthdate
87 | t.string :salt
88 | end
89 | end
90 | end
91 | end
92 |
93 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
94 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # attr_encrypted #
2 |
3 | ## 3.0.3 ##
4 | * Fixed: attr_was would decrypt the attribute upon every call. This is inefficient and introduces problems when the options change between decrypting an old value and encrypting a new value; for example, when rotating the encryption key. As such, the new approach caches the decrypted value of the old encrypted value such that the old options are no longer needed. (@johnny-lai) (@saghaulor)
5 |
6 | ## 3.0.2 ##
7 | * Changed: Removed alias_method_chain for compatibility with Rails v5.x (@grosser)
8 | * Changed: Updated Travis build matrix to include Rails 5. (@saghaulor) (@connorshea)
9 | * Changed: Removed `.silence_stream` from tests as it has been removed from Rails 5. (@sblackstone)
10 |
11 | ## 3.0.1 ##
12 | * Fixed: attr_was method no longer calls undefined methods. (@saghaulor)
13 |
14 | ## 3.0.0 ##
15 | * Changed: Updated gemspec to use Encryptor v3.0.0. (@saghaulor)
16 | * Changed: Updated README with instructions related to moving from v2.0.0 to v3.0.0. (@saghaulor)
17 | * Fixed: ActiveModel::Dirty methods in the ActiveRecord adapter. (@saghaulor)
18 |
19 | ## 2.0.0 ##
20 | * Added: Now using Encryptor v2.0.0 (@saghaulor)
21 | * Added: Options are copied to the instance. (@saghaulor)
22 | * Added: Operation option is set during encryption/decryption to allow options to be evaluated in the context of the current operation. (@saghaulor)
23 | * Added: IV and salt can be conditionally encoded. (@saghaulor)
24 | * Added: Changelog! (@saghaulor)
25 | * Changed: attr_encrypted no longer extends object, to use with PORO extend your class, all supported ORMs are already extended. (@saghaulor)
26 | * Changed: Salt is now generated with more entropy. (@saghaulor)
27 | * Changed: The default algorithm is now `aes-256-gcm`. (@saghaulor)
28 | * Changed: The default mode is now `:per_attribute_iv`' (@saghaulor)
29 | * Changed: Extracted class level default options hash to a private method. (@saghaulor)
30 | * Changed: Dynamic finders only work with `:single_iv_and_salt` mode. (@saghaulor)
31 | * Changed: Updated documentation to include v2.0.0 changes and 'things to consider' section. (@saghaulor)
32 | * Fixed: All options are evaluated correctly. (@saghaulor)
33 | * Fixed: IV is generated for every encryption operation. (@saghaulor)
34 | * Deprecated: `:single_iv_and_salt` and `:per_attribute_iv_and_salt` modes are deprecated and will be removed in the next major release. (@saghaulor)
35 | * Deprecated: Dynamic finders via `method_missing` is deprecated and will be removed in the next major release. (@saghaulor)
36 | * Removed: Support for Ruby < 2.x (@saghaulor)
37 | * Removed: Support for Rails < 3.x (@saghaulor)
38 | * Removed: Unnecessary use of `alias_method` from ActiveRecord adapter. (@saghaulor)
39 |
40 | ## 1.4.0 ##
41 | * Added: ActiveModel::Dirty#attribute_was (@saghaulor)
42 | * Added: ActiveModel::Dirty#attribute_changed? (@mwean)
43 |
44 | ## 1.3.5 ##
45 | * Changed: Fixed gemspec to explicitly depend on Encryptor v1.3.0 (@saghaulor)
46 | * Fixed: Evaluate `:mode` option as a symbol or proc. (@cheynewallace)
47 |
48 | ## 1.3.4 ##
49 | * Added: ActiveRecord::Base.reload support. (@rcook)
50 | * Fixed: ActiveRecord adapter no longer forces attribute hashes to be string-keyed. (@tamird)
51 | * Fixed: Mass assignment protection in ActiveRecord 4. (@tamird)
52 | * Changed: Now using rubygems over https. (@tamird)
53 | * Changed: Let ActiveRecord define attribute methods. (@saghaulor)
54 |
55 | ## 1.3.3 ##
56 | * Added: Alias attr_encryptor and attr_encrpted. (@Billy Monk)
57 |
58 | ## 1.3.2 ##
59 | * Fixed: Bug regarding strong parameters. (@S. Brent Faulkner)
60 | * Fixed: Bug regarding loading per instance IV and salt. (@S. Brent Faulkner)
61 | * Fixed: Bug regarding assigning nil. (@S. Brent Faulkner)
62 | * Added: Support for protected attributes. (@S. Brent Faulkner)
63 | * Added: Support for ActiveRecord 4. (@S. Brent Faulkner)
64 |
65 | ## 1.3.1 ##
66 | * Added: Support for Rails 2.3.x and 3.1.x. (@S. Brent Faulkner)
67 |
68 | ## 1.3.0 ##
69 | * Fixed: Serialization bug. (@Billy Monk)
70 | * Added: Support for :per_attribute_iv_and_salt mode. (@rcook)
71 | * Fixed: Added dependencies to gemspec. (@jmazzi)
72 |
73 | ## 1.2.1 ##
74 | * Added: Force encoding when not marshaling. (@mosaicxm)
75 | * Fixed: Issue specifying multiple attributes on the same line. (@austintaylor)
76 | * Added: Typecasting to String before encryption (@shuber)
77 | * Added: `"#{attribute}?"` method. (@shuber)
78 |
79 | ## 1.2.0 ##
80 | * Changed: General code refactoring (@shuber)
81 |
82 | ## 1.1.2 ##
83 | * No significant changes
84 |
85 | ## 1.1.1 ##
86 | * Changled: Updated README. (@shuber)
87 | * Added: `before_type_cast` alias to ActiveRecord adapter. (@shuber)
88 |
--------------------------------------------------------------------------------
/test/legacy_active_record_test.rb:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require_relative 'test_helper'
3 |
4 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
5 |
6 | def create_people_table
7 | ActiveRecord::Schema.define(:version => 1) do
8 | create_table :legacy_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 |
17 | # The table needs to exist before defining the class
18 | create_people_table
19 |
20 | ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)
21 |
22 | class LegacyPerson < ActiveRecord::Base
23 | self.attr_encrypted_options[:insecure_mode] = true
24 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
25 | self.attr_encrypted_options[:mode] = :single_iv_and_salt
26 |
27 | attr_encrypted :email, :key => 'a secret key'
28 | attr_encrypted :credentials, :key => Proc.new { |user| Encryptor.encrypt(:value => user.salt, :key => 'some private key', insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true
29 |
30 | ActiveSupport::Deprecation.silenced = true
31 | def after_initialize; end
32 | ActiveSupport::Deprecation.silenced = false
33 |
34 | after_initialize :initialize_salt_and_credentials
35 |
36 | protected
37 |
38 | def initialize_salt_and_credentials
39 | self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(5)).to_s)
40 | self.credentials ||= { :username => 'example', :password => 'test' }
41 | rescue ActiveRecord::MissingAttributeError
42 | end
43 | end
44 |
45 | class LegacyPersonWithValidation < LegacyPerson
46 | validates_presence_of :email
47 | validates_uniqueness_of :encrypted_email
48 | end
49 |
50 | class LegacyActiveRecordTest < Minitest::Test
51 |
52 | def setup
53 | drop_all_tables
54 | create_people_table
55 | end
56 |
57 | def test_should_decrypt_with_correct_encoding
58 | if defined?(Encoding)
59 | @person = LegacyPerson.create :email => 'test@example.com'
60 | assert_equal 'UTF-8', LegacyPerson.first.email.encoding.name
61 | end
62 | end
63 |
64 | def test_should_encrypt_email
65 | @person = LegacyPerson.create :email => 'test@example.com'
66 | refute_nil @person.encrypted_email
67 | refute_equal @person.email, @person.encrypted_email
68 | assert_equal @person.email, LegacyPerson.first.email
69 | end
70 |
71 | def test_should_marshal_and_encrypt_credentials
72 | @person = LegacyPerson.create
73 | refute_nil @person.encrypted_credentials
74 | refute_equal @person.credentials, @person.encrypted_credentials
75 | assert_equal @person.credentials, LegacyPerson.first.credentials
76 | end
77 |
78 | def test_should_find_by_email
79 | @person = LegacyPerson.create(:email => 'test@example.com')
80 | assert_equal @person, LegacyPerson.find_by_email('test@example.com')
81 | end
82 |
83 | def test_should_find_by_email_and_password
84 | LegacyPerson.create(:email => 'test@example.com', :password => 'invalid')
85 | @person = LegacyPerson.create(:email => 'test@example.com', :password => 'test')
86 | assert_equal @person, LegacyPerson.find_by_email_and_password('test@example.com', 'test')
87 | end
88 |
89 | def test_should_scope_by_email
90 | @person = LegacyPerson.create(:email => 'test@example.com')
91 | assert_equal @person, LegacyPerson.scoped_by_email('test@example.com').first rescue NoMethodError
92 | end
93 |
94 | def test_should_scope_by_email_and_password
95 | LegacyPerson.create(:email => 'test@example.com', :password => 'invalid')
96 | @person = LegacyPerson.create(:email => 'test@example.com', :password => 'test')
97 | assert_equal @person, LegacyPerson.scoped_by_email_and_password('test@example.com', 'test').first rescue NoMethodError
98 | end
99 |
100 | def test_should_encode_by_default
101 | assert LegacyPerson.attr_encrypted_options[:encode]
102 | end
103 |
104 | def test_should_validate_presence_of_email
105 | @person = LegacyPersonWithValidation.new
106 | assert !@person.valid?
107 | assert !@person.errors[:email].empty? || @person.errors.on(:email)
108 | end
109 |
110 | def test_should_validate_uniqueness_of_email
111 | @person = LegacyPersonWithValidation.new :email => 'test@example.com'
112 | assert @person.save
113 | @person2 = LegacyPersonWithValidation.new :email => @person.email
114 | assert !@person2.valid?
115 | assert !@person2.errors[:encrypted_email].empty? || @person2.errors.on(:encrypted_email)
116 | end
117 |
118 | end
119 |
--------------------------------------------------------------------------------
/test/compatibility_test.rb:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require_relative 'test_helper'
3 |
4 | # Test to ensure that existing representations in database do not break on
5 | # migrating to new versions of this gem. This ensures that future versions of
6 | # this gem will retain backwards compatibility with data generated by earlier
7 | # versions.
8 | class CompatibilityTest < Minitest::Test
9 | class NonmarshallingPet < ActiveRecord::Base
10 | PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt')
11 | PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key'
12 | PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt')
13 | PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key'
14 |
15 | self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
16 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
17 | self.attr_encrypted_options[:insecure_mode] = true
18 |
19 | attr_encrypted :nickname,
20 | :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }
21 | attr_encrypted :birthdate,
22 | :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }
23 | end
24 |
25 | class MarshallingPet < ActiveRecord::Base
26 | PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt')
27 | PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key'
28 | PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt')
29 | PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key'
30 |
31 | self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
32 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
33 | self.attr_encrypted_options[:insecure_mode] = true
34 |
35 | attr_encrypted :nickname,
36 | :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') },
37 | :marshal => true
38 | attr_encrypted :birthdate,
39 | :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') },
40 | :marshal => true
41 | end
42 |
43 | def setup
44 | drop_all_tables
45 | create_tables
46 | end
47 |
48 | def test_nonmarshalling_backwards_compatibility
49 | pet = NonmarshallingPet.create!(
50 | :name => 'Fido',
51 | :encrypted_nickname => 'E4lJTxFG/EfkfPg5MpnriQ==',
52 | :encrypted_nickname_iv => 'z4Q8deE4h7f6S8NNZcbPNg==',
53 | :encrypted_nickname_salt => 'adcd833001a873db',
54 | :encrypted_birthdate => '6uKEAiFVdJw+N5El+U6Gow==',
55 | :encrypted_birthdate_iv => 'zxtc1XPssL4s2HwA69nORQ==',
56 | :encrypted_birthdate_salt => '4f879270045eaad7'
57 | )
58 |
59 | assert_equal 'Fido', pet.name
60 | assert_equal 'Fido the Dog', pet.nickname
61 | assert_equal '2011-07-09', pet.birthdate
62 | end
63 |
64 | def test_marshalling_backwards_compatibility
65 | pet = MarshallingPet.create!(
66 | :name => 'Fido',
67 | :encrypted_nickname => 'EsQScJYkPw80vVGvKWkE37Px99HHpXPFjoEPTNa4rbs=',
68 | :encrypted_nickname_iv => 'fNq1OZcGvty4KfcvGTcFSw==',
69 | :encrypted_nickname_salt => '733b459b7d34c217',
70 | :encrypted_birthdate => '+VUlKQGfNWkOgCwI4hv+3qlGIwh9h6cJ/ranJlaxvU+xxQdL3H3cOzTcI2rkYkdR',
71 | :encrypted_birthdate_iv => 'Ka+zF/SwEYZKwVa24lvFfA==',
72 | :encrypted_birthdate_salt => 'd5e892d5bbd81566'
73 | )
74 |
75 | assert_equal 'Fido', pet.name
76 | assert_equal 'Mummy\'s little helper', pet.nickname
77 |
78 | assert_equal Date.new(2011, 7, 9), pet.birthdate
79 | end
80 |
81 | private
82 |
83 | def create_tables
84 | ActiveRecord::Schema.define(:version => 1) do
85 | create_table :nonmarshalling_pets do |t|
86 | t.string :name
87 | t.string :encrypted_nickname
88 | t.string :encrypted_nickname_iv
89 | t.string :encrypted_nickname_salt
90 | t.string :encrypted_birthdate
91 | t.string :encrypted_birthdate_iv
92 | t.string :encrypted_birthdate_salt
93 | end
94 | create_table :marshalling_pets do |t|
95 | t.string :name
96 | t.string :encrypted_nickname
97 | t.string :encrypted_nickname_iv
98 | t.string :encrypted_nickname_salt
99 | t.string :encrypted_birthdate
100 | t.string :encrypted_birthdate_iv
101 | t.string :encrypted_birthdate_salt
102 | end
103 | end
104 | end
105 | end
106 |
107 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
108 |
--------------------------------------------------------------------------------
/lib/attr_encrypted/adapters/active_record.rb:
--------------------------------------------------------------------------------
1 | if defined?(ActiveRecord::Base)
2 | module AttrEncrypted
3 | module Adapters
4 | module ActiveRecord
5 | def self.extended(base) # :nodoc:
6 | base.class_eval do
7 |
8 | # https://github.com/attr-encrypted/attr_encrypted/issues/68
9 | alias_method :reload_without_attr_encrypted, :reload
10 | def reload(*args, &block)
11 | result = reload_without_attr_encrypted(*args, &block)
12 | self.class.encrypted_attributes.keys.each do |attribute_name|
13 | instance_variable_set("@#{attribute_name}", nil)
14 | end
15 | result
16 | end
17 |
18 | attr_encrypted_options[:encode] = true
19 |
20 | class << self
21 | alias_method :method_missing_without_attr_encrypted, :method_missing
22 | alias_method :method_missing, :method_missing_with_attr_encrypted
23 | end
24 |
25 | def perform_attribute_assignment(method, new_attributes, *args)
26 | return if new_attributes.blank?
27 |
28 | send method, new_attributes.reject { |k, _| self.class.encrypted_attributes.key?(k.to_sym) }, *args
29 | send method, new_attributes.reject { |k, _| !self.class.encrypted_attributes.key?(k.to_sym) }, *args
30 | end
31 | private :perform_attribute_assignment
32 |
33 | if ::ActiveRecord::VERSION::STRING > "3.1"
34 | alias_method :assign_attributes_without_attr_encrypted, :assign_attributes
35 | def assign_attributes(*args)
36 | perform_attribute_assignment :assign_attributes_without_attr_encrypted, *args
37 | end
38 | end
39 |
40 | alias_method :attributes_without_attr_encrypted=, :attributes=
41 | def attributes=(*args)
42 | perform_attribute_assignment :attributes_without_attr_encrypted=, *args
43 | end
44 | end
45 | end
46 |
47 | protected
48 |
49 | # attr_encrypted method
50 | def attr_encrypted(*attrs)
51 | super
52 | options = attrs.extract_options!
53 | attr = attrs.pop
54 | attribute attr if ::ActiveRecord::VERSION::STRING >= "5.1.0"
55 | options.merge! encrypted_attributes[attr]
56 |
57 | define_method("#{attr}_was") do
58 | attribute_was(attr)
59 | end
60 |
61 | if ::ActiveRecord::VERSION::STRING >= "4.1"
62 | define_method("#{attr}_changed?") do |options = {}|
63 | attribute_changed?(attr, options)
64 | end
65 | else
66 | define_method("#{attr}_changed?") do
67 | attribute_changed?(attr)
68 | end
69 | end
70 |
71 | define_method("#{attr}_change") do
72 | attribute_change(attr)
73 | end
74 |
75 | define_method("#{attr}_with_dirtiness=") do |value|
76 | attribute_will_change!(attr) if value != __send__(attr)
77 | __send__("#{attr}_without_dirtiness=", value)
78 | end
79 |
80 | alias_method "#{attr}_without_dirtiness=", "#{attr}="
81 | alias_method "#{attr}=", "#{attr}_with_dirtiness="
82 |
83 | alias_method "#{attr}_before_type_cast", attr
84 | end
85 |
86 | def attribute_instance_methods_as_symbols
87 | # We add accessor methods of the db columns to the list of instance
88 | # methods returned to let ActiveRecord define the accessor methods
89 | # for the db columns
90 |
91 | # Use with_connection so the connection doesn't stay pinned to the thread.
92 | connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
93 |
94 | if connected && table_exists?
95 | columns_hash.keys.inject(super) {|instance_methods, column_name| instance_methods.concat [column_name.to_sym, :"#{column_name}="]}
96 | else
97 | super
98 | end
99 | end
100 |
101 | # Allows you to use dynamic methods like find_by_email or scoped_by_email for
102 | # encrypted attributes
103 | #
104 | # NOTE: This only works when the :key option is specified as a string (see the README)
105 | #
106 | # This is useful for encrypting fields like email addresses. Your user's email addresses
107 | # are encrypted in the database, but you can still look up a user by email for logging in
108 | #
109 | # Example
110 | #
111 | # class User < ActiveRecord::Base
112 | # attr_encrypted :email, key: 'secret key'
113 | # end
114 | #
115 | # User.find_by_email_and_password('test@example.com', 'testing')
116 | # # results in a call to
117 | # User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing')
118 | def method_missing_with_attr_encrypted(method, *args, &block)
119 | if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
120 | attribute_names = match.captures.last.split('_and_')
121 | attribute_names.each_with_index do |attribute, index|
122 | if attr_encrypted?(attribute) && encrypted_attributes[attribute.to_sym][:mode] == :single_iv_and_salt
123 | args[index] = send("encrypt_#{attribute}", args[index])
124 | warn "DEPRECATION WARNING: This feature will be removed in the next major release."
125 | attribute_names[index] = encrypted_attributes[attribute.to_sym][:attribute]
126 | end
127 | end
128 | method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
129 | end
130 | method_missing_without_attr_encrypted(method, *args, &block)
131 | end
132 | end
133 | end
134 | end
135 |
136 | ActiveRecord::Base.extend AttrEncrypted
137 | ActiveRecord::Base.extend AttrEncrypted::Adapters::ActiveRecord
138 | end
139 |
--------------------------------------------------------------------------------
/test/legacy_attr_encrypted_test.rb:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require_relative 'test_helper'
3 |
4 | class LegacySillyEncryptor
5 | def self.silly_encrypt(options)
6 | (options[:value] + options[:some_arg]).reverse
7 | end
8 |
9 | def self.silly_decrypt(options)
10 | options[:value].reverse.gsub(/#{options[:some_arg]}$/, '')
11 | end
12 | end
13 |
14 | class LegacyUser
15 | extend AttrEncrypted
16 | self.attr_encrypted_options[:key] = Proc.new { |user| user.class.to_s } # default key
17 | self.attr_encrypted_options[:insecure_mode] = true
18 | self.attr_encrypted_options[:algorithm] = 'aes-256-cbc'
19 | self.attr_encrypted_options[:mode] = :single_iv_and_salt
20 |
21 | attr_encrypted :email, :without_encoding, :key => 'secret key'
22 | attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test'
23 | attr_encrypted :ssn, :key => :salt, :attribute => 'ssn_encrypted'
24 | attr_encrypted :credit_card, :encryptor => LegacySillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test'
25 | attr_encrypted :with_encoding, :key => 'secret key', :encode => true
26 | attr_encrypted :with_custom_encoding, :key => 'secret key', :encode => 'm'
27 | attr_encrypted :with_marshaling, :key => 'secret key', :marshal => true
28 | attr_encrypted :with_true_if, :key => 'secret key', :if => true
29 | attr_encrypted :with_false_if, :key => 'secret key', :if => false
30 | attr_encrypted :with_true_unless, :key => 'secret key', :unless => true
31 | attr_encrypted :with_false_unless, :key => 'secret key', :unless => false
32 | attr_encrypted :with_if_changed, :key => 'secret key', :if => :should_encrypt
33 |
34 | attr_encryptor :aliased, :key => 'secret_key'
35 |
36 | attr_accessor :salt
37 | attr_accessor :should_encrypt
38 |
39 | def initialize
40 | self.salt = Time.now.to_i.to_s
41 | self.should_encrypt = true
42 | end
43 | end
44 |
45 | class LegacyAdmin < LegacyUser
46 | attr_encrypted :testing
47 | end
48 |
49 | class LegacySomeOtherClass
50 | extend AttrEncrypted
51 | def self.call(object)
52 | object.class
53 | end
54 | end
55 |
56 | class LegacyAttrEncryptedTest < Minitest::Test
57 |
58 | def test_should_store_email_in_encrypted_attributes
59 | assert LegacyUser.encrypted_attributes.include?(:email)
60 | end
61 |
62 | def test_should_not_store_salt_in_encrypted_attributes
63 | assert !LegacyUser.encrypted_attributes.include?(:salt)
64 | end
65 |
66 | def test_attr_encrypted_should_return_true_for_email
67 | assert LegacyUser.attr_encrypted?('email')
68 | end
69 |
70 | def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line
71 | refute_equal LegacyUser.encrypted_attributes[:email][:attribute], LegacyUser.encrypted_attributes[:without_encoding][:attribute]
72 | end
73 |
74 | def test_attr_encrypted_should_return_false_for_salt
75 | assert !LegacyUser.attr_encrypted?('salt')
76 | end
77 |
78 | def test_should_generate_an_encrypted_attribute
79 | assert LegacyUser.new.respond_to?(:encrypted_email)
80 | end
81 |
82 | def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix
83 | assert LegacyUser.new.respond_to?(:crypted_password_test)
84 | end
85 |
86 | def test_should_generate_an_encrypted_attribute_with_the_attribute_option
87 | assert LegacyUser.new.respond_to?(:ssn_encrypted)
88 | end
89 |
90 | def test_should_not_encrypt_nil_value
91 | assert_nil LegacyUser.encrypt_email(nil)
92 | end
93 |
94 | def test_should_not_encrypt_empty_string
95 | assert_equal '', LegacyUser.encrypt_email('')
96 | end
97 |
98 | def test_should_encrypt_email
99 | refute_nil LegacyUser.encrypt_email('test@example.com')
100 | refute_equal 'test@example.com', LegacyUser.encrypt_email('test@example.com')
101 | end
102 |
103 | def test_should_encrypt_email_when_modifying_the_attr_writer
104 | @user = LegacyUser.new
105 | assert_nil @user.encrypted_email
106 | @user.email = 'test@example.com'
107 | refute_nil @user.encrypted_email
108 | assert_equal LegacyUser.encrypt_email('test@example.com'), @user.encrypted_email
109 | end
110 |
111 | def test_should_not_decrypt_nil_value
112 | assert_nil LegacyUser.decrypt_email(nil)
113 | end
114 |
115 | def test_should_not_decrypt_empty_string
116 | assert_equal '', LegacyUser.decrypt_email('')
117 | end
118 |
119 | def test_should_decrypt_email
120 | encrypted_email = LegacyUser.encrypt_email('test@example.com')
121 | refute_equal 'test@test.com', encrypted_email
122 | assert_equal 'test@example.com', LegacyUser.decrypt_email(encrypted_email)
123 | end
124 |
125 | def test_should_decrypt_email_when_reading
126 | @user = LegacyUser.new
127 | assert_nil @user.email
128 | @user.encrypted_email = LegacyUser.encrypt_email('test@example.com')
129 | assert_equal 'test@example.com', @user.email
130 | end
131 |
132 | def test_should_encrypt_with_encoding
133 | assert_equal LegacyUser.encrypt_with_encoding('test'), [LegacyUser.encrypt_without_encoding('test')].pack('m')
134 | end
135 |
136 | def test_should_decrypt_with_encoding
137 | encrypted = LegacyUser.encrypt_with_encoding('test')
138 | assert_equal 'test', LegacyUser.decrypt_with_encoding(encrypted)
139 | assert_equal LegacyUser.decrypt_with_encoding(encrypted), LegacyUser.decrypt_without_encoding(encrypted.unpack('m').first)
140 | end
141 |
142 | def test_should_decrypt_utf8_with_encoding
143 | encrypted = LegacyUser.encrypt_with_encoding("test\xC2\xA0utf-8\xC2\xA0text")
144 | assert_equal "test\xC2\xA0utf-8\xC2\xA0text", LegacyUser.decrypt_with_encoding(encrypted)
145 | assert_equal LegacyUser.decrypt_with_encoding(encrypted), LegacyUser.decrypt_without_encoding(encrypted.unpack('m').first)
146 | end
147 |
148 | def test_should_encrypt_with_custom_encoding
149 | assert_equal LegacyUser.encrypt_with_custom_encoding('test'), [LegacyUser.encrypt_without_encoding('test')].pack('m')
150 | end
151 |
152 | def test_should_decrypt_with_custom_encoding
153 | encrypted = LegacyUser.encrypt_with_custom_encoding('test')
154 | assert_equal 'test', LegacyUser.decrypt_with_custom_encoding(encrypted)
155 | assert_equal LegacyUser.decrypt_with_custom_encoding(encrypted), LegacyUser.decrypt_without_encoding(encrypted.unpack('m').first)
156 | end
157 |
158 | def test_should_encrypt_with_marshaling
159 | @user = LegacyUser.new
160 | @user.with_marshaling = [1, 2, 3]
161 | refute_nil @user.encrypted_with_marshaling
162 | assert_equal LegacyUser.encrypt_with_marshaling([1, 2, 3]), @user.encrypted_with_marshaling
163 | end
164 |
165 | def test_should_decrypt_with_marshaling
166 | encrypted = LegacyUser.encrypt_with_marshaling([1, 2, 3])
167 | @user = LegacyUser.new
168 | assert_nil @user.with_marshaling
169 | @user.encrypted_with_marshaling = encrypted
170 | assert_equal [1, 2, 3], @user.with_marshaling
171 | end
172 |
173 | def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments
174 | assert_equal LegacySillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), LegacyUser.encrypt_credit_card('testing')
175 | end
176 |
177 | def test_should_evaluate_a_key_passed_as_a_symbol
178 | @user = LegacyUser.new
179 | assert_nil @user.ssn_encrypted
180 | @user.ssn = 'testing'
181 | refute_nil @user.ssn_encrypted
182 | assert_equal Encryptor.encrypt(:value => 'testing', :key => @user.salt, insecure_mode: true, algorithm: 'aes-256-cbc'), @user.ssn_encrypted
183 | end
184 |
185 | def test_should_evaluate_a_key_passed_as_a_proc
186 | @user = LegacyUser.new
187 | assert_nil @user.crypted_password_test
188 | @user.password = 'testing'
189 | refute_nil @user.crypted_password_test
190 | assert_equal Encryptor.encrypt(:value => 'testing', :key => 'LegacyUser', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.crypted_password_test
191 | end
192 |
193 | def test_should_use_options_found_in_the_attr_encrypted_options_attribute
194 | @user = LegacyUser.new
195 | assert_nil @user.crypted_password_test
196 | @user.password = 'testing'
197 | refute_nil @user.crypted_password_test
198 | assert_equal Encryptor.encrypt(:value => 'testing', :key => 'LegacyUser', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.crypted_password_test
199 | end
200 |
201 | def test_should_inherit_encrypted_attributes
202 | assert_equal [LegacyUser.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, LegacyAdmin.encrypted_attributes.keys.collect { |key| key.to_s }.sort
203 | end
204 |
205 | def test_should_inherit_attr_encrypted_options
206 | assert !LegacyUser.attr_encrypted_options.empty?
207 | assert_equal LegacyUser.attr_encrypted_options, LegacyAdmin.attr_encrypted_options
208 | end
209 |
210 | def test_should_not_inherit_unrelated_attributes
211 | assert LegacySomeOtherClass.attr_encrypted_options.empty?
212 | assert LegacySomeOtherClass.encrypted_attributes.empty?
213 | end
214 |
215 | def test_should_evaluate_a_symbol_option
216 | assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, :class)
217 | end
218 |
219 | def test_should_evaluate_a_proc_option
220 | assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, proc { |object| object.class })
221 | end
222 |
223 | def test_should_evaluate_a_lambda_option
224 | assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, lambda { |object| object.class })
225 | end
226 |
227 | def test_should_evaluate_a_method_option
228 | assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, LegacySomeOtherClass.method(:call))
229 | end
230 |
231 | def test_should_return_a_string_option
232 | class_string = 'LegacySomeOtherClass'
233 | assert_equal class_string, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, class_string)
234 | end
235 |
236 | def test_should_encrypt_with_true_if
237 | @user = LegacyUser.new
238 | assert_nil @user.encrypted_with_true_if
239 | @user.with_true_if = 'testing'
240 | refute_nil @user.encrypted_with_true_if
241 | assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.encrypted_with_true_if
242 | end
243 |
244 | def test_should_not_encrypt_with_false_if
245 | @user = LegacyUser.new
246 | assert_nil @user.encrypted_with_false_if
247 | @user.with_false_if = 'testing'
248 | refute_nil @user.encrypted_with_false_if
249 | assert_equal 'testing', @user.encrypted_with_false_if
250 | end
251 |
252 | def test_should_encrypt_with_false_unless
253 | @user = LegacyUser.new
254 | assert_nil @user.encrypted_with_false_unless
255 | @user.with_false_unless = 'testing'
256 | refute_nil @user.encrypted_with_false_unless
257 | assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.encrypted_with_false_unless
258 | end
259 |
260 | def test_should_not_encrypt_with_true_unless
261 | @user = LegacyUser.new
262 | assert_nil @user.encrypted_with_true_unless
263 | @user.with_true_unless = 'testing'
264 | refute_nil @user.encrypted_with_true_unless
265 | assert_equal 'testing', @user.encrypted_with_true_unless
266 | end
267 |
268 | def test_should_work_with_aliased_attr_encryptor
269 | assert LegacyUser.encrypted_attributes.include?(:aliased)
270 | end
271 |
272 | def test_should_always_reset_options
273 | @user = LegacyUser.new
274 | @user.with_if_changed = "encrypt_stuff"
275 |
276 | @user = LegacyUser.new
277 | @user.should_encrypt = false
278 | @user.with_if_changed = "not_encrypted_stuff"
279 | assert_equal "not_encrypted_stuff", @user.with_if_changed
280 | assert_equal "not_encrypted_stuff", @user.encrypted_with_if_changed
281 | end
282 |
283 | def test_should_cast_values_as_strings_before_encrypting
284 | string_encrypted_email = LegacyUser.encrypt_email('3')
285 | assert_equal string_encrypted_email, LegacyUser.encrypt_email(3)
286 | assert_equal '3', LegacyUser.decrypt_email(string_encrypted_email)
287 | end
288 |
289 | def test_should_create_query_accessor
290 | @user = LegacyUser.new
291 | assert !@user.email?
292 | @user.email = ''
293 | assert !@user.email?
294 | @user.email = 'test@example.com'
295 | assert @user.email?
296 | end
297 |
298 | end
299 |
--------------------------------------------------------------------------------
/test/active_record_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
4 |
5 | def create_tables
6 | ActiveRecord::Schema.define(version: 1) do
7 | self.verbose = false
8 | create_table :people do |t|
9 | t.string :encrypted_email
10 | t.string :password
11 | t.string :encrypted_credentials
12 | t.binary :salt
13 | t.binary :key_iv
14 | t.string :encrypted_email_salt
15 | t.string :encrypted_credentials_salt
16 | t.string :encrypted_email_iv
17 | t.string :encrypted_credentials_iv
18 | end
19 | create_table :accounts do |t|
20 | t.string :encrypted_password
21 | t.string :encrypted_password_iv
22 | t.string :encrypted_password_salt
23 | t.string :key
24 | end
25 | create_table :users do |t|
26 | t.string :login
27 | t.string :encrypted_password
28 | t.string :encrypted_password_iv
29 | t.boolean :is_admin
30 | end
31 | create_table :prime_ministers do |t|
32 | t.string :encrypted_name
33 | t.string :encrypted_name_iv
34 | end
35 | create_table :addresses do |t|
36 | t.binary :encrypted_street
37 | t.binary :encrypted_street_iv
38 | t.binary :encrypted_zipcode
39 | t.string :mode
40 | end
41 | end
42 | end
43 |
44 | # The table needs to exist before defining the class
45 | create_tables
46 |
47 | ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)
48 |
49 | if ::ActiveRecord::VERSION::STRING > "4.0"
50 | module Rack
51 | module Test
52 | class UploadedFile; end
53 | end
54 | end
55 |
56 | require 'action_controller/metal/strong_parameters'
57 | end
58 |
59 | class Person < ActiveRecord::Base
60 | self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
61 | attr_encrypted :email, key: SECRET_KEY
62 | attr_encrypted :credentials, key: Proc.new { |user| Encryptor.encrypt(value: user.salt, key: SECRET_KEY, iv: user.key_iv) }, marshal: true
63 |
64 | after_initialize :initialize_salt_and_credentials
65 |
66 | protected
67 |
68 | def initialize_salt_and_credentials
69 | self.key_iv ||= SecureRandom.random_bytes(12)
70 | self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15]
71 | self.credentials ||= { username: 'example', password: 'test' }
72 | end
73 | end
74 |
75 | class PersonWithValidation < Person
76 | validates_presence_of :email
77 | end
78 |
79 | class PersonWithProcMode < Person
80 | attr_encrypted :email, key: SECRET_KEY, mode: Proc.new { :per_attribute_iv_and_salt }
81 | attr_encrypted :credentials, key: SECRET_KEY, mode: Proc.new { :single_iv_and_salt }, insecure_mode: true
82 | end
83 |
84 | class Account < ActiveRecord::Base
85 | ACCOUNT_ENCRYPTION_KEY = SecureRandom.urlsafe_base64(24)
86 | attr_encrypted :password, key: :password_encryption_key
87 |
88 | def encrypting?(attr)
89 | encrypted_attributes[attr][:operation] == :encrypting
90 | end
91 |
92 | def password_encryption_key
93 | if encrypting?(:password)
94 | self.key = ACCOUNT_ENCRYPTION_KEY
95 | else
96 | self.key
97 | end
98 | end
99 | end
100 |
101 | class PersonWithSerialization < ActiveRecord::Base
102 | self.table_name = 'people'
103 | attr_encrypted :email, key: SECRET_KEY
104 | serialize :password
105 | end
106 |
107 | class UserWithProtectedAttribute < ActiveRecord::Base
108 | self.table_name = 'users'
109 | attr_encrypted :password, key: SECRET_KEY
110 | attr_protected :is_admin if ::ActiveRecord::VERSION::STRING < "4.0"
111 | end
112 |
113 | class PersonUsingAlias < ActiveRecord::Base
114 | self.table_name = 'people'
115 | attr_encryptor :email, key: SECRET_KEY
116 | end
117 |
118 | class PrimeMinister < ActiveRecord::Base
119 | attr_encrypted :name, marshal: true, key: SECRET_KEY
120 | end
121 |
122 | class Address < ActiveRecord::Base
123 | self.attr_encrypted_options[:marshal] = false
124 | self.attr_encrypted_options[:encode] = false
125 | attr_encrypted :street, encode_iv: false, key: SECRET_KEY
126 | attr_encrypted :zipcode, key: SECRET_KEY, mode: Proc.new { |address| address.mode.to_sym }, insecure_mode: true
127 | end
128 |
129 | class ActiveRecordTest < Minitest::Test
130 |
131 | def setup
132 | drop_all_tables
133 | create_tables
134 | end
135 |
136 | def test_should_encrypt_email
137 | @person = Person.create(email: 'test@example.com')
138 | refute_nil @person.encrypted_email
139 | refute_equal @person.email, @person.encrypted_email
140 | assert_equal @person.email, Person.first.email
141 | end
142 |
143 | def test_should_marshal_and_encrypt_credentials
144 | @person = Person.create
145 | refute_nil @person.encrypted_credentials
146 | refute_equal @person.credentials, @person.encrypted_credentials
147 | assert_equal @person.credentials, Person.first.credentials
148 | end
149 |
150 | def test_should_encode_by_default
151 | assert Person.attr_encrypted_options[:encode]
152 | end
153 |
154 | def test_should_validate_presence_of_email
155 | @person = PersonWithValidation.new
156 | assert !@person.valid?
157 | assert !@person.errors[:email].empty? || @person.errors.on(:email)
158 | end
159 |
160 | def test_should_encrypt_decrypt_with_iv
161 | @person = Person.create(email: 'test@example.com')
162 | @person2 = Person.find(@person.id)
163 | refute_nil @person2.encrypted_email_iv
164 | assert_equal 'test@example.com', @person2.email
165 | end
166 |
167 | def test_should_ensure_attributes_can_be_deserialized
168 | @person = PersonWithSerialization.new(email: 'test@example.com', password: %w(an array of strings))
169 | @person.save
170 | assert_equal @person.password, %w(an array of strings)
171 | end
172 |
173 | def test_should_create_an_account_regardless_of_arguments_order
174 | Account.create!(key: SECRET_KEY, password: "password")
175 | Account.create!(password: "password" , key: SECRET_KEY)
176 | end
177 |
178 | def test_should_set_attributes_regardless_of_arguments_order
179 | # minitest does not implement `assert_nothing_raised` https://github.com/seattlerb/minitest/issues/112
180 | Account.new.attributes = { password: "password", key: SECRET_KEY }
181 | end
182 |
183 | def test_should_create_changed_predicate
184 | person = Person.create!(email: 'test@example.com')
185 | refute person.email_changed?
186 | person.email = 'test@example.com'
187 | refute person.email_changed?
188 | person.email = nil
189 | assert person.email_changed?
190 | person.email = 'test2@example.com'
191 | assert person.email_changed?
192 | end
193 |
194 | def test_should_create_was_predicate
195 | original_email = 'test@example.com'
196 | person = Person.create!(email: original_email)
197 | assert_equal original_email, person.email_was
198 | person.email = 'test2@example.com'
199 | assert_equal original_email, person.email_was
200 | old_pm_name = "Winston Churchill"
201 | pm = PrimeMinister.create!(name: old_pm_name)
202 | assert_equal old_pm_name, pm.name_was
203 | old_zipcode = "90210"
204 | address = Address.create!(zipcode: old_zipcode, mode: "single_iv_and_salt")
205 | assert_equal old_zipcode, address.zipcode_was
206 | end
207 |
208 | def test_attribute_was_works_when_options_for_old_encrypted_value_are_different_than_options_for_new_encrypted_value
209 | pw = 'password'
210 | crypto_key = SecureRandom.urlsafe_base64(24)
211 | old_iv = SecureRandom.random_bytes(12)
212 | account = Account.create
213 | encrypted_value = Encryptor.encrypt(value: pw, iv: old_iv, key: crypto_key)
214 | Account.where(id: account.id).update_all(key: crypto_key, encrypted_password_iv: [old_iv].pack('m'), encrypted_password: [encrypted_value].pack('m'))
215 | account = Account.find(account.id)
216 | assert_equal pw, account.password
217 | account.password = pw.reverse
218 | assert_equal pw, account.password_was
219 | account.save
220 | account.reload
221 | assert_equal Account::ACCOUNT_ENCRYPTION_KEY, account.key
222 | assert_equal pw.reverse, account.password
223 | end
224 |
225 | if ::ActiveRecord::VERSION::STRING > "4.0"
226 | def test_should_assign_attributes
227 | @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
228 | @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login)
229 | assert_equal 'modified', @user.login
230 | end
231 |
232 | def test_should_not_assign_protected_attributes
233 | @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
234 | @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login)
235 | assert !@user.is_admin?
236 | end
237 |
238 | def test_should_raise_exception_if_not_permitted
239 | @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
240 | assert_raises ActiveModel::ForbiddenAttributesError do
241 | @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true)
242 | end
243 | end
244 |
245 | def test_should_raise_exception_on_init_if_not_permitted
246 | assert_raises ActiveModel::ForbiddenAttributesError do
247 | @user = UserWithProtectedAttribute.new ActionController::Parameters.new(login: 'modified', is_admin: true)
248 | end
249 | end
250 | else
251 | def test_should_assign_attributes
252 | @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
253 | @user.attributes = { login: 'modified', is_admin: true }
254 | assert_equal 'modified', @user.login
255 | end
256 |
257 | def test_should_not_assign_protected_attributes
258 | @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
259 | @user.attributes = { login: 'modified', is_admin: true }
260 | assert !@user.is_admin?
261 | end
262 |
263 | def test_should_assign_protected_attributes
264 | @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
265 | if ::ActiveRecord::VERSION::STRING > "3.1"
266 | @user.send(:assign_attributes, { login: 'modified', is_admin: true }, without_protection: true)
267 | else
268 | @user.send(:attributes=, { login: 'modified', is_admin: true }, false)
269 | end
270 | assert @user.is_admin?
271 | end
272 | end
273 |
274 | def test_should_allow_assignment_of_nil_attributes
275 | @person = Person.new
276 | assert_nil(@person.attributes = nil)
277 | end
278 |
279 | def test_should_allow_proc_based_mode
280 | @person = PersonWithProcMode.create(email: 'test@example.com', credentials: 'password123')
281 |
282 | # Email is :per_attribute_iv_and_salt
283 | assert_equal @person.class.encrypted_attributes[:email][:mode].class, Proc
284 | assert_equal @person.class.encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt
285 | refute_nil @person.encrypted_email_salt
286 | refute_nil @person.encrypted_email_iv
287 |
288 | # Credentials is :single_iv_and_salt
289 | assert_equal @person.class.encrypted_attributes[:credentials][:mode].class, Proc
290 | assert_equal @person.class.encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt
291 | assert_nil @person.encrypted_credentials_salt
292 | assert_nil @person.encrypted_credentials_iv
293 | end
294 |
295 | if ::ActiveRecord::VERSION::STRING > "3.1"
296 | def test_should_allow_assign_attributes_with_nil
297 | @person = Person.new
298 | assert_nil(@person.assign_attributes nil)
299 | end
300 | end
301 |
302 | def test_that_alias_encrypts_column
303 | user = PersonUsingAlias.new
304 | user.email = 'test@example.com'
305 | user.save
306 |
307 | refute_nil user.encrypted_email
308 | refute_equal user.email, user.encrypted_email
309 | assert_equal user.email, PersonUsingAlias.first.email
310 | end
311 |
312 | # See https://github.com/attr-encrypted/attr_encrypted/issues/68
313 | def test_should_invalidate_virtual_attributes_on_reload
314 | old_pm_name = 'Winston Churchill'
315 | new_pm_name = 'Neville Chamberlain'
316 | pm = PrimeMinister.create!(name: old_pm_name)
317 | assert_equal old_pm_name, pm.name
318 | pm.name = new_pm_name
319 | assert_equal new_pm_name, pm.name
320 |
321 | result = pm.reload
322 | assert_equal pm, result
323 | assert_equal old_pm_name, pm.name
324 | end
325 |
326 | def test_should_save_encrypted_data_as_binary
327 | street = '123 Elm'
328 | address = Address.create!(street: street)
329 | refute_equal address.encrypted_street, street
330 | assert_equal Address.first.street, street
331 | end
332 |
333 | def test_should_evaluate_proc_based_mode
334 | street = '123 Elm'
335 | zipcode = '12345'
336 | address = Address.new(street: street, zipcode: zipcode, mode: :single_iv_and_salt)
337 | assert_nil address.encrypted_zipcode_iv
338 | end
339 | end
340 |
--------------------------------------------------------------------------------
/test/attr_encrypted_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require_relative 'test_helper'
3 |
4 | class SillyEncryptor
5 | def self.silly_encrypt(options)
6 | (options[:value] + options[:some_arg]).reverse
7 | end
8 |
9 | def self.silly_decrypt(options)
10 | options[:value].reverse.gsub(/#{options[:some_arg]}$/, '')
11 | end
12 | end
13 |
14 | class User
15 | extend AttrEncrypted
16 | self.attr_encrypted_options[:key] = Proc.new { |user| SECRET_KEY } # default key
17 | self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
18 |
19 | attr_encrypted :email, :without_encoding, :key => SECRET_KEY
20 | attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test'
21 | attr_encrypted :ssn, :key => :secret_key, :attribute => 'ssn_encrypted'
22 | attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test'
23 | attr_encrypted :with_encoding, :key => SECRET_KEY, :encode => true
24 | attr_encrypted :with_custom_encoding, :key => SECRET_KEY, :encode => 'm'
25 | attr_encrypted :with_marshaling, :key => SECRET_KEY, :marshal => true
26 | attr_encrypted :with_true_if, :key => SECRET_KEY, :if => true, mode: :per_attribute_iv_and_salt
27 | attr_encrypted :with_false_if, :key => SECRET_KEY, :if => false, mode: :per_attribute_iv_and_salt
28 | attr_encrypted :with_true_unless, :key => SECRET_KEY, :unless => true, mode: :per_attribute_iv_and_salt
29 | attr_encrypted :with_false_unless, :key => SECRET_KEY, :unless => false, mode: :per_attribute_iv_and_salt
30 | attr_encrypted :with_if_changed, :key => SECRET_KEY, :if => :should_encrypt
31 | attr_encrypted :with_allow_empty_value, key: SECRET_KEY, allow_empty_value: true, marshal: true
32 |
33 | attr_encryptor :aliased, :key => SECRET_KEY
34 |
35 | attr_accessor :salt
36 | attr_accessor :should_encrypt
37 |
38 | def initialize(email: nil)
39 | self.email = email
40 | self.salt = Time.now.to_i.to_s
41 | self.should_encrypt = true
42 | end
43 |
44 | private
45 | def secret_key
46 | SECRET_KEY
47 | end
48 | end
49 |
50 | class Admin < User
51 | attr_encrypted :testing
52 | end
53 |
54 | class SomeOtherClass
55 | extend AttrEncrypted
56 | def self.call(object)
57 | object.class
58 | end
59 | end
60 |
61 | class YetAnotherClass
62 | extend AttrEncrypted
63 | self.attr_encrypted_options[:encode_iv] = false
64 |
65 | attr_encrypted :email, :key => SECRET_KEY
66 | attr_encrypted :phone_number, :key => SECRET_KEY, mode: Proc.new { |thing| thing.mode }, encode_iv: Proc.new { |thing| thing.encode_iv }, encode_salt: Proc.new { |thing| thing.encode_salt }
67 |
68 | def initialize(email: nil, encode_iv: 'm', encode_salt: 'm', mode: :per_attribute_iv_and_salt)
69 | self.email = email
70 | @encode_iv = encode_iv
71 | @encode_salt = encode_salt
72 | @mode = mode
73 | end
74 |
75 | attr_reader :encode_iv, :encode_salt, :mode
76 | end
77 |
78 | class AttrEncryptedTest < Minitest::Test
79 | def setup
80 | @iv = SecureRandom.random_bytes(12)
81 | end
82 |
83 | def test_should_store_email_in_encrypted_attributes
84 | assert User.encrypted_attributes.include?(:email)
85 | end
86 |
87 | def test_should_not_store_salt_in_encrypted_attributes
88 | refute User.encrypted_attributes.include?(:salt)
89 | end
90 |
91 | def test_attr_encrypted_should_return_true_for_email
92 | assert User.attr_encrypted?('email')
93 | end
94 |
95 | def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line
96 | refute_equal User.encrypted_attributes[:email][:attribute], User.encrypted_attributes[:without_encoding][:attribute]
97 | end
98 |
99 | def test_attr_encrypted_should_return_false_for_salt
100 | assert !User.attr_encrypted?('salt')
101 | end
102 |
103 | def test_should_generate_an_encrypted_attribute
104 | assert User.new.respond_to?(:encrypted_email)
105 | end
106 |
107 | def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix
108 | assert User.new.respond_to?(:crypted_password_test)
109 | end
110 |
111 | def test_should_generate_an_encrypted_attribute_with_the_attribute_option
112 | assert User.new.respond_to?(:ssn_encrypted)
113 | end
114 |
115 | def test_should_not_encrypt_nil_value
116 | assert_nil User.encrypt_email(nil, iv: @iv)
117 | end
118 |
119 | def test_should_not_encrypt_empty_string_by_default
120 | assert_equal '', User.encrypt_email('', iv: @iv)
121 | end
122 |
123 | def test_should_encrypt_email
124 | refute_nil User.encrypt_email('test@example.com', iv: @iv)
125 | refute_equal 'test@example.com', User.encrypt_email('test@example.com', iv: @iv)
126 | end
127 |
128 | def test_should_encrypt_email_when_modifying_the_attr_writer
129 | @user = User.new
130 | assert_nil @user.encrypted_email
131 | @user.email = 'test@example.com'
132 | refute_nil @user.encrypted_email
133 | iv = @user.encrypted_email_iv.unpack('m').first
134 | salt = @user.encrypted_email_salt[1..-1].unpack('m').first
135 | assert_equal User.encrypt_email('test@example.com', iv: iv, salt: salt), @user.encrypted_email
136 | end
137 |
138 | def test_should_not_decrypt_nil_value
139 | assert_nil User.decrypt_email(nil, iv: @iv)
140 | end
141 |
142 | def test_should_not_decrypt_empty_string
143 | assert_equal '', User.decrypt_email('', iv: @iv)
144 | end
145 |
146 | def test_should_decrypt_email
147 | encrypted_email = User.encrypt_email('test@example.com', iv: @iv)
148 | refute_equal 'test@test.com', encrypted_email
149 | assert_equal 'test@example.com', User.decrypt_email(encrypted_email, iv: @iv)
150 | end
151 |
152 | def test_should_decrypt_email_when_reading
153 | @user = User.new
154 | assert_nil @user.email
155 | options = @user.encrypted_attributes[:email]
156 | iv = @user.send(:generate_iv, options[:algorithm])
157 | encoded_iv = [iv].pack(options[:encode_iv])
158 | salt = SecureRandom.random_bytes
159 | encoded_salt = @user.send(:prefix_and_encode_salt, salt, options[:encode_salt])
160 | @user.encrypted_email = User.encrypt_email('test@example.com', iv: iv, salt: salt)
161 | @user.encrypted_email_iv = encoded_iv
162 | @user.encrypted_email_salt = encoded_salt
163 | assert_equal 'test@example.com', @user.email
164 | end
165 |
166 | def test_should_encrypt_with_encoding
167 | assert_equal User.encrypt_with_encoding('test', iv: @iv), [User.encrypt_without_encoding('test', iv: @iv)].pack('m')
168 | end
169 |
170 | def test_should_decrypt_with_encoding
171 | encrypted = User.encrypt_with_encoding('test', iv: @iv)
172 | assert_equal 'test', User.decrypt_with_encoding(encrypted, iv: @iv)
173 | assert_equal User.decrypt_with_encoding(encrypted, iv: @iv), User.decrypt_without_encoding(encrypted.unpack('m').first, iv: @iv)
174 | end
175 |
176 | def test_should_encrypt_with_custom_encoding
177 | assert_equal User.encrypt_with_encoding('test', iv: @iv), [User.encrypt_without_encoding('test', iv: @iv)].pack('m')
178 | end
179 |
180 | def test_should_decrypt_with_custom_encoding
181 | encrypted = User.encrypt_with_encoding('test', iv: @iv)
182 | assert_equal 'test', User.decrypt_with_encoding(encrypted, iv: @iv)
183 | assert_equal User.decrypt_with_encoding(encrypted, iv: @iv), User.decrypt_without_encoding(encrypted.unpack('m').first, iv: @iv)
184 | end
185 |
186 | def test_should_encrypt_with_marshaling
187 | @user = User.new
188 | @user.with_marshaling = [1, 2, 3]
189 | refute_nil @user.encrypted_with_marshaling
190 | end
191 |
192 | def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments
193 | assert_equal SillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), User.encrypt_credit_card('testing')
194 | end
195 |
196 | def test_should_evaluate_a_key_passed_as_a_symbol
197 | @user = User.new
198 | assert_nil @user.ssn_encrypted
199 | @user.ssn = 'testing'
200 | refute_nil @user.ssn_encrypted
201 | encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.ssn_encrypted_iv.unpack("m").first, :salt => @user.ssn_encrypted_salt.unpack("m").first )
202 | assert_equal encrypted, @user.ssn_encrypted
203 | end
204 |
205 | def test_should_evaluate_a_key_passed_as_a_proc
206 | @user = User.new
207 | assert_nil @user.crypted_password_test
208 | @user.password = 'testing'
209 | refute_nil @user.crypted_password_test
210 | encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.crypted_password_test_iv.unpack("m").first, :salt => @user.crypted_password_test_salt.unpack("m").first)
211 | assert_equal encrypted, @user.crypted_password_test
212 | end
213 |
214 | def test_should_use_options_found_in_the_attr_encrypted_options_attribute
215 | @user = User.new
216 | assert_nil @user.crypted_password_test
217 | @user.password = 'testing'
218 | refute_nil @user.crypted_password_test
219 | encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.crypted_password_test_iv.unpack("m").first, :salt => @user.crypted_password_test_salt.unpack("m").first)
220 | assert_equal encrypted, @user.crypted_password_test
221 | end
222 |
223 | def test_should_inherit_encrypted_attributes
224 | assert_equal [User.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.encrypted_attributes.keys.collect { |key| key.to_s }.sort
225 | end
226 |
227 | def test_should_inherit_attr_encrypted_options
228 | assert !User.attr_encrypted_options.empty?
229 | assert_equal User.attr_encrypted_options, Admin.attr_encrypted_options
230 | end
231 |
232 | def test_should_not_inherit_unrelated_attributes
233 | assert SomeOtherClass.attr_encrypted_options.empty?
234 | assert SomeOtherClass.encrypted_attributes.empty?
235 | end
236 |
237 | def test_should_evaluate_a_symbol_option
238 | assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, :class)
239 | end
240 |
241 | def test_should_evaluate_a_proc_option
242 | assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, proc { |object| object.class })
243 | end
244 |
245 | def test_should_evaluate_a_lambda_option
246 | assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, lambda { |object| object.class })
247 | end
248 |
249 | def test_should_evaluate_a_method_option
250 | assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, SomeOtherClass.method(:call))
251 | end
252 |
253 | def test_should_return_a_string_option
254 | class_string = 'SomeOtherClass'
255 | assert_equal class_string, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, class_string)
256 | end
257 |
258 | def test_should_encrypt_with_true_if
259 | @user = User.new
260 | assert_nil @user.encrypted_with_true_if
261 | @user.with_true_if = 'testing'
262 | refute_nil @user.encrypted_with_true_if
263 | encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.encrypted_with_true_if_iv.unpack("m").first, :salt => @user.encrypted_with_true_if_salt.unpack("m").first)
264 | assert_equal encrypted, @user.encrypted_with_true_if
265 | end
266 |
267 | def test_should_not_encrypt_with_false_if
268 | @user = User.new
269 | assert_nil @user.encrypted_with_false_if
270 | @user.with_false_if = 'testing'
271 | refute_nil @user.encrypted_with_false_if
272 | assert_equal 'testing', @user.encrypted_with_false_if
273 | end
274 |
275 | def test_should_encrypt_with_false_unless
276 | @user = User.new
277 | assert_nil @user.encrypted_with_false_unless
278 | @user.with_false_unless = 'testing'
279 | refute_nil @user.encrypted_with_false_unless
280 | encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.encrypted_with_false_unless_iv.unpack("m").first, :salt => @user.encrypted_with_false_unless_salt.unpack("m").first)
281 | assert_equal encrypted, @user.encrypted_with_false_unless
282 | end
283 |
284 | def test_should_not_encrypt_with_true_unless
285 | @user = User.new
286 | assert_nil @user.encrypted_with_true_unless
287 | @user.with_true_unless = 'testing'
288 | refute_nil @user.encrypted_with_true_unless
289 | assert_equal 'testing', @user.encrypted_with_true_unless
290 | end
291 |
292 | def test_should_encrypt_empty_with_truthy_allow_empty_value_option
293 | @user = User.new
294 | assert_nil @user.encrypted_with_allow_empty_value
295 | @user.with_allow_empty_value = ''
296 | refute_nil @user.encrypted_with_allow_empty_value
297 | assert_equal '', @user.with_allow_empty_value
298 | @user = User.new
299 | @user.with_allow_empty_value = nil
300 | refute_nil @user.encrypted_with_allow_empty_value
301 | assert_nil @user.with_allow_empty_value
302 | end
303 |
304 | def test_should_work_with_aliased_attr_encryptor
305 | assert User.encrypted_attributes.include?(:aliased)
306 | end
307 |
308 | def test_should_always_reset_options
309 | @user = User.new
310 | @user.with_if_changed = "encrypt_stuff"
311 |
312 | @user = User.new
313 | @user.should_encrypt = false
314 | @user.with_if_changed = "not_encrypted_stuff"
315 | assert_equal "not_encrypted_stuff", @user.with_if_changed
316 | assert_equal "not_encrypted_stuff", @user.encrypted_with_if_changed
317 | end
318 |
319 | def test_should_cast_values_as_strings_before_encrypting
320 | string_encrypted_email = User.encrypt_email('3', iv: @iv)
321 | assert_equal string_encrypted_email, User.encrypt_email(3, iv: @iv)
322 | assert_equal '3', User.decrypt_email(string_encrypted_email, iv: @iv)
323 | end
324 |
325 | def test_should_create_query_accessor
326 | @user = User.new
327 | assert !@user.email?
328 | @user.email = ''
329 | assert !@user.email?
330 | @user.email = 'test@example.com'
331 | assert @user.email?
332 | end
333 |
334 | def test_should_vary_iv_per_attribute
335 | @user = User.new
336 | @user.email = 'email@example.com'
337 | @user.password = 'p455w0rd'
338 | refute_equal @user.encrypted_email_iv, @user.crypted_password_test_iv
339 | end
340 |
341 | def test_should_generate_iv_per_attribute_by_default
342 | thing = YetAnotherClass.new(email: 'thing@thing.com')
343 | refute_nil thing.encrypted_email_iv
344 | end
345 |
346 | def test_should_vary_iv_per_instance
347 | @user1 = User.new
348 | @user1.email = 'email@example.com'
349 | @user2 = User.new
350 | @user2.email = 'email@example.com'
351 | refute_equal @user1.encrypted_email_iv, @user2.encrypted_email_iv
352 | refute_equal @user1.encrypted_email, @user2.encrypted_email
353 | end
354 |
355 | def test_should_vary_salt_per_attribute
356 | @user = User.new
357 | @user.email = 'email@example.com'
358 | @user.password = 'p455w0rd'
359 | refute_equal @user.encrypted_email_salt, @user.crypted_password_test_salt
360 | end
361 |
362 | def test_should_vary_salt_per_instance
363 | @user1 = User.new
364 | @user1.email = 'email@example.com'
365 | @user2 = User.new
366 | @user2.email = 'email@example.com'
367 | refute_equal @user1.encrypted_email_salt, @user2.encrypted_email_salt
368 | end
369 |
370 | def test_should_not_generate_salt_per_attribute_by_default
371 | thing = YetAnotherClass.new(email: 'thing@thing.com')
372 | assert_nil thing.encrypted_email_salt
373 | end
374 |
375 | def test_should_decrypt_second_record
376 | @user1 = User.new
377 | @user1.email = 'test@example.com'
378 |
379 | @user2 = User.new
380 | @user2.email = 'test@example.com'
381 |
382 | assert_equal 'test@example.com', @user1.decrypt(:email, @user1.encrypted_email)
383 | end
384 |
385 | def test_should_specify_the_default_algorithm
386 | assert YetAnotherClass.encrypted_attributes[:email][:algorithm]
387 | assert_equal YetAnotherClass.encrypted_attributes[:email][:algorithm], 'aes-256-gcm'
388 | end
389 |
390 | def test_should_not_encode_iv_when_encode_iv_is_false
391 | email = 'thing@thing.com'
392 | thing = YetAnotherClass.new(email: email)
393 | refute thing.encrypted_email_iv =~ base64_encoding_regex
394 | assert_equal thing.email, email
395 | end
396 |
397 | def test_should_base64_encode_iv_by_default
398 | phone_number = '555-555-5555'
399 | thing = YetAnotherClass.new
400 | thing.phone_number = phone_number
401 | assert thing.encrypted_phone_number_iv =~ base64_encoding_regex
402 | assert_equal thing.phone_number, phone_number
403 | end
404 |
405 | def test_should_generate_unique_iv_for_every_encrypt_operation
406 | user = User.new
407 | user.email = 'initial_value@test.com'
408 | original_iv = user.encrypted_email_iv
409 | user.email = 'revised_value@test.com'
410 | refute_equal original_iv, user.encrypted_email_iv
411 | end
412 |
413 | def test_should_not_generate_iv_for_attribute_when_if_option_is_false
414 | user = User.new
415 | user.with_false_if = 'derp'
416 | assert_nil user.encrypted_with_false_if_iv
417 | end
418 |
419 | def test_should_generate_iv_for_attribute_when_if_option_is_true
420 | user = User.new
421 | user.with_true_if = 'derp'
422 | refute_nil user.encrypted_with_true_if_iv
423 | end
424 |
425 | def test_should_not_generate_salt_for_attribute_when_if_option_is_false
426 | user = User.new
427 | user.with_false_if = 'derp'
428 | assert_nil user.encrypted_with_false_if_salt
429 | end
430 |
431 | def test_should_generate_salt_for_attribute_when_if_option_is_true
432 | user = User.new
433 | user.with_true_if = 'derp'
434 | refute_nil user.encrypted_with_true_if_salt
435 | end
436 |
437 | def test_should_generate_iv_for_attribute_when_unless_option_is_false
438 | user = User.new
439 | user.with_false_unless = 'derp'
440 | refute_nil user.encrypted_with_false_unless_iv
441 | end
442 |
443 | def test_should_not_generate_iv_for_attribute_when_unless_option_is_true
444 | user = User.new
445 | user.with_true_unless = 'derp'
446 | assert_nil user.encrypted_with_true_unless_iv
447 | end
448 |
449 | def test_should_generate_salt_for_attribute_when_unless_option_is_false
450 | user = User.new
451 | user.with_false_unless = 'derp'
452 | refute_nil user.encrypted_with_false_unless_salt
453 | end
454 |
455 | def test_should_not_generate_salt_for_attribute_when_unless_option_is_true
456 | user = User.new
457 | user.with_true_unless = 'derp'
458 | assert_nil user.encrypted_with_true_unless_salt
459 | end
460 |
461 | def test_should_not_by_default_generate_iv_when_attribute_is_empty
462 | user = User.new
463 | user.with_true_if = nil
464 | assert_nil user.encrypted_with_true_if_iv
465 | end
466 | end
467 |
--------------------------------------------------------------------------------
/lib/attr_encrypted.rb:
--------------------------------------------------------------------------------
1 | require 'encryptor'
2 |
3 | # Adds attr_accessors that encrypt and decrypt an object's attributes
4 | module AttrEncrypted
5 | autoload :Version, 'attr_encrypted/version'
6 |
7 | def self.extended(base) # :nodoc:
8 | base.class_eval do
9 | include InstanceMethods
10 | attr_writer :attr_encrypted_options
11 | @attr_encrypted_options, @encrypted_attributes = {}, {}
12 | end
13 | end
14 |
15 | # Generates attr_accessors that encrypt and decrypt attributes transparently
16 | #
17 | # Options (any other options you specify are passed to the Encryptor's encrypt and decrypt methods)
18 | #
19 | # attribute: The name of the referenced encrypted attribute. For example
20 | # attr_accessor :email, attribute: :ee would generate an
21 | # attribute named 'ee' to store the encrypted email. This is useful when defining
22 | # one attribute to encrypt at a time or when the :prefix and :suffix options
23 | # aren't enough.
24 | # Defaults to nil.
25 | #
26 | # prefix: A prefix used to generate the name of the referenced encrypted attributes.
27 | # For example attr_accessor :email, prefix: 'crypted_' would
28 | # generate attributes named 'crypted_email' to store the encrypted
29 | # email and password.
30 | # Defaults to 'encrypted_'.
31 | #
32 | # suffix: A suffix used to generate the name of the referenced encrypted attributes.
33 | # For example attr_accessor :email, prefix: '', suffix: '_encrypted'
34 | # would generate attributes named 'email_encrypted' to store the
35 | # encrypted email.
36 | # Defaults to ''.
37 | #
38 | # key: The encryption key. This option may not be required if
39 | # you're using a custom encryptor. If you pass a symbol
40 | # representing an instance method then the :key option
41 | # will be replaced with the result of the method before
42 | # being passed to the encryptor. Objects that respond
43 | # to :call are evaluated as well (including procs).
44 | # Any other key types will be passed directly to the encryptor.
45 | # Defaults to nil.
46 | #
47 | # encode: If set to true, attributes will be encoded as well as
48 | # encrypted. This is useful if you're planning on storing
49 | # the encrypted attributes in a database. The default
50 | # encoding is 'm' (base64), however this can be overwritten
51 | # by setting the :encode option to some other encoding
52 | # string instead of just 'true'. See
53 | # http://www.ruby-doc.org/core/classes/Array.html#M002245
54 | # for more encoding directives.
55 | # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
56 | #
57 | # encode_iv: Defaults to true.
58 |
59 | # encode_salt: Defaults to true.
60 | #
61 | # default_encoding: Defaults to 'm' (base64).
62 | #
63 | # marshal: If set to true, attributes will be marshaled as well
64 | # as encrypted. This is useful if you're planning on
65 | # encrypting something other than a string.
66 | # Defaults to false.
67 | #
68 | # marshaler: The object to use for marshaling.
69 | # Defaults to Marshal.
70 | #
71 | # dump_method: The dump method name to call on the :marshaler object to.
72 | # Defaults to 'dump'.
73 | #
74 | # load_method: The load method name to call on the :marshaler object.
75 | # Defaults to 'load'.
76 | #
77 | # encryptor: The object to use for encrypting.
78 | # Defaults to Encryptor.
79 | #
80 | # encrypt_method: The encrypt method name to call on the :encryptor object.
81 | # Defaults to 'encrypt'.
82 | #
83 | # decrypt_method: The decrypt method name to call on the :encryptor object.
84 | # Defaults to 'decrypt'.
85 | #
86 | # if: Attributes are only encrypted if this option evaluates
87 | # to true. If you pass a symbol representing an instance
88 | # method then the result of the method will be evaluated.
89 | # Any objects that respond to :call are evaluated as well.
90 | # Defaults to true.
91 | #
92 | # unless: Attributes are only encrypted if this option evaluates
93 | # to false. If you pass a symbol representing an instance
94 | # method then the result of the method will be evaluated.
95 | # Any objects that respond to :call are evaluated as well.
96 | # Defaults to false.
97 | #
98 | # mode: Selects encryption mode for attribute: choose :single_iv_and_salt for compatibility
99 | # with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used.
100 | # The :per_attribute_iv_and_salt mode uses a per-attribute IV and salt. The salt is used to derive a unique key per attribute.
101 | # A :per_attribute_iv mode derives a unique IV per attribute; salt is not used.
102 | # Defaults to :per_attribute_iv.
103 | #
104 | # allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option
105 | # has a truthy value.
106 | #
107 | # You can specify your own default options
108 | #
109 | # class User
110 | # # Now all attributes will be encoded and marshaled by default
111 | # attr_encrypted_options.merge!(encode: true, marshal: true, some_other_option: true)
112 | # attr_encrypted :configuration, key: 'my secret key'
113 | # end
114 | #
115 | #
116 | # Example
117 | #
118 | # class User
119 | # attr_encrypted :email, key: 'some secret key'
120 | # attr_encrypted :configuration, key: 'some other secret key', marshal: true
121 | # end
122 | #
123 | # @user = User.new
124 | # @user.encrypted_email # nil
125 | # @user.email? # false
126 | # @user.email = 'test@example.com'
127 | # @user.email? # true
128 | # @user.encrypted_email # returns the encrypted version of 'test@example.com'
129 | #
130 | # @user.configuration = { time_zone: 'UTC' }
131 | # @user.encrypted_configuration # returns the encrypted version of configuration
132 | #
133 | # See README for more examples
134 | def attr_encrypted(*attributes)
135 | options = attributes.last.is_a?(Hash) ? attributes.pop : {}
136 | options = attr_encrypted_default_options.dup.merge!(attr_encrypted_options).merge!(options)
137 |
138 | options[:encode] = options[:default_encoding] if options[:encode] == true
139 | options[:encode_iv] = options[:default_encoding] if options[:encode_iv] == true
140 | options[:encode_salt] = options[:default_encoding] if options[:encode_salt] == true
141 |
142 | attributes.each do |attribute|
143 | encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
144 |
145 | instance_methods_as_symbols = attribute_instance_methods_as_symbols
146 | attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
147 | attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
148 |
149 | iv_name = "#{encrypted_attribute_name}_iv".to_sym
150 | attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
151 | attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
152 |
153 | salt_name = "#{encrypted_attribute_name}_salt".to_sym
154 | attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
155 | attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
156 |
157 | define_method(attribute) do
158 | instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
159 | end
160 |
161 | define_method("#{attribute}=") do |value|
162 | send("#{encrypted_attribute_name}=", encrypt(attribute, value))
163 | instance_variable_set("@#{attribute}", value)
164 | end
165 |
166 | define_method("#{attribute}?") do
167 | value = send(attribute)
168 | value.respond_to?(:empty?) ? !value.empty? : !!value
169 | end
170 |
171 | encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
172 | end
173 | end
174 |
175 | alias_method :attr_encryptor, :attr_encrypted
176 |
177 | # Default options to use with calls to attr_encrypted
178 | #
179 | # It will inherit existing options from its superclass
180 | def attr_encrypted_options
181 | @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
182 | end
183 |
184 | def attr_encrypted_default_options
185 | {
186 | prefix: 'encrypted_',
187 | suffix: '',
188 | if: true,
189 | unless: false,
190 | encode: false,
191 | encode_iv: true,
192 | encode_salt: true,
193 | default_encoding: 'm',
194 | marshal: false,
195 | marshaler: Marshal,
196 | dump_method: 'dump',
197 | load_method: 'load',
198 | encryptor: Encryptor,
199 | encrypt_method: 'encrypt',
200 | decrypt_method: 'decrypt',
201 | mode: :per_attribute_iv,
202 | algorithm: 'aes-256-gcm',
203 | allow_empty_value: false,
204 | }
205 | end
206 |
207 | private :attr_encrypted_default_options
208 |
209 | # Checks if an attribute is configured with attr_encrypted
210 | #
211 | # Example
212 | #
213 | # class User
214 | # attr_accessor :name
215 | # attr_encrypted :email
216 | # end
217 | #
218 | # User.attr_encrypted?(:name) # false
219 | # User.attr_encrypted?(:email) # true
220 | def attr_encrypted?(attribute)
221 | encrypted_attributes.has_key?(attribute.to_sym)
222 | end
223 |
224 | # Decrypts a value for the attribute specified
225 | #
226 | # Example
227 | #
228 | # class User
229 | # attr_encrypted :email
230 | # end
231 | #
232 | # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
233 | def decrypt(attribute, encrypted_value, options = {})
234 | options = encrypted_attributes[attribute.to_sym].merge(options)
235 | if options[:if] && !options[:unless] && not_empty?(encrypted_value)
236 | encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
237 | value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
238 | if options[:marshal]
239 | value = options[:marshaler].send(options[:load_method], value)
240 | elsif defined?(Encoding)
241 | encoding = Encoding.default_internal || Encoding.default_external
242 | value = value.force_encoding(encoding.name)
243 | end
244 | value
245 | else
246 | encrypted_value
247 | end
248 | end
249 |
250 | # Encrypts a value for the attribute specified
251 | #
252 | # Example
253 | #
254 | # class User
255 | # attr_encrypted :email
256 | # end
257 | #
258 | # encrypted_email = User.encrypt(:email, 'test@example.com')
259 | def encrypt(attribute, value, options = {})
260 | options = encrypted_attributes[attribute.to_sym].merge(options)
261 | if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
262 | value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
263 | encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
264 | encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
265 | encrypted_value
266 | else
267 | value
268 | end
269 | end
270 |
271 | def not_empty?(value)
272 | !value.nil? && !(value.is_a?(String) && value.empty?)
273 | end
274 |
275 | # Contains a hash of encrypted attributes with virtual attribute names as keys
276 | # and their corresponding options as values
277 | #
278 | # Example
279 | #
280 | # class User
281 | # attr_encrypted :email, key: 'my secret key'
282 | # end
283 | #
284 | # User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
285 | def encrypted_attributes
286 | @encrypted_attributes ||= superclass.encrypted_attributes.dup
287 | end
288 |
289 | # Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
290 | # if attribute was configured with attr_encrypted
291 | #
292 | # Example
293 | #
294 | # class User
295 | # attr_encrypted :email, key: 'my secret key'
296 | # end
297 | #
298 | # User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
299 | def method_missing(method, *arguments, &block)
300 | if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
301 | send($1, $3, *arguments)
302 | else
303 | super
304 | end
305 | end
306 |
307 | module InstanceMethods
308 | # Decrypts a value for the attribute specified using options evaluated in the current object's scope
309 | #
310 | # Example
311 | #
312 | # class User
313 | # attr_accessor :secret_key
314 | # attr_encrypted :email, key: :secret_key
315 | #
316 | # def initialize(secret_key)
317 | # self.secret_key = secret_key
318 | # end
319 | # end
320 | #
321 | # @user = User.new('some-secret-key')
322 | # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
323 | def decrypt(attribute, encrypted_value)
324 | encrypted_attributes[attribute.to_sym][:operation] = :decrypting
325 | encrypted_attributes[attribute.to_sym][:value_present] = (encrypted_value && !encrypted_value.empty?)
326 | self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
327 | end
328 |
329 | # Encrypts a value for the attribute specified using options evaluated in the current object's scope
330 | #
331 | # Example
332 | #
333 | # class User
334 | # attr_accessor :secret_key
335 | # attr_encrypted :email, key: :secret_key
336 | #
337 | # def initialize(secret_key)
338 | # self.secret_key = secret_key
339 | # end
340 | # end
341 | #
342 | # @user = User.new('some-secret-key')
343 | # @user.encrypt(:email, 'test@example.com')
344 | def encrypt(attribute, value)
345 | encrypted_attributes[attribute.to_sym][:operation] = :encrypting
346 | encrypted_attributes[attribute.to_sym][:value_present] = (value && !value.empty?)
347 | self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
348 | end
349 |
350 | # Copies the class level hash of encrypted attributes with virtual attribute names as keys
351 | # and their corresponding options as values to the instance
352 | #
353 | def encrypted_attributes
354 | @encrypted_attributes ||= self.class.encrypted_attributes.dup
355 | end
356 |
357 | protected
358 |
359 | # Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
360 | def evaluated_attr_encrypted_options_for(attribute)
361 | evaluated_options = Hash.new
362 | attribute_option_value = encrypted_attributes[attribute.to_sym][:attribute]
363 | encrypted_attributes[attribute.to_sym].map do |option, value|
364 | evaluated_options[option] = evaluate_attr_encrypted_option(value)
365 | end
366 |
367 | evaluated_options[:attribute] = attribute_option_value
368 |
369 | evaluated_options.tap do |options|
370 | if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
371 | unless options[:mode] == :single_iv_and_salt
372 | load_iv_for_attribute(attribute, options)
373 | end
374 |
375 | if options[:mode] == :per_attribute_iv_and_salt
376 | load_salt_for_attribute(attribute, options)
377 | end
378 | end
379 | end
380 | end
381 |
382 | # Evaluates symbol (method reference) or proc (responds to call) options
383 | #
384 | # If the option is not a symbol or proc then the original option is returned
385 | def evaluate_attr_encrypted_option(option)
386 | if option.is_a?(Symbol) && respond_to?(option, true)
387 | send(option)
388 | elsif option.respond_to?(:call)
389 | option.call(self)
390 | else
391 | option
392 | end
393 | end
394 |
395 | def load_iv_for_attribute(attribute, options)
396 | encrypted_attribute_name = options[:attribute]
397 | encode_iv = options[:encode_iv]
398 | iv = options[:iv] || send("#{encrypted_attribute_name}_iv")
399 | if options[:operation] == :encrypting
400 | begin
401 | iv = generate_iv(options[:algorithm])
402 | iv = [iv].pack(encode_iv) if encode_iv
403 | send("#{encrypted_attribute_name}_iv=", iv)
404 | rescue RuntimeError
405 | end
406 | end
407 | if iv && !iv.empty?
408 | iv = iv.unpack(encode_iv).first if encode_iv
409 | options[:iv] = iv
410 | end
411 | end
412 |
413 | def generate_iv(algorithm)
414 | algo = OpenSSL::Cipher.new(algorithm)
415 | algo.encrypt
416 | algo.random_iv
417 | end
418 |
419 | def load_salt_for_attribute(attribute, options)
420 | encrypted_attribute_name = options[:attribute]
421 | encode_salt = options[:encode_salt]
422 | salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
423 | if options[:operation] == :encrypting
424 | salt = SecureRandom.random_bytes
425 | salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
426 | send("#{encrypted_attribute_name}_salt=", salt)
427 | end
428 | if salt && !salt.empty?
429 | salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt
430 | options[:salt] = salt
431 | end
432 | end
433 |
434 | def prefix_and_encode_salt(salt, encoding)
435 | prefix = '_'
436 | prefix + [salt].pack(encoding)
437 | end
438 |
439 | def decode_salt_if_encoded(salt, encoding)
440 | prefix = '_'
441 | salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt
442 | end
443 | end
444 |
445 | protected
446 |
447 | def attribute_instance_methods_as_symbols
448 | instance_methods.collect { |method| method.to_sym }
449 | end
450 |
451 | end
452 |
453 |
454 | Dir[File.join(File.dirname(__FILE__), 'attr_encrypted', 'adapters', '*.rb')].each { |adapter| require adapter }
455 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # attr_encrypted
2 | [](https://travis-ci.org/attr-encrypted/attr_encrypted) [](https://codeclimate.com/github/attr-encrypted/attr_encrypted/coverage) [](https://codeclimate.com/github/attr-encrypted/attr_encrypted) [](https://badge.fury.io/rb/attr_encrypted) [](https://hakiri.io/github/attr-encrypted/attr_encrypted/master)
3 |
4 | Generates attr_accessors that transparently encrypt and decrypt attributes.
5 |
6 | It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord`, `DataMapper`, or `Sequel`.
7 |
8 |
9 | ## Installation
10 |
11 | Add attr_encrypted to your gemfile:
12 |
13 | ```ruby
14 | gem "attr_encrypted", "~> 3.0.0"
15 | ```
16 |
17 | Then install the gem:
18 |
19 | ```bash
20 | bundle install
21 | ```
22 |
23 | ## Usage
24 |
25 | If you're using an ORM like `ActiveRecord`, `DataMapper`, or `Sequel`, using attr_encrypted is easy:
26 |
27 | ```ruby
28 | class User
29 | attr_encrypted :ssn, key: 'This is a key that is 256 bits!!'
30 | end
31 | ```
32 |
33 | If you're using a PORO, you have to do a little bit more work by extending the class:
34 |
35 | ```ruby
36 | class User
37 | extend AttrEncrypted
38 | attr_accessor :name
39 | attr_encrypted :ssn, key: 'This is a key that is 256 bits!!'
40 |
41 | def load
42 | # loads the stored data
43 | end
44 |
45 | def save
46 | # saves the :name and :encrypted_ssn attributes somewhere (e.g. filesystem, database, etc)
47 | end
48 | end
49 |
50 | user = User.new
51 | user.ssn = '123-45-6789'
52 | user.ssn # returns the unencrypted object ie. '123-45-6789'
53 | user.encrypted_ssn # returns the encrypted version of :ssn
54 | user.save
55 |
56 | user = User.load
57 | user.ssn # decrypts :encrypted_ssn and returns '123-45-6789'
58 | ```
59 |
60 | ### Encrypt/decrypt attribute class methods
61 |
62 | Two class methods are available for each attribute: `User.encrypt_email` and `User.decrypt_email`. They accept as arguments the same options that the `attr_encrypted` class method accepts. For example:
63 |
64 | ```ruby
65 | key = SecureRandom.random_bytes(32)
66 | iv = SecureRandom.random_bytes(12)
67 | encrypted_email = User.encrypt_email('test@test.com', iv: iv, key: key)
68 | email = User.decrypt_email(encrypted_email, iv: iv, key: key)
69 | ```
70 |
71 | The `attr_encrypted` class method is also aliased as `attr_encryptor` to conform to Ruby's `attr_` naming conventions. I should have called this project `attr_encryptor` but it was too late when I realized it ='(.
72 |
73 | ### attr_encrypted with database persistence
74 |
75 | By default, `attr_encrypted` uses the `:per_attribute_iv` encryption mode. This mode requires a column to store your cipher text and a column to store your IV.
76 |
77 | Create or modify the table that your model uses to add a column with the `encrypted_` prefix (which can be modified, see below), e.g. `encrypted_ssn` via a migration like the following:
78 |
79 | ```ruby
80 | create_table :users do |t|
81 | t.string :name
82 | t.string :encrypted_ssn
83 | t.string :encrypted_ssn_iv
84 | t.timestamps
85 | end
86 | ```
87 |
88 | You can use a string or binary column type. (See the encode option section below for more info)
89 |
90 | ### Specifying the encrypted attribute name
91 |
92 | 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. You have a couple of options if you want to name your attribute or db column something else, see below for more details.
93 |
94 |
95 | ## attr_encrypted options
96 |
97 | #### Options are evaluated
98 | All options will be evaluated at the instance level. If you pass in a symbol it will be passed as a message to the instance of your class. If you pass a proc or any object that responds to `:call` it will be called. You can pass in the instance of your class as an argument to the proc. Anything else will be returned. For example:
99 |
100 | ##### Symbols representing instance methods
101 |
102 | Here is an example class that uses an instance method to determines the encryption key to use.
103 |
104 | ```ruby
105 | class User
106 | attr_encrypted :email, key: :encryption_key
107 |
108 | def encryption_key
109 | # does some fancy logic and returns an encryption key
110 | end
111 | end
112 | ```
113 |
114 |
115 | ##### Procs
116 |
117 | Here is an example of passing a proc/lambda object as the `:key` option as well:
118 |
119 | ```ruby
120 | class User
121 | attr_encrypted :email, key: proc { |user| user.key }
122 | end
123 | ```
124 |
125 |
126 | ### Default options
127 |
128 | The following are the default options used by `attr_encrypted`:
129 |
130 | ```ruby
131 | prefix: 'encrypted_',
132 | suffix: '',
133 | if: true,
134 | unless: false,
135 | encode: false,
136 | encode_iv: true,
137 | encode_salt: true,
138 | default_encoding: 'm',
139 | marshal: false,
140 | marshaler: Marshal,
141 | dump_method: 'dump',
142 | load_method: 'load',
143 | encryptor: Encryptor,
144 | encrypt_method: 'encrypt',
145 | decrypt_method: 'decrypt',
146 | mode: :per_attribute_iv,
147 | algorithm: 'aes-256-gcm',
148 | allow_empty_value: false
149 | ```
150 |
151 | All of the aforementioned options are explained in depth below.
152 |
153 | Additionally, you can specify default options for all encrypted attributes in your class. Instead of having to define your class like this:
154 |
155 | ```ruby
156 | class User
157 | attr_encrypted :email, key: 'This is a key that is 256 bits!!', prefix: '', suffix: '_crypted'
158 | attr_encrypted :ssn, key: 'a different secret key', prefix: '', suffix: '_crypted'
159 | attr_encrypted :credit_card, key: 'another secret key', prefix: '', suffix: '_crypted'
160 | end
161 | ```
162 |
163 | You can simply define some default options like so:
164 |
165 | ```ruby
166 | class User
167 | attr_encrypted_options.merge!(prefix: '', :suffix => '_crypted')
168 | attr_encrypted :email, key: 'This is a key that is 256 bits!!'
169 | attr_encrypted :ssn, key: 'a different secret key'
170 | attr_encrypted :credit_card, key: 'another secret key'
171 | end
172 | ```
173 |
174 | This should help keep your classes clean and DRY.
175 |
176 | ### The `:attribute` option
177 |
178 | You can simply pass the name of the encrypted attribute as the `:attribute` option:
179 |
180 | ```ruby
181 | class User
182 | attr_encrypted :email, key: 'This is a key that is 256 bits!!', attribute: 'email_encrypted'
183 | end
184 | ```
185 |
186 | This would generate an attribute named `email_encrypted`
187 |
188 |
189 | ### The `:prefix` and `:suffix` options
190 |
191 | If you don't like the `encrypted_#{attribute}` naming convention then you can specify your own:
192 |
193 | ```ruby
194 | class User
195 | attr_encrypted :email, key: 'This is a key that is 256 bits!!', prefix: 'secret_', suffix: '_crypted'
196 | end
197 | ```
198 |
199 | This would generate the following attribute: `secret_email_crypted`.
200 |
201 |
202 | ### The `:key` option
203 |
204 | The `:key` option is used to pass in a data encryption key to be used with whatever encryption class you use. If you're using `Encryptor`, the key must meet minimum length requirements respective to the algorithm that you use; aes-256 requires a 256 bit key, etc. The `:key` option is not required (see custom encryptor below).
205 |
206 |
207 | ##### Unique keys for each attribute
208 |
209 | You can specify unique keys for each attribute if you'd like:
210 |
211 | ```ruby
212 | class User
213 | attr_encrypted :email, key: 'This is a key that is 256 bits!!'
214 | attr_encrypted :ssn, key: 'a different secret key'
215 | end
216 | ```
217 |
218 | It is recommended to use a symbol or a proc for the key and to store information regarding what key was used to encrypt your data. (See below for more details.)
219 |
220 |
221 | ### The `:if` and `:unless` options
222 |
223 | 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 attributes when you're in development mode. You can specify conditions like this:
224 |
225 | ```ruby
226 | class User < ActiveRecord::Base
227 | attr_encrypted :email, key: 'This is a key that is 256 bits!!', unless: Rails.env.development?
228 | attr_encrypted :ssn, key: 'This is a key that is 256 bits!!', if: Rails.env.development?
229 | end
230 | ```
231 |
232 | You can specify both `:if` and `:unless` options.
233 |
234 |
235 | ### The `:encryptor`, `:encrypt_method`, and `:decrypt_method` options
236 |
237 | The `Encryptor` class is used by default. You may use your own custom encryptor by specifying the `:encryptor`, `:encrypt_method`, and `:decrypt_method` options.
238 |
239 | Lets suppose you'd like to use this custom encryptor class:
240 |
241 | ```ruby
242 | class SillyEncryptor
243 | def self.silly_encrypt(options)
244 | (options[:value] + options[:secret_key]).reverse
245 | end
246 |
247 | def self.silly_decrypt(options)
248 | options[:value].reverse.gsub(/#{options[:secret_key]}$/, '')
249 | end
250 | end
251 | ```
252 |
253 | Simply set up your class like so:
254 |
255 | ```ruby
256 | class User
257 | attr_encrypted :email, secret_key: 'This is a key that is 256 bits!!', encryptor: SillyEncryptor, encrypt_method: :silly_encrypt, decrypt_method: :silly_decrypt
258 | end
259 | ```
260 |
261 | Any options that you pass to `attr_encrypted` will be passed to the encryptor class along with the `:value` option which contains the string to encrypt/decrypt. Notice that the above example uses `:secret_key` instead of `:key`. See [encryptor](https://github.com/attr-encrypted/encryptor) for more info regarding the default encryptor class.
262 |
263 |
264 | ### The `:mode` option
265 |
266 | The mode options allows you to specify in what mode your data will be encrypted. There are currently three modes: `:per_attribute_iv`, `:per_attribute_iv_and_salt`, and `:single_iv_and_salt`.
267 |
268 | __NOTE: `:per_attribute_iv_and_salt` and `:single_iv_and_salt` modes are deprecated and will be removed in the next major release.__
269 |
270 |
271 | ### The `:algorithm` option
272 |
273 | The default `Encryptor` class uses the standard ruby OpenSSL library. Its default algorithm is `aes-256-gcm`. You can modify this by passing the `:algorithm` option to the `attr_encrypted` call like so:
274 |
275 | ```ruby
276 | class User
277 | attr_encrypted :email, key: 'This is a key that is 256 bits!!', algorithm: 'aes-256-cbc'
278 | end
279 | ```
280 |
281 | To view a list of all cipher algorithms that are supported on your platform, run the following code in your favorite Ruby REPL:
282 |
283 | ```ruby
284 | require 'openssl'
285 | puts OpenSSL::Cipher.ciphers
286 | ```
287 | See [Encryptor](https://github.com/attr-encrypted/encryptor#algorithms) for more information.
288 |
289 |
290 | ### The `:encode`, `:encode_iv`, `:encode_salt`, and `:default_encoding` options
291 |
292 | You're probably going to be storing your encrypted attributes somehow (e.g. filesystem, database, etc). You can simply pass the `:encode` option to automatically encode/decode when encrypting/decrypting. The default behavior assumes that you're using a string column type and will base64 encode your cipher text. If you choose to use the binary column type then encoding is not required, but be sure to pass in `false` with the `:encode` option.
293 |
294 | ```ruby
295 | class User
296 | attr_encrypted :email, key: 'some secret key', encode: true, encode_iv: true, encode_salt: true
297 | end
298 | ```
299 |
300 | The default encoding is `m` (base64). You can change this by setting `encode: 'some encoding'`. See [`Array#pack`](http://ruby-doc.org/core-2.3.0/Array.html#method-i-pack) for more encoding options.
301 |
302 |
303 | ### The `:marshal`, `:dump_method`, and `:load_method` options
304 |
305 | 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.
306 |
307 | ```ruby
308 | class User
309 | attr_encrypted :credentials, key: 'some secret key', marshal: true
310 | end
311 | ```
312 |
313 | You may also optionally specify `:marshaler`, `:dump_method`, and `:load_method` if you want to use something other than the default `Marshal` object.
314 |
315 | ### The `:allow_empty_value` option
316 |
317 | You may want to encrypt empty strings or nil so as to not reveal which records are populated and which records are not.
318 |
319 | ```ruby
320 | class User
321 | attr_encrypted :credentials, key: 'some secret key', marshal: true, allow_empty_value: true
322 | end
323 | ```
324 |
325 |
326 | ## ORMs
327 |
328 | ### ActiveRecord
329 |
330 | If you're using this gem with `ActiveRecord`, you get a few extra features:
331 |
332 | #### Default options
333 |
334 | The `:encode` option is set to true by default.
335 |
336 | #### Dynamic `find_by_` and `scoped_by_` methods
337 |
338 | 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:
339 |
340 | ```ruby
341 | class User < ActiveRecord::Base
342 | attr_encrypted :email, key: 'This is a key that is 256 bits!!'
343 | attr_encrypted :password, key: 'some other secret key'
344 | end
345 | ```
346 |
347 | You can now lookup and login users like so:
348 |
349 | ```ruby
350 | User.find_by_email_and_password('test@example.com', 'testing')
351 | ```
352 |
353 | The call to `find_by_email_and_password` is intercepted and modified to `find_by_encrypted_email_and_encrypted_password('ENCRYPTED EMAIL', 'ENCRYPTED PASSWORD')`. The dynamic scope methods like `scoped_by_email_and_password` work the same way.
354 |
355 | NOTE: This only works if all records are encrypted with the same encryption key (per attribute).
356 |
357 | __NOTE: This feature is deprecated and will be removed in the next major release.__
358 |
359 |
360 | ### DataMapper and Sequel
361 |
362 | #### Default options
363 |
364 | The `:encode` option is set to true by default.
365 |
366 |
367 | ## Deprecations
368 |
369 | attr_encrypted v2.0.0 now depends on encryptor v2.0.0. As part of both major releases many insecure defaults and behaviors have been deprecated. The new default behavior is as follows:
370 |
371 | * Default `:mode` is now `:per_attribute_iv`, the default `:mode` in attr_encrypted v1.x was `:single_iv_and_salt`.
372 | * Default `:algorithm` is now 'aes-256-gcm', the default `:algorithm` in attr_encrypted v1.x was 'aes-256-cbc'.
373 | * The encryption key provided must be of appropriate length respective to the algorithm used. Previously, encryptor did not verify minimum key length.
374 | * The dynamic finders available in ActiveRecord will only work with `:single_iv_and_salt` mode. It is strongly advised that you do not use this mode. If you can search the encrypted data, it wasn't encrypted securely. This functionality will be deprecated in the next major release.
375 | * `:per_attribute_iv_and_salt` and `:single_iv_and_salt` modes are deprecated and will be removed in the next major release.
376 |
377 | Backwards compatibility is supported by providing a special option that is passed to encryptor, namely, `:insecure_mode`:
378 |
379 | ```ruby
380 | class User
381 | attr_encrypted :email, key: 'a secret key', algorithm: 'aes-256-cbc', mode: :single_iv_and_salt, insecure_mode: true
382 | end
383 | ```
384 |
385 | The `:insecure_mode` option will allow encryptor to ignore the new security requirements. It is strongly advised that if you use this older insecure behavior that you migrate to the newer more secure behavior.
386 |
387 |
388 | ## Upgrading from attr_encrypted v1.x to v3.x
389 |
390 | Modify your gemfile to include the new version of attr_encrypted:
391 |
392 | ```ruby
393 | gem attr_encrypted, "~> 3.0.0"
394 | ```
395 |
396 | The update attr_encrypted:
397 |
398 | ```bash
399 | bundle update attr_encrypted
400 | ```
401 |
402 | Then modify your models using attr\_encrypted to account for the changes in default options. Specifically, pass in the `:mode` and `:algorithm` options that you were using if you had not previously done so. If your key is insufficient length relative to the algorithm that you use, you should also pass in `insecure_mode: true`; this will prevent Encryptor from raising an exception regarding insufficient key length. Please see the Deprecations sections for more details including an example of how to specify your model with default options from attr_encrypted v1.x.
403 |
404 | ## Upgrading from attr_encrypted v2.x to v3.x
405 |
406 | A bug was discovered in Encryptor v2.0.0 that inccorectly set the IV when using an AES-\*-GCM algorithm. Unfornately fixing this major security issue results in the inability to decrypt records encrypted using an AES-*-GCM algorithm from Encryptor v2.0.0. Please see [Upgrading to Encryptor v3.0.0](https://github.com/attr-encrypted/encryptor#upgrading-from-v200-to-v300) for more info.
407 |
408 | It is strongly advised that you re-encrypt your data encrypted with Encryptor v2.0.0. However, you'll have to take special care to re-encrypt. To decrypt data encrypted with Encryptor v2.0.0 using an AES-\*-GCM algorithm you can use the `:v2_gcm_iv` option.
409 |
410 | It is recommended that you implement a strategy to insure that you do not mix the encryption implementations of Encryptor. One way to do this is to re-encrypt everything while your application is offline.Another way is to add a column that keeps track of what implementation was used. The path that you choose will depend on your situtation. Below is an example of how you might go about re-encrypting your data.
411 |
412 | ```ruby
413 | class User
414 | attr_encrypted :ssn, key: :encryption_key, v2_gcm_iv: is_decrypting?(:ssn)
415 |
416 | def is_decrypting?(attribute)
417 | encrypted_attributes[attribute][:operation] == :decrypting
418 | end
419 | end
420 |
421 | User.all.each do |user|
422 | old_ssn = user.ssn
423 | user.ssn= old_ssn
424 | user.save
425 | end
426 | ```
427 |
428 | ## Things to consider before using attr_encrypted
429 |
430 | #### Searching, joining, etc
431 | While choosing to encrypt at the attribute level is the most secure solution, it is not without drawbacks. Namely, you cannot search the encrypted data, and because you can't search it, you can't index it either. You also can't use joins on the encrypted data. Data that is securely encrypted is effectively noise. So any operations that rely on the data not being noise will not work. If you need to do any of the aforementioned operations, please consider using database and file system encryption along with transport encryption as it moves through your stack.
432 |
433 | #### Data leaks
434 | Please also consider where your data leaks. If you're using attr_encrypted with Rails, it's highly likely that this data will enter your app as a request parameter. You'll want to be sure that you're filtering your request params from you logs or else your data is sitting in the clear in your logs. [Parameter Filtering in Rails](http://apidock.com/rails/ActionDispatch/Http/FilterParameters) Please also consider other possible leak points.
435 |
436 | #### Storage requirements
437 | When storing your encrypted data, please consider the length requirements of the db column that you're storing the cipher text in. Older versions of Mysql attempt to 'help' you by truncating strings that are too large for the column. When this happens, you will not be able to decrypt your data. [MySQL Strict Trans](http://www.davidpashley.com/2009/02/15/silently-truncated/)
438 |
439 | #### Metadata regarding your crypto implementation
440 | It is advisable to also store metadata regarding the circumstances of your encrypted data. Namely, you should store information about the key used to encrypt your data, as well as the algorithm. Having this metadata with every record will make key rotation and migrating to a new algorithm signficantly easier. It will allow you to continue to decrypt old data using the information provided in the metadata and new data can be encrypted using your new key and algorithm of choice.
441 |
442 | #### Enforcing the IV as a nonce
443 | On a related note, most alorithms require that your IV be unique for every record and key combination. You can enforce this using composite unique indexes on your IV and encryption key name/id column. [RFC 5084](https://tools.ietf.org/html/rfc5084#section-1.5)
444 |
445 | #### Unique key per record
446 | Lastly, while the `:per_attribute_iv_and_salt` mode is more secure than `:per_attribute_iv` mode because it uses a unique key per record, it uses a PBKDF function which introduces a huge performance hit (175x slower by my benchmarks). There are other ways of deriving a unique key per record that would be much faster.
447 |
448 | ## Note on Patches/Pull Requests
449 |
450 | * Fork the project.
451 | * Make your feature addition or bug fix.
452 | * Add tests for it. This is important so I don't break it in a
453 | future version unintentionally.
454 | * Commit, do not mess with rakefile, version, changelog, or history.
455 | * Send me a pull request. Bonus points for topic branches.
456 |
--------------------------------------------------------------------------------