pax_global_header00006660000000000000000000000064132745702310014516gustar00rootroot0000000000000052 comment=38fbfaf466d4238bf1c77d686efdab6a1137d1d0 ruby-attr-encrypted-3.1.0/000077500000000000000000000000001327457023100154435ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/.gitignore000066400000000000000000000000721327457023100174320ustar00rootroot00000000000000.bundle .DS_Store .ruby-version pkg Gemfile.lock coverage ruby-attr-encrypted-3.1.0/.travis.yml000066400000000000000000000023011327457023100175500ustar00rootroot00000000000000sudo: false language: ruby cache: bundler rvm: - 2.0 - 2.1 - 2.2.2 - 2.3.0 - 2.4.0 - 2.5.0 - rbx env: - ACTIVERECORD=3.0.0 - ACTIVERECORD=3.1.0 - ACTIVERECORD=3.2.0 - ACTIVERECORD=4.0.0 - ACTIVERECORD=4.1.0 - ACTIVERECORD=4.2.0 - ACTIVERECORD=5.0.0 - ACTIVERECORD=5.1.1 matrix: exclude: - rvm: 2.0 env: ACTIVERECORD=5.0.0 - rvm: 2.0 env: ACTIVERECORD=5.1.1 - rvm: 2.1 env: ACTIVERECORD=5.0.0 - rvm: 2.1 env: ACTIVERECORD=5.1.1 - rvm: 2.4.0 env: ACTIVERECORD=3.0.0 - rvm: 2.4.0 env: ACTIVERECORD=3.1.0 - rvm: 2.4.0 env: ACTIVERECORD=3.2.0 - rvm: 2.4.0 env: ACTIVERECORD=4.0.0 - rvm: 2.4.0 env: ACTIVERECORD=4.1.0 - rvm: 2.5.0 env: ACTIVERECORD=3.0.0 - rvm: 2.5.0 env: ACTIVERECORD=3.1.0 - rvm: 2.5.0 env: ACTIVERECORD=3.2.0 - rvm: 2.5.0 env: ACTIVERECORD=4.0.0 - rvm: 2.5.0 env: ACTIVERECORD=4.1.0 - rvm: rbx env: ACTIVERECORD=5.0.0 - rvm: rbx env: ACTIVERECORD=5.1.1 allow_failures: - rvm: rbx fast_finish: true addons: code_climate: repo_token: a90435ed4954dd6e9f3697a20c5bc3754f67d94703f870e8fc7b00f69f5b2d06 ruby-attr-encrypted-3.1.0/CHANGELOG.md000066400000000000000000000117641327457023100172650ustar00rootroot00000000000000# attr_encrypted # ## 3.1.0 ## * Added: Abitilty to encrypt empty values. (@tamird) * Added: MIT license * Added: MRI 2.5.x support (@saghaulor) * Fixed: No long generate IV and salt if value is empty, unless :allow_empty_value options is passed. (@saghaulor) * Fixed: Only generate IV and salt when :if and :unless options evaluate such that encryption should be performed. (@saghaulor) * Fixed: Private methods are correctly evaluated as options. (@saghaulor) * Fixed: Mark virtual attributes for Rails 5.x compatibility (@grosser) * Fixed: Only check empty on strings, allows for encrypting non-string type objects * Fixed: Fixed how accessors for db columns are defined in the ActiveRecord adapter, preventing premature definition. (@nagachika) ## 3.0.3 ## * 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) ## 3.0.2 ## * Changed: Removed alias_method_chain for compatibility with Rails v5.x (@grosser) * Changed: Updated Travis build matrix to include Rails 5. (@saghaulor) (@connorshea) * Changed: Removed `.silence_stream` from tests as it has been removed from Rails 5. (@sblackstone) ## 3.0.1 ## * Fixed: attr_was method no longer calls undefined methods. (@saghaulor) ## 3.0.0 ## * Changed: Updated gemspec to use Encryptor v3.0.0. (@saghaulor) * Changed: Updated README with instructions related to moving from v2.0.0 to v3.0.0. (@saghaulor) * Fixed: ActiveModel::Dirty methods in the ActiveRecord adapter. (@saghaulor) ## 2.0.0 ## * Added: Now using Encryptor v2.0.0 (@saghaulor) * Added: Options are copied to the instance. (@saghaulor) * Added: Operation option is set during encryption/decryption to allow options to be evaluated in the context of the current operation. (@saghaulor) * Added: IV and salt can be conditionally encoded. (@saghaulor) * Added: Changelog! (@saghaulor) * Changed: attr_encrypted no longer extends object, to use with PORO extend your class, all supported ORMs are already extended. (@saghaulor) * Changed: Salt is now generated with more entropy. (@saghaulor) * Changed: The default algorithm is now `aes-256-gcm`. (@saghaulor) * Changed: The default mode is now `:per_attribute_iv`' (@saghaulor) * Changed: Extracted class level default options hash to a private method. (@saghaulor) * Changed: Dynamic finders only work with `:single_iv_and_salt` mode. (@saghaulor) * Changed: Updated documentation to include v2.0.0 changes and 'things to consider' section. (@saghaulor) * Fixed: All options are evaluated correctly. (@saghaulor) * Fixed: IV is generated for every encryption operation. (@saghaulor) * Deprecated: `:single_iv_and_salt` and `:per_attribute_iv_and_salt` modes are deprecated and will be removed in the next major release. (@saghaulor) * Deprecated: Dynamic finders via `method_missing` is deprecated and will be removed in the next major release. (@saghaulor) * Removed: Support for Ruby < 2.x (@saghaulor) * Removed: Support for Rails < 3.x (@saghaulor) * Removed: Unnecessary use of `alias_method` from ActiveRecord adapter. (@saghaulor) ## 1.4.0 ## * Added: ActiveModel::Dirty#attribute_was (@saghaulor) * Added: ActiveModel::Dirty#attribute_changed? (@mwean) ## 1.3.5 ## * Changed: Fixed gemspec to explicitly depend on Encryptor v1.3.0 (@saghaulor) * Fixed: Evaluate `:mode` option as a symbol or proc. (@cheynewallace) ## 1.3.4 ## * Added: ActiveRecord::Base.reload support. (@rcook) * Fixed: ActiveRecord adapter no longer forces attribute hashes to be string-keyed. (@tamird) * Fixed: Mass assignment protection in ActiveRecord 4. (@tamird) * Changed: Now using rubygems over https. (@tamird) * Changed: Let ActiveRecord define attribute methods. (@saghaulor) ## 1.3.3 ## * Added: Alias attr_encryptor and attr_encrpted. (@Billy Monk) ## 1.3.2 ## * Fixed: Bug regarding strong parameters. (@S. Brent Faulkner) * Fixed: Bug regarding loading per instance IV and salt. (@S. Brent Faulkner) * Fixed: Bug regarding assigning nil. (@S. Brent Faulkner) * Added: Support for protected attributes. (@S. Brent Faulkner) * Added: Support for ActiveRecord 4. (@S. Brent Faulkner) ## 1.3.1 ## * Added: Support for Rails 2.3.x and 3.1.x. (@S. Brent Faulkner) ## 1.3.0 ## * Fixed: Serialization bug. (@Billy Monk) * Added: Support for :per_attribute_iv_and_salt mode. (@rcook) * Fixed: Added dependencies to gemspec. (@jmazzi) ## 1.2.1 ## * Added: Force encoding when not marshaling. (@mosaicxm) * Fixed: Issue specifying multiple attributes on the same line. (@austintaylor) * Added: Typecasting to String before encryption (@shuber) * Added: `"#{attribute}?"` method. (@shuber) ## 1.2.0 ## * Changed: General code refactoring (@shuber) ## 1.1.2 ## * No significant changes ## 1.1.1 ## * Changled: Updated README. (@shuber) * Added: `before_type_cast` alias to ActiveRecord adapter. (@shuber) ruby-attr-encrypted-3.1.0/Gemfile000066400000000000000000000000471327457023100167370ustar00rootroot00000000000000source 'https://rubygems.org' gemspec ruby-attr-encrypted-3.1.0/MIT-LICENSE000066400000000000000000000020621327457023100170770ustar00rootroot00000000000000Copyright (c) 2008 Sean Huber - shuber@huberry.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.ruby-attr-encrypted-3.1.0/README.md000066400000000000000000000466351327457023100167400ustar00rootroot00000000000000# attr_encrypted [![Build Status](https://secure.travis-ci.org/attr-encrypted/attr_encrypted.svg)](https://travis-ci.org/attr-encrypted/attr_encrypted) [![Test Coverage](https://codeclimate.com/github/attr-encrypted/attr_encrypted/badges/coverage.svg)](https://codeclimate.com/github/attr-encrypted/attr_encrypted/coverage) [![Code Climate](https://codeclimate.com/github/attr-encrypted/attr_encrypted/badges/gpa.svg)](https://codeclimate.com/github/attr-encrypted/attr_encrypted) [![Gem Version](https://badge.fury.io/rb/attr_encrypted.svg)](https://badge.fury.io/rb/attr_encrypted) [![security](https://hakiri.io/github/attr-encrypted/attr_encrypted/master.svg)](https://hakiri.io/github/attr-encrypted/attr_encrypted/master) Generates attr_accessors that transparently encrypt and decrypt attributes. It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord`, `DataMapper`, or `Sequel`. ## Installation Add attr_encrypted to your gemfile: ```ruby gem "attr_encrypted", "~> 3.0.0" ``` Then install the gem: ```bash bundle install ``` ## Usage If you're using an ORM like `ActiveRecord`, `DataMapper`, or `Sequel`, using attr_encrypted is easy: ```ruby class User attr_encrypted :ssn, key: 'This is a key that is 256 bits!!' end ``` If you're using a PORO, you have to do a little bit more work by extending the class: ```ruby class User extend AttrEncrypted attr_accessor :name attr_encrypted :ssn, key: 'This is a key that is 256 bits!!' def load # loads the stored data end def save # saves the :name and :encrypted_ssn attributes somewhere (e.g. filesystem, database, etc) end end user = User.new user.ssn = '123-45-6789' user.ssn # returns the unencrypted object ie. '123-45-6789' user.encrypted_ssn # returns the encrypted version of :ssn user.save user = User.load user.ssn # decrypts :encrypted_ssn and returns '123-45-6789' ``` ### Encrypt/decrypt attribute class methods 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: ```ruby key = SecureRandom.random_bytes(32) iv = SecureRandom.random_bytes(12) encrypted_email = User.encrypt_email('test@test.com', iv: iv, key: key) email = User.decrypt_email(encrypted_email, iv: iv, key: key) ``` 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 ='(. ### attr_encrypted with database persistence 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 (initialization vector). 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: ```ruby create_table :users do |t| t.string :name t.string :encrypted_ssn t.string :encrypted_ssn_iv t.timestamps end ``` You can use a string or binary column type. (See the encode option section below for more info) ### Specifying the encrypted attribute name 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. ## attr_encrypted options #### Options are evaluated 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: ##### Symbols representing instance methods Here is an example class that uses an instance method to determines the encryption key to use. ```ruby class User attr_encrypted :email, key: :encryption_key def encryption_key # does some fancy logic and returns an encryption key end end ``` ##### Procs Here is an example of passing a proc/lambda object as the `:key` option as well: ```ruby class User attr_encrypted :email, key: proc { |user| user.key } end ``` ### Default options The following are the default options used by `attr_encrypted`: ```ruby prefix: 'encrypted_', suffix: '', if: true, unless: false, encode: false, encode_iv: true, encode_salt: true, default_encoding: 'm', marshal: false, marshaler: Marshal, dump_method: 'dump', load_method: 'load', encryptor: Encryptor, encrypt_method: 'encrypt', decrypt_method: 'decrypt', mode: :per_attribute_iv, algorithm: 'aes-256-gcm', allow_empty_value: false ``` All of the aforementioned options are explained in depth below. Additionally, you can specify default options for all encrypted attributes in your class. Instead of having to define your class like this: ```ruby class User attr_encrypted :email, key: 'This is a key that is 256 bits!!', prefix: '', suffix: '_crypted' attr_encrypted :ssn, key: 'a different secret key', prefix: '', suffix: '_crypted' attr_encrypted :credit_card, key: 'another secret key', prefix: '', suffix: '_crypted' end ``` You can simply define some default options like so: ```ruby class User attr_encrypted_options.merge!(prefix: '', :suffix => '_crypted') attr_encrypted :email, key: 'This is a key that is 256 bits!!' attr_encrypted :ssn, key: 'a different secret key' attr_encrypted :credit_card, key: 'another secret key' end ``` This should help keep your classes clean and DRY. ### The `:attribute` option You can simply pass the name of the encrypted attribute as the `:attribute` option: ```ruby class User attr_encrypted :email, key: 'This is a key that is 256 bits!!', attribute: 'email_encrypted' end ``` This would generate an attribute named `email_encrypted` ### The `:prefix` and `:suffix` options If you don't like the `encrypted_#{attribute}` naming convention then you can specify your own: ```ruby class User attr_encrypted :email, key: 'This is a key that is 256 bits!!', prefix: 'secret_', suffix: '_crypted' end ``` This would generate the following attribute: `secret_email_crypted`. ### The `:key` option 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). ##### Unique keys for each attribute You can specify unique keys for each attribute if you'd like: ```ruby class User attr_encrypted :email, key: 'This is a key that is 256 bits!!' attr_encrypted :ssn, key: 'a different secret key' end ``` 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.) ### The `:if` and `:unless` options 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: ```ruby class User < ActiveRecord::Base attr_encrypted :email, key: 'This is a key that is 256 bits!!', unless: Rails.env.development? attr_encrypted :ssn, key: 'This is a key that is 256 bits!!', if: Rails.env.development? end ``` You can specify both `:if` and `:unless` options. ### The `:encryptor`, `:encrypt_method`, and `:decrypt_method` options The `Encryptor` class is used by default. You may use your own custom encryptor by specifying the `:encryptor`, `:encrypt_method`, and `:decrypt_method` options. Lets suppose you'd like to use this custom encryptor class: ```ruby class SillyEncryptor def self.silly_encrypt(options) (options[:value] + options[:secret_key]).reverse end def self.silly_decrypt(options) options[:value].reverse.gsub(/#{options[:secret_key]}$/, '') end end ``` Simply set up your class like so: ```ruby class User attr_encrypted :email, secret_key: 'This is a key that is 256 bits!!', encryptor: SillyEncryptor, encrypt_method: :silly_encrypt, decrypt_method: :silly_decrypt end ``` 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. ### The `:mode` option 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`. __NOTE: `:per_attribute_iv_and_salt` and `:single_iv_and_salt` modes are deprecated and will be removed in the next major release.__ ### The `:algorithm` option 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: ```ruby class User attr_encrypted :email, key: 'This is a key that is 256 bits!!', algorithm: 'aes-256-cbc' end ``` To view a list of all cipher algorithms that are supported on your platform, run the following code in your favorite Ruby REPL: ```ruby require 'openssl' puts OpenSSL::Cipher.ciphers ``` See [Encryptor](https://github.com/attr-encrypted/encryptor#algorithms) for more information. ### The `:encode`, `:encode_iv`, `:encode_salt`, and `:default_encoding` options 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. ```ruby class User attr_encrypted :email, key: 'some secret key', encode: true, encode_iv: true, encode_salt: true end ``` 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. ### The `:marshal`, `:dump_method`, and `:load_method` options 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. ```ruby class User attr_encrypted :credentials, key: 'some secret key', marshal: true end ``` You may also optionally specify `:marshaler`, `:dump_method`, and `:load_method` if you want to use something other than the default `Marshal` object. ### The `:allow_empty_value` option You may want to encrypt empty strings or nil so as to not reveal which records are populated and which records are not. ```ruby class User attr_encrypted :credentials, key: 'some secret key', marshal: true, allow_empty_value: true end ``` ## ORMs ### ActiveRecord If you're using this gem with `ActiveRecord`, you get a few extra features: #### Default options The `:encode` option is set to true by default. #### Dynamic `find_by_` and `scoped_by_` methods 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: ```ruby class User < ActiveRecord::Base attr_encrypted :email, key: 'This is a key that is 256 bits!!' attr_encrypted :password, key: 'some other secret key' end ``` You can now lookup and login users like so: ```ruby User.find_by_email_and_password('test@example.com', 'testing') ``` 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. NOTE: This only works if all records are encrypted with the same encryption key (per attribute). __NOTE: This feature is deprecated and will be removed in the next major release.__ ### DataMapper and Sequel #### Default options The `:encode` option is set to true by default. ## Deprecations 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: * Default `:mode` is now `:per_attribute_iv`, the default `:mode` in attr_encrypted v1.x was `:single_iv_and_salt`. * Default `:algorithm` is now 'aes-256-gcm', the default `:algorithm` in attr_encrypted v1.x was 'aes-256-cbc'. * The encryption key provided must be of appropriate length respective to the algorithm used. Previously, encryptor did not verify minimum key length. * 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. * `:per_attribute_iv_and_salt` and `:single_iv_and_salt` modes are deprecated and will be removed in the next major release. Backwards compatibility is supported by providing a special option that is passed to encryptor, namely, `:insecure_mode`: ```ruby class User attr_encrypted :email, key: 'a secret key', algorithm: 'aes-256-cbc', mode: :single_iv_and_salt, insecure_mode: true end ``` 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. ## Upgrading from attr_encrypted v1.x to v3.x Modify your gemfile to include the new version of attr_encrypted: ```ruby gem attr_encrypted, "~> 3.0.0" ``` The update attr_encrypted: ```bash bundle update attr_encrypted ``` 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. ## Upgrading from attr_encrypted v2.x to v3.x 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. 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. 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. ```ruby class User attr_encrypted :ssn, key: :encryption_key, v2_gcm_iv: is_decrypting?(:ssn) def is_decrypting?(attribute) encrypted_attributes[attribute][:operation] == :decrypting end end User.all.each do |user| old_ssn = user.ssn user.ssn= old_ssn user.save end ``` ## Things to consider before using attr_encrypted #### Searching, joining, etc 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. #### Data leaks 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. #### Storage requirements 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/) #### Metadata regarding your crypto implementation 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. #### Enforcing the IV as a nonce 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) #### Unique key per record 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. ## Note on Patches/Pull Requests * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with rakefile, version, changelog, or history. * Send me a pull request. Bonus points for topic branches. ruby-attr-encrypted-3.1.0/Rakefile000066400000000000000000000011121327457023100171030ustar00rootroot00000000000000require 'rake/testtask' require 'rdoc/task' require "bundler/gem_tasks" desc 'Test the attr_encrypted gem.' Rake::TestTask.new(:test) do |t| t.libs << 'lib' t.pattern = 'test/**/*_test.rb' t.warning = false t.verbose = true end desc 'Generate documentation for the attr_encrypted gem.' Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = 'attr_encrypted' rdoc.options << '--line-numbers' << '--inline-source' rdoc.rdoc_files.include('README*') rdoc.rdoc_files.include('lib/**/*.rb') end desc 'Default: run unit tests.' task :default => :test ruby-attr-encrypted-3.1.0/attr_encrypted.gemspec000066400000000000000000000052211327457023100220370ustar00rootroot00000000000000# -*- encoding: utf-8 -*- lib = File.expand_path('../lib/', __FILE__) $:.unshift lib unless $:.include?(lib) require 'attr_encrypted/version' require 'date' Gem::Specification.new do |s| s.name = 'attr_encrypted' s.version = AttrEncrypted::Version.string s.date = Date.today s.summary = 'Encrypt and decrypt attributes' s.description = 'Generates attr_accessors that encrypt and decrypt attributes transparently' s.authors = ['Sean Huber', 'S. Brent Faulkner', 'William Monk', 'Stephen Aghaulor'] s.email = ['seah@shuber.io', 'sbfaulkner@gmail.com', 'billy.monk@gmail.com', 'saghaulor@gmail.com'] s.homepage = 'http://github.com/attr-encrypted/attr_encrypted' s.license = 'MIT' s.has_rdoc = false s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.rdoc'] s.require_paths = ['lib'] s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- test/*`.split("\n") s.required_ruby_version = '>= 2.0.0' s.add_dependency('encryptor', ['~> 3.0.0']) # support for testing with specific active record version activerecord_version = if ENV.key?('ACTIVERECORD') "~> #{ENV['ACTIVERECORD']}" else '>= 2.0.0' end s.add_development_dependency('activerecord', activerecord_version) s.add_development_dependency('actionpack', activerecord_version) s.add_development_dependency('datamapper') s.add_development_dependency('rake') s.add_development_dependency('minitest') s.add_development_dependency('sequel') if RUBY_VERSION < '2.1.0' s.add_development_dependency('nokogiri', '< 1.7.0') s.add_development_dependency('public_suffix', '< 3.0.0') end if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby s.add_development_dependency('activerecord-jdbcsqlite3-adapter') s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke else s.add_development_dependency('sqlite3') end s.add_development_dependency('dm-sqlite-adapter') s.add_development_dependency('simplecov') s.add_development_dependency('simplecov-rcov') s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0') s.cert_chain = ['certs/saghaulor.pem'] s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/ s.post_install_message = "\n\n\nWARNING: Several insecure default options and features were deprecated in attr_encrypted v2.0.0.\n Additionally, there was a bug in Encryptor v2.0.0 that insecurely encrypted data when using an AES-*-GCM algorithm.\n This bug was fixed but introduced breaking changes between v2.x and v3.x.\n Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n" end ruby-attr-encrypted-3.1.0/certs/000077500000000000000000000000001327457023100165635ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/certs/saghaulor.pem000066400000000000000000000023511327457023100212540ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDdDCCAlygAwIBAgIBATANBgkqhkiG9w0BAQUFADBAMRIwEAYDVQQDDAlzYWdo YXVsb3IxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv bTAeFw0xODAyMTIwMzMzMThaFw0xOTAyMTIwMzMzMThaMEAxEjAQBgNVBAMMCXNh Z2hhdWxvcjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOLqbSmj5txfw39a Ki0g3BJWGrfGBiSRq9aThUGzoiaqyDo/m1WMQdgPioZG+P923okChEWFjhSymBQU eMdys6JRPm5ortp5sh9gesOWoozqb8R55d8rr1V7pY533cCut53Kb1wiitjkfXjR efT2HPh6nV6rYjGMJek/URaCNzsZo7HCkRsKdezP+BKr4V4wOka69tfJX5pcvFvR iiqfaiP4RK12hYdsFnSVKiKP7SAFTFiYcohbL8TUW6ezQQqJCK0M6fu74EWVCnBS gFVjj931BuD8qhuxMiB6uC6FKxemB5TRGBLzn7RcrOMAo2inMAopjkGeQJUAyVCm J5US3wIDAQABo3kwdzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU hJEuSZgvuuIhIsxQ/0pRQTBVzokwHgYDVR0RBBcwFYETc2FnaGF1bG9yQGdtYWls LmNvbTAeBgNVHRIEFzAVgRNzYWdoYXVsb3JAZ21haWwuY29tMA0GCSqGSIb3DQEB BQUAA4IBAQCsBS2cxqTmV4nXJEH/QbdgjVDAZbK6xf2gpM3vCRlYsy7Wz6GEoOpD bzRkjxZwGNbhXShMUZwm6zahYQ/L1/HFztLoMBMkm8EIfPxH0PDrP4aWl0oyWxmU OLm0/t9icSWRPPJ1tLJvuAaDdVpY5dEHd6VdnNJGQC5vHKRInt1kEyqEttIJ/xmJ leSEFyMeoFsR+C/WPG9WSC+xN0eXqakCu6YUJoQzCn/7znv8WxpHEbeZjNIHq0qb nbqZ/ZW1bwzj1T/NIbnMr37wqV29XwkI4+LbewMkb6/bDPYl0qZpAkCxKtGYCCJp l6KPs9K/72yH00dxuAhiTXikTkcLXeQJ -----END CERTIFICATE----- ruby-attr-encrypted-3.1.0/checksum/000077500000000000000000000000001327457023100172455ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.0.gem.sha256000066400000000000000000000001001327457023100244200ustar00rootroot00000000000000845fc3cb09a19c3ac76192aba443788f92c880744617bca99b16fd31ce843e07ruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.0.gem.sha512000066400000000000000000000002001327457023100244140ustar00rootroot0000000000000081a065442258cc3702aab62c7b2307a48ed3e0deb803600d11a7480cce0db7c43fd9929acd2755081042f8989236553fd694b6cb62776bbfc53f9165a22cbca1ruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.1.gem.sha256000066400000000000000000000001001327457023100244210ustar00rootroot0000000000000033140af4b223177db7a19efb2fa38472a299a745b29ca1c5ba9d3fa947390b77ruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.1.gem.sha512000066400000000000000000000002001327457023100244150ustar00rootroot000000000000000c467cab98b9b2eb331f9818323a90ae01392d6cb03cf1f32faccc954d0fc54be65f0fc7bf751b0fce57925eef1c9e2af90181bc40d81ad93e21d15a001c53c6ruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.2.gem.sha256000066400000000000000000000001001327457023100244220ustar00rootroot00000000000000c1256b459336d4a2012a0d0c70ce5cd3dac46acb5e78da6f77f6f104cb1e8b7bruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.2.gem.sha512000066400000000000000000000002001327457023100244160ustar00rootroot00000000000000dca0c8a729974c0e26fde4cd4216c7d0f66d9eca9f6cf0ccca64999f5180a00bf7c05b630c1d420ec1673141a2923946e8bd28b12e711faf64a4cd42c7a3ac9eruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.3.gem.sha256000066400000000000000000000001001327457023100244230ustar00rootroot000000000000006d84c64852c4bbc0926b92fe7a93295671a9e69cb2939b96fb1e4b5e8a5b33b6ruby-attr-encrypted-3.1.0/checksum/attr_encrypted-3.0.3.gem.sha512000066400000000000000000000002001327457023100244170ustar00rootroot000000000000000f960e8a2f63c747c273241f7395dcceb0dd8a6f79349bee453db741fc7ea5ceb4342d7d5908e540e3b5acea2216ff38bef8c743e6e7c8559bacb4a731ab27c4ruby-attr-encrypted-3.1.0/checksums.yaml.gz.sig000066400000000000000000000004001327457023100215060ustar00rootroot00000000000000)áºû`'\Ç´±ðÃÙl{Éþ‚Æù-áÆp.åIâKÍyŸ£4˜Ë•’­`ì{‚¹ó§ÍŽL‰öóªÁ—uho¾ dÞˆEÉVHzP ™¹ÓUO˜#Ùt.ÖÂòo£†Ø/±ñ§tªÑ˜z}àÅÝ#3l`a ¿JDKcòô¡½‰)^ŽÈ ‰†ã?µ<¨ø±^EÛÞ2& ´BoF«qZé£Ë9•–ù¤Ìúà—69Z—³s.Á„ö(hïU—È?{³þ¼zp?øP›»\3§·éâ mª¨³ëNäKjó¢#`0nÝ9J”Otݼý{Þ£ªº}‰Å ¾)ruby-attr-encrypted-3.1.0/data.tar.gz.sig000066400000000000000000000004001327457023100202560ustar00rootroot00000000000000Môr¿ø¿+õp"!,¨rkE⤗äÙ,bN®;¶›z4EÓÁ0‰¡Sc\@âå¬]$¢ÅÆuD“+5OŒBøÕ( ˆ/:ÕE™ì ¡Ø|4½÷W…¨€è`¬•¯Z¥õúð¯âõ ²tíUaµæj<±z6¤²læ€ÀÆ¿„E\ý$Ø8^ìmuóºkárØ”±;Ü“‰*®¿ë¼¼†Aཧ^°q–ñªK:†/°ü/µÐ…ÍY©ìG4_£ûIŸ ¼àdHC”ÛÝ÷u{°_ê ".¼2/éswÛ;UQ)rªël(µ‘ÂdS Qm ±näÜHÄꪑ¦.C¤ruby-attr-encrypted-3.1.0/lib/000077500000000000000000000000001327457023100162115ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/lib/attr_encrypted.rb000066400000000000000000000435231327457023100215740ustar00rootroot00000000000000require 'encryptor' # Adds attr_accessors that encrypt and decrypt an object's attributes module AttrEncrypted autoload :Version, 'attr_encrypted/version' def self.extended(base) # :nodoc: base.class_eval do include InstanceMethods attr_writer :attr_encrypted_options @attr_encrypted_options, @encrypted_attributes = {}, {} end end # Generates attr_accessors that encrypt and decrypt attributes transparently # # Options (any other options you specify are passed to the Encryptor's encrypt and decrypt methods) # # attribute: The name of the referenced encrypted attribute. For example # attr_accessor :email, attribute: :ee would generate an # attribute named 'ee' to store the encrypted email. This is useful when defining # one attribute to encrypt at a time or when the :prefix and :suffix options # aren't enough. # Defaults to nil. # # prefix: A prefix used to generate the name of the referenced encrypted attributes. # For example attr_accessor :email, prefix: 'crypted_' would # generate attributes named 'crypted_email' to store the encrypted # email and password. # Defaults to 'encrypted_'. # # suffix: A suffix used to generate the name of the referenced encrypted attributes. # For example attr_accessor :email, prefix: '', suffix: '_encrypted' # would generate attributes named 'email_encrypted' to store the # encrypted email. # Defaults to ''. # # key: The encryption key. This option may not be required if # you're using a custom encryptor. If you pass a symbol # representing an instance method then the :key option # will be replaced with the result of the method before # being passed to the encryptor. Objects that respond # to :call are evaluated as well (including procs). # Any other key types will be passed directly to the encryptor. # Defaults to nil. # # encode: If set to true, attributes will be encoded as well as # encrypted. This is useful if you're planning on storing # the encrypted attributes in a database. The default # encoding is 'm' (base64), however this can be overwritten # by setting the :encode option to some other encoding # string instead of just 'true'. See # http://www.ruby-doc.org/core/classes/Array.html#M002245 # for more encoding directives. # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel. # # encode_iv: Defaults to true. # encode_salt: Defaults to true. # # default_encoding: Defaults to 'm' (base64). # # marshal: If set to true, attributes will be marshaled as well # as encrypted. This is useful if you're planning on # encrypting something other than a string. # Defaults to false. # # marshaler: The object to use for marshaling. # Defaults to Marshal. # # dump_method: The dump method name to call on the :marshaler object to. # Defaults to 'dump'. # # load_method: The load method name to call on the :marshaler object. # Defaults to 'load'. # # encryptor: The object to use for encrypting. # Defaults to Encryptor. # # encrypt_method: The encrypt method name to call on the :encryptor object. # Defaults to 'encrypt'. # # decrypt_method: The decrypt method name to call on the :encryptor object. # Defaults to 'decrypt'. # # if: Attributes are only encrypted if this option evaluates # to true. If you pass a symbol representing an instance # method then the result of the method will be evaluated. # Any objects that respond to :call are evaluated as well. # Defaults to true. # # unless: Attributes are only encrypted if this option evaluates # to false. If you pass a symbol representing an instance # method then the result of the method will be evaluated. # Any objects that respond to :call are evaluated as well. # Defaults to false. # # mode: Selects encryption mode for attribute: choose :single_iv_and_salt for compatibility # with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used. # 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. # A :per_attribute_iv mode derives a unique IV per attribute; salt is not used. # Defaults to :per_attribute_iv. # # allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option # has a truthy value. # # You can specify your own default options # # class User # # Now all attributes will be encoded and marshaled by default # attr_encrypted_options.merge!(encode: true, marshal: true, some_other_option: true) # attr_encrypted :configuration, key: 'my secret key' # end # # # Example # # class User # attr_encrypted :email, key: 'some secret key' # attr_encrypted :configuration, key: 'some other secret key', marshal: true # end # # @user = User.new # @user.encrypted_email # nil # @user.email? # false # @user.email = 'test@example.com' # @user.email? # true # @user.encrypted_email # returns the encrypted version of 'test@example.com' # # @user.configuration = { time_zone: 'UTC' } # @user.encrypted_configuration # returns the encrypted version of configuration # # See README for more examples def attr_encrypted(*attributes) options = attributes.last.is_a?(Hash) ? attributes.pop : {} options = attr_encrypted_default_options.dup.merge!(attr_encrypted_options).merge!(options) options[:encode] = options[:default_encoding] if options[:encode] == true options[:encode_iv] = options[:default_encoding] if options[:encode_iv] == true options[:encode_salt] = options[:default_encoding] if options[:encode_salt] == true attributes.each do |attribute| encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym instance_methods_as_symbols = attribute_instance_methods_as_symbols if attribute_instance_methods_as_symbols_available? attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name) attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=") iv_name = "#{encrypted_attribute_name}_iv".to_sym attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name) attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=") salt_name = "#{encrypted_attribute_name}_salt".to_sym attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name) attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=") end define_method(attribute) do instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name))) end define_method("#{attribute}=") do |value| send("#{encrypted_attribute_name}=", encrypt(attribute, value)) instance_variable_set("@#{attribute}", value) end define_method("#{attribute}?") do value = send(attribute) value.respond_to?(:empty?) ? !value.empty? : !!value end encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name) end end alias_method :attr_encryptor, :attr_encrypted # Default options to use with calls to attr_encrypted # # It will inherit existing options from its superclass def attr_encrypted_options @attr_encrypted_options ||= superclass.attr_encrypted_options.dup end def attr_encrypted_default_options { prefix: 'encrypted_', suffix: '', if: true, unless: false, encode: false, encode_iv: true, encode_salt: true, default_encoding: 'm', marshal: false, marshaler: Marshal, dump_method: 'dump', load_method: 'load', encryptor: Encryptor, encrypt_method: 'encrypt', decrypt_method: 'decrypt', mode: :per_attribute_iv, algorithm: 'aes-256-gcm', allow_empty_value: false, } end private :attr_encrypted_default_options # Checks if an attribute is configured with attr_encrypted # # Example # # class User # attr_accessor :name # attr_encrypted :email # end # # User.attr_encrypted?(:name) # false # User.attr_encrypted?(:email) # true def attr_encrypted?(attribute) encrypted_attributes.has_key?(attribute.to_sym) end # Decrypts a value for the attribute specified # # Example # # class User # attr_encrypted :email # end # # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING') def decrypt(attribute, encrypted_value, options = {}) options = encrypted_attributes[attribute.to_sym].merge(options) if options[:if] && !options[:unless] && not_empty?(encrypted_value) encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode] value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value)) if options[:marshal] value = options[:marshaler].send(options[:load_method], value) elsif defined?(Encoding) encoding = Encoding.default_internal || Encoding.default_external value = value.force_encoding(encoding.name) end value else encrypted_value end end # Encrypts a value for the attribute specified # # Example # # class User # attr_encrypted :email # end # # encrypted_email = User.encrypt(:email, 'test@example.com') def encrypt(attribute, value, options = {}) options = encrypted_attributes[attribute.to_sym].merge(options) if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value)) value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value)) encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode] encrypted_value else value end end def not_empty?(value) !value.nil? && !(value.is_a?(String) && value.empty?) end # Contains a hash of encrypted attributes with virtual attribute names as keys # and their corresponding options as values # # Example # # class User # attr_encrypted :email, key: 'my secret key' # end # # User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } } def encrypted_attributes @encrypted_attributes ||= superclass.encrypted_attributes.dup end # Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method # if attribute was configured with attr_encrypted # # Example # # class User # attr_encrypted :email, key: 'my secret key' # end # # User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING') def method_missing(method, *arguments, &block) if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3) send($1, $3, *arguments) else super end end module InstanceMethods # Decrypts a value for the attribute specified using options evaluated in the current object's scope # # Example # # class User # attr_accessor :secret_key # attr_encrypted :email, key: :secret_key # # def initialize(secret_key) # self.secret_key = secret_key # end # end # # @user = User.new('some-secret-key') # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING') def decrypt(attribute, encrypted_value) encrypted_attributes[attribute.to_sym][:operation] = :decrypting encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value) self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute)) end # Encrypts a value for the attribute specified using options evaluated in the current object's scope # # Example # # class User # attr_accessor :secret_key # attr_encrypted :email, key: :secret_key # # def initialize(secret_key) # self.secret_key = secret_key # end # end # # @user = User.new('some-secret-key') # @user.encrypt(:email, 'test@example.com') def encrypt(attribute, value) encrypted_attributes[attribute.to_sym][:operation] = :encrypting encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value) self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute)) end # Copies the class level hash of encrypted attributes with virtual attribute names as keys # and their corresponding options as values to the instance # def encrypted_attributes @encrypted_attributes ||= self.class.encrypted_attributes.dup end protected # Returns attr_encrypted options evaluated in the current object's scope for the attribute specified def evaluated_attr_encrypted_options_for(attribute) evaluated_options = Hash.new attribute_option_value = encrypted_attributes[attribute.to_sym][:attribute] encrypted_attributes[attribute.to_sym].map do |option, value| evaluated_options[option] = evaluate_attr_encrypted_option(value) end evaluated_options[:attribute] = attribute_option_value evaluated_options.tap do |options| if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value] unless options[:mode] == :single_iv_and_salt load_iv_for_attribute(attribute, options) end if options[:mode] == :per_attribute_iv_and_salt load_salt_for_attribute(attribute, options) end end end end # Evaluates symbol (method reference) or proc (responds to call) options # # If the option is not a symbol or proc then the original option is returned def evaluate_attr_encrypted_option(option) if option.is_a?(Symbol) && respond_to?(option, true) send(option) elsif option.respond_to?(:call) option.call(self) else option end end def load_iv_for_attribute(attribute, options) encrypted_attribute_name = options[:attribute] encode_iv = options[:encode_iv] iv = options[:iv] || send("#{encrypted_attribute_name}_iv") if options[:operation] == :encrypting begin iv = generate_iv(options[:algorithm]) iv = [iv].pack(encode_iv) if encode_iv send("#{encrypted_attribute_name}_iv=", iv) rescue RuntimeError end end if iv && !iv.empty? iv = iv.unpack(encode_iv).first if encode_iv options[:iv] = iv end end def generate_iv(algorithm) algo = OpenSSL::Cipher.new(algorithm) algo.encrypt algo.random_iv end def load_salt_for_attribute(attribute, options) encrypted_attribute_name = options[:attribute] encode_salt = options[:encode_salt] salt = options[:salt] || send("#{encrypted_attribute_name}_salt") if options[:operation] == :encrypting salt = SecureRandom.random_bytes salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt send("#{encrypted_attribute_name}_salt=", salt) end if salt && !salt.empty? salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt options[:salt] = salt end end def prefix_and_encode_salt(salt, encoding) prefix = '_' prefix + [salt].pack(encoding) end def decode_salt_if_encoded(salt, encoding) prefix = '_' salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt end end protected def attribute_instance_methods_as_symbols instance_methods.collect { |method| method.to_sym } end def attribute_instance_methods_as_symbols_available? true end end Dir[File.join(File.dirname(__FILE__), 'attr_encrypted', 'adapters', '*.rb')].each { |adapter| require adapter } ruby-attr-encrypted-3.1.0/lib/attr_encrypted/000077500000000000000000000000001327457023100212405ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/lib/attr_encrypted/adapters/000077500000000000000000000000001327457023100230435ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.rb000066400000000000000000000130451327457023100262040ustar00rootroot00000000000000if defined?(ActiveRecord::Base) module AttrEncrypted module Adapters module ActiveRecord def self.extended(base) # :nodoc: base.class_eval do # https://github.com/attr-encrypted/attr_encrypted/issues/68 alias_method :reload_without_attr_encrypted, :reload def reload(*args, &block) result = reload_without_attr_encrypted(*args, &block) self.class.encrypted_attributes.keys.each do |attribute_name| instance_variable_set("@#{attribute_name}", nil) end result end attr_encrypted_options[:encode] = true class << self alias_method :method_missing_without_attr_encrypted, :method_missing alias_method :method_missing, :method_missing_with_attr_encrypted end def perform_attribute_assignment(method, new_attributes, *args) return if new_attributes.blank? send method, new_attributes.reject { |k, _| self.class.encrypted_attributes.key?(k.to_sym) }, *args send method, new_attributes.reject { |k, _| !self.class.encrypted_attributes.key?(k.to_sym) }, *args end private :perform_attribute_assignment if ::ActiveRecord::VERSION::STRING > "3.1" alias_method :assign_attributes_without_attr_encrypted, :assign_attributes def assign_attributes(*args) perform_attribute_assignment :assign_attributes_without_attr_encrypted, *args end end alias_method :attributes_without_attr_encrypted=, :attributes= def attributes=(*args) perform_attribute_assignment :attributes_without_attr_encrypted=, *args end end end protected # attr_encrypted method def attr_encrypted(*attrs) super options = attrs.extract_options! attr = attrs.pop attribute attr if ::ActiveRecord::VERSION::STRING >= "5.1.0" options.merge! encrypted_attributes[attr] define_method("#{attr}_was") do attribute_was(attr) end if ::ActiveRecord::VERSION::STRING >= "4.1" define_method("#{attr}_changed?") do |options = {}| attribute_changed?(attr, options) end else define_method("#{attr}_changed?") do attribute_changed?(attr) end end define_method("#{attr}_change") do attribute_change(attr) end define_method("#{attr}_with_dirtiness=") do |value| attribute_will_change!(attr) if value != __send__(attr) __send__("#{attr}_without_dirtiness=", value) end alias_method "#{attr}_without_dirtiness=", "#{attr}=" alias_method "#{attr}=", "#{attr}_with_dirtiness=" alias_method "#{attr}_before_type_cast", attr end def attribute_instance_methods_as_symbols # We add accessor methods of the db columns to the list of instance # methods returned to let ActiveRecord define the accessor methods # for the db columns if connected? && table_exists? columns_hash.keys.inject(super) {|instance_methods, column_name| instance_methods.concat [column_name.to_sym, :"#{column_name}="]} else super end end def attribute_instance_methods_as_symbols_available? connected? && table_exists? end # Allows you to use dynamic methods like find_by_email or scoped_by_email for # encrypted attributes # # NOTE: This only works when the :key option is specified as a string (see the README) # # This is useful for encrypting fields like email addresses. Your user's email addresses # are encrypted in the database, but you can still look up a user by email for logging in # # Example # # class User < ActiveRecord::Base # attr_encrypted :email, key: 'secret key' # end # # User.find_by_email_and_password('test@example.com', 'testing') # # results in a call to # User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing') def method_missing_with_attr_encrypted(method, *args, &block) if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s) attribute_names = match.captures.last.split('_and_') attribute_names.each_with_index do |attribute, index| if attr_encrypted?(attribute) && encrypted_attributes[attribute.to_sym][:mode] == :single_iv_and_salt args[index] = send("encrypt_#{attribute}", args[index]) warn "DEPRECATION WARNING: This feature will be removed in the next major release." attribute_names[index] = encrypted_attributes[attribute.to_sym][:attribute] end end method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym end method_missing_without_attr_encrypted(method, *args, &block) end end end end ActiveRecord::Base.extend AttrEncrypted ActiveRecord::Base.extend AttrEncrypted::Adapters::ActiveRecord end ruby-attr-encrypted-3.1.0/lib/attr_encrypted/adapters/data_mapper.rb000066400000000000000000000011421327457023100256430ustar00rootroot00000000000000if defined?(DataMapper) module AttrEncrypted module Adapters module DataMapper def self.extended(base) # :nodoc: class << base alias_method :included_without_attr_encrypted, :included alias_method :included, :included_with_attr_encrypted end end def included_with_attr_encrypted(base) included_without_attr_encrypted(base) base.extend AttrEncrypted base.attr_encrypted_options[:encode] = true end end end end DataMapper::Resource.extend AttrEncrypted::Adapters::DataMapper endruby-attr-encrypted-3.1.0/lib/attr_encrypted/adapters/sequel.rb000066400000000000000000000004671327457023100246750ustar00rootroot00000000000000if defined?(Sequel) module AttrEncrypted module Adapters module Sequel def self.extended(base) # :nodoc: base.attr_encrypted_options[:encode] = true end end end end Sequel::Model.extend AttrEncrypted Sequel::Model.extend AttrEncrypted::Adapters::Sequel endruby-attr-encrypted-3.1.0/lib/attr_encrypted/version.rb000066400000000000000000000005711327457023100232550ustar00rootroot00000000000000module AttrEncrypted # Contains information about this gem's version module Version MAJOR = 3 MINOR = 1 PATCH = 0 # Returns a version string by joining MAJOR, MINOR, and PATCH with '.' # # Example # # Version.string # '1.0.2' def self.string [MAJOR, MINOR, PATCH].join('.') end end end ruby-attr-encrypted-3.1.0/metadata.gz.sig000066400000000000000000000004001327457023100203400ustar00rootroot00000000000000AeÃmL¶ÄúøÏÉ6>©o(pƒñÍ{ÿž~xóO.]Là |ZgfQ ËL44uؤŠ¥:òøS!oÜ@€Äž€§óau9s- Xx÷6ïù²ÏM …+ bæmš7“öÚžG‘¾«£Ð8Ñ8ˆ’+Ãúàͬ\‰Ýàç?´ê üÎ) nÖÁ‘¶ƒ¸»C7†§–¾àñ”rv”å`by¥¨$8&Ë.Ô«Í@"¸¿ˆø¾é^ÒÕ&xxUÉ2ïÔÈØœfž5øØNi<ÆÆe}á÷½Ò7øÓ:È[]çüÎ{˜DÏè”åkÒ(W»Ù„ë#ꕣʱ®ruby-attr-encrypted-3.1.0/test/000077500000000000000000000000001327457023100164225ustar00rootroot00000000000000ruby-attr-encrypted-3.1.0/test/active_record_test.rb000066400000000000000000000264331327457023100226270ustar00rootroot00000000000000require_relative 'test_helper' ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') def create_tables ActiveRecord::Schema.define(version: 1) do self.verbose = false create_table :people do |t| t.string :encrypted_email t.string :password t.string :encrypted_credentials t.binary :salt t.binary :key_iv t.string :encrypted_email_salt t.string :encrypted_credentials_salt t.string :encrypted_email_iv t.string :encrypted_credentials_iv end create_table :accounts do |t| t.string :encrypted_password t.string :encrypted_password_iv t.string :encrypted_password_salt t.string :key end create_table :users do |t| t.string :login t.string :encrypted_password t.string :encrypted_password_iv t.boolean :is_admin end create_table :prime_ministers do |t| t.string :encrypted_name t.string :encrypted_name_iv end create_table :addresses do |t| t.binary :encrypted_street t.binary :encrypted_street_iv t.binary :encrypted_zipcode t.string :mode end end end ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError) if ::ActiveRecord::VERSION::STRING > "4.0" module Rack module Test class UploadedFile; end end end require 'action_controller/metal/strong_parameters' end class Person < ActiveRecord::Base self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt attr_encrypted :email, key: SECRET_KEY attr_encrypted :credentials, key: Proc.new { |user| Encryptor.encrypt(value: user.salt, key: SECRET_KEY, iv: user.key_iv) }, marshal: true after_initialize :initialize_salt_and_credentials protected def initialize_salt_and_credentials self.key_iv ||= SecureRandom.random_bytes(12) self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15] self.credentials ||= { username: 'example', password: 'test' } end end class PersonWithValidation < Person validates_presence_of :email end class PersonWithProcMode < Person attr_encrypted :email, key: SECRET_KEY, mode: Proc.new { :per_attribute_iv_and_salt } attr_encrypted :credentials, key: SECRET_KEY, mode: Proc.new { :single_iv_and_salt }, insecure_mode: true end class Account < ActiveRecord::Base ACCOUNT_ENCRYPTION_KEY = SecureRandom.urlsafe_base64(24) attr_encrypted :password, key: :password_encryption_key def encrypting?(attr) encrypted_attributes[attr][:operation] == :encrypting end def password_encryption_key if encrypting?(:password) self.key = ACCOUNT_ENCRYPTION_KEY else self.key end end end class PersonWithSerialization < ActiveRecord::Base self.table_name = 'people' attr_encrypted :email, key: SECRET_KEY serialize :password end class UserWithProtectedAttribute < ActiveRecord::Base self.table_name = 'users' attr_encrypted :password, key: SECRET_KEY attr_protected :is_admin if ::ActiveRecord::VERSION::STRING < "4.0" end class PersonUsingAlias < ActiveRecord::Base self.table_name = 'people' attr_encryptor :email, key: SECRET_KEY end class PrimeMinister < ActiveRecord::Base attr_encrypted :name, marshal: true, key: SECRET_KEY end class Address < ActiveRecord::Base self.attr_encrypted_options[:marshal] = false self.attr_encrypted_options[:encode] = false attr_encrypted :street, encode_iv: false, key: SECRET_KEY attr_encrypted :zipcode, key: SECRET_KEY, mode: Proc.new { |address| address.mode.to_sym }, insecure_mode: true end class ActiveRecordTest < Minitest::Test def setup drop_all_tables create_tables end def test_should_encrypt_email @person = Person.create(email: 'test@example.com') refute_nil @person.encrypted_email refute_equal @person.email, @person.encrypted_email assert_equal @person.email, Person.first.email end def test_should_marshal_and_encrypt_credentials @person = Person.create refute_nil @person.encrypted_credentials refute_equal @person.credentials, @person.encrypted_credentials assert_equal @person.credentials, Person.first.credentials end def test_should_encode_by_default assert Person.attr_encrypted_options[:encode] end def test_should_validate_presence_of_email @person = PersonWithValidation.new assert !@person.valid? assert !@person.errors[:email].empty? || @person.errors.on(:email) end def test_should_encrypt_decrypt_with_iv @person = Person.create(email: 'test@example.com') @person2 = Person.find(@person.id) refute_nil @person2.encrypted_email_iv assert_equal 'test@example.com', @person2.email end def test_should_ensure_attributes_can_be_deserialized @person = PersonWithSerialization.new(email: 'test@example.com', password: %w(an array of strings)) @person.save assert_equal @person.password, %w(an array of strings) end def test_should_create_an_account_regardless_of_arguments_order Account.create!(key: SECRET_KEY, password: "password") Account.create!(password: "password" , key: SECRET_KEY) end def test_should_set_attributes_regardless_of_arguments_order # minitest does not implement `assert_nothing_raised` https://github.com/seattlerb/minitest/issues/112 Account.new.attributes = { password: "password", key: SECRET_KEY } end def test_should_create_changed_predicate person = Person.create!(email: 'test@example.com') refute person.email_changed? person.email = 'test@example.com' refute person.email_changed? person.email = nil assert person.email_changed? person.email = 'test2@example.com' assert person.email_changed? end def test_should_create_was_predicate original_email = 'test@example.com' person = Person.create!(email: original_email) assert_equal original_email, person.email_was person.email = 'test2@example.com' assert_equal original_email, person.email_was old_pm_name = "Winston Churchill" pm = PrimeMinister.create!(name: old_pm_name) assert_equal old_pm_name, pm.name_was old_zipcode = "90210" address = Address.create!(zipcode: old_zipcode, mode: "single_iv_and_salt") assert_equal old_zipcode, address.zipcode_was end def test_attribute_was_works_when_options_for_old_encrypted_value_are_different_than_options_for_new_encrypted_value pw = 'password' crypto_key = SecureRandom.urlsafe_base64(24) old_iv = SecureRandom.random_bytes(12) account = Account.create encrypted_value = Encryptor.encrypt(value: pw, iv: old_iv, key: crypto_key) Account.where(id: account.id).update_all(key: crypto_key, encrypted_password_iv: [old_iv].pack('m'), encrypted_password: [encrypted_value].pack('m')) account = Account.find(account.id) assert_equal pw, account.password account.password = pw.reverse assert_equal pw, account.password_was account.save account.reload assert_equal Account::ACCOUNT_ENCRYPTION_KEY, account.key assert_equal pw.reverse, account.password end if ::ActiveRecord::VERSION::STRING > "4.0" def test_should_assign_attributes @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) assert_equal 'modified', @user.login end def test_should_not_assign_protected_attributes @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) assert !@user.is_admin? end def test_should_raise_exception_if_not_permitted @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) assert_raises ActiveModel::ForbiddenAttributesError do @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true) end end def test_should_raise_exception_on_init_if_not_permitted assert_raises ActiveModel::ForbiddenAttributesError do @user = UserWithProtectedAttribute.new ActionController::Parameters.new(login: 'modified', is_admin: true) end end else def test_should_assign_attributes @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) @user.attributes = { login: 'modified', is_admin: true } assert_equal 'modified', @user.login end def test_should_not_assign_protected_attributes @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) @user.attributes = { login: 'modified', is_admin: true } assert !@user.is_admin? end def test_should_assign_protected_attributes @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) if ::ActiveRecord::VERSION::STRING > "3.1" @user.send(:assign_attributes, { login: 'modified', is_admin: true }, without_protection: true) else @user.send(:attributes=, { login: 'modified', is_admin: true }, false) end assert @user.is_admin? end end def test_should_allow_assignment_of_nil_attributes @person = Person.new assert_nil(@person.attributes = nil) end def test_should_allow_proc_based_mode @person = PersonWithProcMode.create(email: 'test@example.com', credentials: 'password123') # Email is :per_attribute_iv_and_salt assert_equal @person.class.encrypted_attributes[:email][:mode].class, Proc assert_equal @person.class.encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt refute_nil @person.encrypted_email_salt refute_nil @person.encrypted_email_iv # Credentials is :single_iv_and_salt assert_equal @person.class.encrypted_attributes[:credentials][:mode].class, Proc assert_equal @person.class.encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt assert_nil @person.encrypted_credentials_salt assert_nil @person.encrypted_credentials_iv end if ::ActiveRecord::VERSION::STRING > "3.1" def test_should_allow_assign_attributes_with_nil @person = Person.new assert_nil(@person.assign_attributes nil) end end def test_that_alias_encrypts_column user = PersonUsingAlias.new user.email = 'test@example.com' user.save refute_nil user.encrypted_email refute_equal user.email, user.encrypted_email assert_equal user.email, PersonUsingAlias.first.email end # See https://github.com/attr-encrypted/attr_encrypted/issues/68 def test_should_invalidate_virtual_attributes_on_reload old_pm_name = 'Winston Churchill' new_pm_name = 'Neville Chamberlain' pm = PrimeMinister.create!(name: old_pm_name) assert_equal old_pm_name, pm.name pm.name = new_pm_name assert_equal new_pm_name, pm.name result = pm.reload assert_equal pm, result assert_equal old_pm_name, pm.name end def test_should_save_encrypted_data_as_binary street = '123 Elm' address = Address.create!(street: street) refute_equal address.encrypted_street, street assert_equal Address.first.street, street end def test_should_evaluate_proc_based_mode street = '123 Elm' zipcode = '12345' address = Address.create(street: street, zipcode: zipcode, mode: :single_iv_and_salt) address.reload refute_equal address.encrypted_zipcode, zipcode assert_equal address.zipcode, zipcode end end ruby-attr-encrypted-3.1.0/test/attr_encrypted_test.rb000066400000000000000000000406301327457023100230400ustar00rootroot00000000000000# encoding: UTF-8 require_relative 'test_helper' class SillyEncryptor def self.silly_encrypt(options) (options[:value] + options[:some_arg]).reverse end def self.silly_decrypt(options) options[:value].reverse.gsub(/#{options[:some_arg]}$/, '') end end class User extend AttrEncrypted self.attr_encrypted_options[:key] = Proc.new { |user| SECRET_KEY } # default key self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt attr_encrypted :email, :without_encoding, :key => SECRET_KEY attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test' attr_encrypted :ssn, :key => :secret_key, :attribute => 'ssn_encrypted' attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test' attr_encrypted :with_encoding, :key => SECRET_KEY, :encode => true attr_encrypted :with_custom_encoding, :key => SECRET_KEY, :encode => 'm' attr_encrypted :with_marshaling, :key => SECRET_KEY, :marshal => true attr_encrypted :with_true_if, :key => SECRET_KEY, :if => true, mode: :per_attribute_iv_and_salt attr_encrypted :with_false_if, :key => SECRET_KEY, :if => false, mode: :per_attribute_iv_and_salt attr_encrypted :with_true_unless, :key => SECRET_KEY, :unless => true, mode: :per_attribute_iv_and_salt attr_encrypted :with_false_unless, :key => SECRET_KEY, :unless => false, mode: :per_attribute_iv_and_salt attr_encrypted :with_if_changed, :key => SECRET_KEY, :if => :should_encrypt attr_encrypted :with_allow_empty_value, key: SECRET_KEY, allow_empty_value: true, marshal: true attr_encryptor :aliased, :key => SECRET_KEY attr_accessor :salt attr_accessor :should_encrypt def initialize(email: nil) self.email = email self.salt = Time.now.to_i.to_s self.should_encrypt = true end private def secret_key SECRET_KEY end end class Admin < User attr_encrypted :testing end class SomeOtherClass extend AttrEncrypted def self.call(object) object.class end end class YetAnotherClass extend AttrEncrypted self.attr_encrypted_options[:encode_iv] = false attr_encrypted :email, :key => SECRET_KEY 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 } def initialize(email: nil, encode_iv: 'm', encode_salt: 'm', mode: :per_attribute_iv_and_salt) self.email = email @encode_iv = encode_iv @encode_salt = encode_salt @mode = mode end attr_reader :encode_iv, :encode_salt, :mode end class AttrEncryptedTest < Minitest::Test def setup @iv = SecureRandom.random_bytes(12) end def test_should_store_email_in_encrypted_attributes assert User.encrypted_attributes.include?(:email) end def test_should_not_store_salt_in_encrypted_attributes refute User.encrypted_attributes.include?(:salt) end def test_attr_encrypted_should_return_true_for_email assert User.attr_encrypted?('email') end def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line refute_equal User.encrypted_attributes[:email][:attribute], User.encrypted_attributes[:without_encoding][:attribute] end def test_attr_encrypted_should_return_false_for_salt assert !User.attr_encrypted?('salt') end def test_should_generate_an_encrypted_attribute assert User.new.respond_to?(:encrypted_email) end def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix assert User.new.respond_to?(:crypted_password_test) end def test_should_generate_an_encrypted_attribute_with_the_attribute_option assert User.new.respond_to?(:ssn_encrypted) end def test_should_not_encrypt_nil_value assert_nil User.encrypt_email(nil, iv: @iv) end def test_should_not_encrypt_empty_string_by_default assert_equal '', User.encrypt_email('', iv: @iv) end def test_should_encrypt_email refute_nil User.encrypt_email('test@example.com', iv: @iv) refute_equal 'test@example.com', User.encrypt_email('test@example.com', iv: @iv) end def test_should_encrypt_email_when_modifying_the_attr_writer @user = User.new assert_nil @user.encrypted_email @user.email = 'test@example.com' refute_nil @user.encrypted_email iv = @user.encrypted_email_iv.unpack('m').first salt = @user.encrypted_email_salt[1..-1].unpack('m').first assert_equal User.encrypt_email('test@example.com', iv: iv, salt: salt), @user.encrypted_email end def test_should_not_decrypt_nil_value assert_nil User.decrypt_email(nil, iv: @iv) end def test_should_not_decrypt_empty_string assert_equal '', User.decrypt_email('', iv: @iv) end def test_should_decrypt_email encrypted_email = User.encrypt_email('test@example.com', iv: @iv) refute_equal 'test@test.com', encrypted_email assert_equal 'test@example.com', User.decrypt_email(encrypted_email, iv: @iv) end def test_should_decrypt_email_when_reading @user = User.new assert_nil @user.email options = @user.encrypted_attributes[:email] iv = @user.send(:generate_iv, options[:algorithm]) encoded_iv = [iv].pack(options[:encode_iv]) salt = SecureRandom.random_bytes encoded_salt = @user.send(:prefix_and_encode_salt, salt, options[:encode_salt]) @user.encrypted_email = User.encrypt_email('test@example.com', iv: iv, salt: salt) @user.encrypted_email_iv = encoded_iv @user.encrypted_email_salt = encoded_salt assert_equal 'test@example.com', @user.email end def test_should_encrypt_with_encoding assert_equal User.encrypt_with_encoding('test', iv: @iv), [User.encrypt_without_encoding('test', iv: @iv)].pack('m') end def test_should_decrypt_with_encoding encrypted = User.encrypt_with_encoding('test', iv: @iv) assert_equal 'test', User.decrypt_with_encoding(encrypted, iv: @iv) assert_equal User.decrypt_with_encoding(encrypted, iv: @iv), User.decrypt_without_encoding(encrypted.unpack('m').first, iv: @iv) end def test_should_encrypt_with_custom_encoding assert_equal User.encrypt_with_encoding('test', iv: @iv), [User.encrypt_without_encoding('test', iv: @iv)].pack('m') end def test_should_decrypt_with_custom_encoding encrypted = User.encrypt_with_encoding('test', iv: @iv) assert_equal 'test', User.decrypt_with_encoding(encrypted, iv: @iv) assert_equal User.decrypt_with_encoding(encrypted, iv: @iv), User.decrypt_without_encoding(encrypted.unpack('m').first, iv: @iv) end def test_should_encrypt_with_marshaling @user = User.new @user.with_marshaling = [1, 2, 3] refute_nil @user.encrypted_with_marshaling end def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments assert_equal SillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), User.encrypt_credit_card('testing') end def test_should_evaluate_a_key_passed_as_a_symbol @user = User.new assert_nil @user.ssn_encrypted @user.ssn = 'testing' refute_nil @user.ssn_encrypted encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.ssn_encrypted_iv.unpack("m").first, :salt => @user.ssn_encrypted_salt.unpack("m").first ) assert_equal encrypted, @user.ssn_encrypted end def test_should_evaluate_a_key_passed_as_a_proc @user = User.new assert_nil @user.crypted_password_test @user.password = 'testing' refute_nil @user.crypted_password_test 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) assert_equal encrypted, @user.crypted_password_test end def test_should_use_options_found_in_the_attr_encrypted_options_attribute @user = User.new assert_nil @user.crypted_password_test @user.password = 'testing' refute_nil @user.crypted_password_test 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) assert_equal encrypted, @user.crypted_password_test end def test_should_inherit_encrypted_attributes assert_equal [User.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.encrypted_attributes.keys.collect { |key| key.to_s }.sort end def test_should_inherit_attr_encrypted_options assert !User.attr_encrypted_options.empty? assert_equal User.attr_encrypted_options, Admin.attr_encrypted_options end def test_should_not_inherit_unrelated_attributes assert SomeOtherClass.attr_encrypted_options.empty? assert SomeOtherClass.encrypted_attributes.empty? end def test_should_evaluate_a_symbol_option assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, :class) end def test_should_evaluate_a_proc_option assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, proc { |object| object.class }) end def test_should_evaluate_a_lambda_option assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, lambda { |object| object.class }) end def test_should_evaluate_a_method_option assert_equal SomeOtherClass, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, SomeOtherClass.method(:call)) end def test_should_return_a_string_option class_string = 'SomeOtherClass' assert_equal class_string, SomeOtherClass.new.send(:evaluate_attr_encrypted_option, class_string) end def test_should_encrypt_with_true_if @user = User.new assert_nil @user.encrypted_with_true_if @user.with_true_if = 'testing' refute_nil @user.encrypted_with_true_if 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) assert_equal encrypted, @user.encrypted_with_true_if end def test_should_not_encrypt_with_false_if @user = User.new assert_nil @user.encrypted_with_false_if @user.with_false_if = 'testing' refute_nil @user.encrypted_with_false_if assert_equal 'testing', @user.encrypted_with_false_if end def test_should_encrypt_with_false_unless @user = User.new assert_nil @user.encrypted_with_false_unless @user.with_false_unless = 'testing' refute_nil @user.encrypted_with_false_unless 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) assert_equal encrypted, @user.encrypted_with_false_unless end def test_should_not_encrypt_with_true_unless @user = User.new assert_nil @user.encrypted_with_true_unless @user.with_true_unless = 'testing' refute_nil @user.encrypted_with_true_unless assert_equal 'testing', @user.encrypted_with_true_unless end def test_should_encrypt_empty_with_truthy_allow_empty_value_option @user = User.new assert_nil @user.encrypted_with_allow_empty_value @user.with_allow_empty_value = '' refute_nil @user.encrypted_with_allow_empty_value assert_equal '', @user.with_allow_empty_value @user = User.new @user.with_allow_empty_value = nil refute_nil @user.encrypted_with_allow_empty_value assert_nil @user.with_allow_empty_value end def test_should_work_with_aliased_attr_encryptor assert User.encrypted_attributes.include?(:aliased) end def test_should_always_reset_options @user = User.new @user.with_if_changed = "encrypt_stuff" @user = User.new @user.should_encrypt = false @user.with_if_changed = "not_encrypted_stuff" assert_equal "not_encrypted_stuff", @user.with_if_changed assert_equal "not_encrypted_stuff", @user.encrypted_with_if_changed end def test_should_cast_values_as_strings_before_encrypting string_encrypted_email = User.encrypt_email('3', iv: @iv) assert_equal string_encrypted_email, User.encrypt_email(3, iv: @iv) assert_equal '3', User.decrypt_email(string_encrypted_email, iv: @iv) end def test_should_create_query_accessor @user = User.new assert !@user.email? @user.email = '' assert !@user.email? @user.email = 'test@example.com' assert @user.email? end def test_should_vary_iv_per_attribute @user = User.new @user.email = 'email@example.com' @user.password = 'p455w0rd' refute_equal @user.encrypted_email_iv, @user.crypted_password_test_iv end def test_should_generate_iv_per_attribute_by_default thing = YetAnotherClass.new(email: 'thing@thing.com') refute_nil thing.encrypted_email_iv end def test_should_vary_iv_per_instance @user1 = User.new @user1.email = 'email@example.com' @user2 = User.new @user2.email = 'email@example.com' refute_equal @user1.encrypted_email_iv, @user2.encrypted_email_iv refute_equal @user1.encrypted_email, @user2.encrypted_email end def test_should_vary_salt_per_attribute @user = User.new @user.email = 'email@example.com' @user.password = 'p455w0rd' refute_equal @user.encrypted_email_salt, @user.crypted_password_test_salt end def test_should_vary_salt_per_instance @user1 = User.new @user1.email = 'email@example.com' @user2 = User.new @user2.email = 'email@example.com' refute_equal @user1.encrypted_email_salt, @user2.encrypted_email_salt end def test_should_not_generate_salt_per_attribute_by_default thing = YetAnotherClass.new(email: 'thing@thing.com') assert_nil thing.encrypted_email_salt end def test_should_decrypt_second_record @user1 = User.new @user1.email = 'test@example.com' @user2 = User.new @user2.email = 'test@example.com' assert_equal 'test@example.com', @user1.decrypt(:email, @user1.encrypted_email) end def test_should_specify_the_default_algorithm assert YetAnotherClass.encrypted_attributes[:email][:algorithm] assert_equal YetAnotherClass.encrypted_attributes[:email][:algorithm], 'aes-256-gcm' end def test_should_not_encode_iv_when_encode_iv_is_false email = 'thing@thing.com' thing = YetAnotherClass.new(email: email) refute thing.encrypted_email_iv =~ base64_encoding_regex assert_equal thing.email, email end def test_should_base64_encode_iv_by_default phone_number = '555-555-5555' thing = YetAnotherClass.new thing.phone_number = phone_number assert thing.encrypted_phone_number_iv =~ base64_encoding_regex assert_equal thing.phone_number, phone_number end def test_should_generate_unique_iv_for_every_encrypt_operation user = User.new user.email = 'initial_value@test.com' original_iv = user.encrypted_email_iv user.email = 'revised_value@test.com' refute_equal original_iv, user.encrypted_email_iv end def test_should_not_generate_iv_for_attribute_when_if_option_is_false user = User.new user.with_false_if = 'derp' assert_nil user.encrypted_with_false_if_iv end def test_should_generate_iv_for_attribute_when_if_option_is_true user = User.new user.with_true_if = 'derp' refute_nil user.encrypted_with_true_if_iv user.with_true_if = Object.new refute_nil user.encrypted_with_true_if_iv end def test_should_not_generate_salt_for_attribute_when_if_option_is_false user = User.new user.with_false_if = 'derp' assert_nil user.encrypted_with_false_if_salt end def test_should_generate_salt_for_attribute_when_if_option_is_true user = User.new user.with_true_if = 'derp' refute_nil user.encrypted_with_true_if_salt end def test_should_generate_iv_for_attribute_when_unless_option_is_false user = User.new user.with_false_unless = 'derp' refute_nil user.encrypted_with_false_unless_iv end def test_should_not_generate_iv_for_attribute_when_unless_option_is_true user = User.new user.with_true_unless = 'derp' assert_nil user.encrypted_with_true_unless_iv end def test_should_generate_salt_for_attribute_when_unless_option_is_false user = User.new user.with_false_unless = 'derp' refute_nil user.encrypted_with_false_unless_salt end def test_should_not_generate_salt_for_attribute_when_unless_option_is_true user = User.new user.with_true_unless = 'derp' assert_nil user.encrypted_with_true_unless_salt end def test_should_not_by_default_generate_iv_when_attribute_is_empty user = User.new user.with_true_if = nil assert_nil user.encrypted_with_true_if_iv end end ruby-attr-encrypted-3.1.0/test/compatibility_test.rb000066400000000000000000000104011327457023100226530ustar00rootroot00000000000000# -*- encoding: utf-8 -*- require_relative 'test_helper' # Test to ensure that existing representations in database do not break on # migrating to new versions of this gem. This ensures that future versions of # this gem will retain backwards compatibility with data generated by earlier # versions. class CompatibilityTest < Minitest::Test class NonmarshallingPet < ActiveRecord::Base PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt') PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key' PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt') PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key' self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:insecure_mode] = true attr_encrypted :nickname, :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') } attr_encrypted :birthdate, :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') } end class MarshallingPet < ActiveRecord::Base PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt') PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key' PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt') PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key' self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:insecure_mode] = true attr_encrypted :nickname, :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true attr_encrypted :birthdate, :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true end def setup drop_all_tables create_tables end def test_nonmarshalling_backwards_compatibility pet = NonmarshallingPet.create!( :name => 'Fido', :encrypted_nickname => 'E4lJTxFG/EfkfPg5MpnriQ==', :encrypted_nickname_iv => 'z4Q8deE4h7f6S8NNZcbPNg==', :encrypted_nickname_salt => 'adcd833001a873db', :encrypted_birthdate => '6uKEAiFVdJw+N5El+U6Gow==', :encrypted_birthdate_iv => 'zxtc1XPssL4s2HwA69nORQ==', :encrypted_birthdate_salt => '4f879270045eaad7' ) assert_equal 'Fido', pet.name assert_equal 'Fido the Dog', pet.nickname assert_equal '2011-07-09', pet.birthdate end def test_marshalling_backwards_compatibility pet = MarshallingPet.create!( :name => 'Fido', :encrypted_nickname => 'EsQScJYkPw80vVGvKWkE37Px99HHpXPFjoEPTNa4rbs=', :encrypted_nickname_iv => 'fNq1OZcGvty4KfcvGTcFSw==', :encrypted_nickname_salt => '733b459b7d34c217', :encrypted_birthdate => '+VUlKQGfNWkOgCwI4hv+3qlGIwh9h6cJ/ranJlaxvU+xxQdL3H3cOzTcI2rkYkdR', :encrypted_birthdate_iv => 'Ka+zF/SwEYZKwVa24lvFfA==', :encrypted_birthdate_salt => 'd5e892d5bbd81566' ) assert_equal 'Fido', pet.name assert_equal 'Mummy\'s little helper', pet.nickname assert_equal Date.new(2011, 7, 9), pet.birthdate end private def create_tables ActiveRecord::Schema.define(:version => 1) do create_table :nonmarshalling_pets do |t| t.string :name t.string :encrypted_nickname t.string :encrypted_nickname_iv t.string :encrypted_nickname_salt t.string :encrypted_birthdate t.string :encrypted_birthdate_iv t.string :encrypted_birthdate_salt end create_table :marshalling_pets do |t| t.string :name t.string :encrypted_nickname t.string :encrypted_nickname_iv t.string :encrypted_nickname_salt t.string :encrypted_birthdate t.string :encrypted_birthdate_iv t.string :encrypted_birthdate_salt end end end end ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:' ruby-attr-encrypted-3.1.0/test/data_mapper_test.rb000066400000000000000000000027441327457023100222720ustar00rootroot00000000000000require_relative 'test_helper' DataMapper.setup(:default, 'sqlite3::memory:') class Client include DataMapper::Resource property :id, Serial property :encrypted_email, String property :encrypted_email_iv, String property :encrypted_email_salt, String property :encrypted_credentials, Text property :encrypted_credentials_iv, Text property :encrypted_credentials_salt, Text self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt attr_encrypted :email, :key => SECRET_KEY attr_encrypted :credentials, :key => SECRET_KEY, :marshal => true def initialize(attrs = {}) super attrs self.credentials ||= { :username => 'example', :password => 'test' } end end DataMapper.auto_migrate! class DataMapperTest < Minitest::Test def setup Client.all.each(&:destroy) end def test_should_encrypt_email @client = Client.new :email => 'test@example.com' assert @client.save refute_nil @client.encrypted_email refute_equal @client.email, @client.encrypted_email assert_equal @client.email, Client.first.email end def test_should_marshal_and_encrypt_credentials @client = Client.new assert @client.save refute_nil @client.encrypted_credentials refute_equal @client.credentials, @client.encrypted_credentials assert_equal @client.credentials, Client.first.credentials assert Client.first.credentials.is_a?(Hash) end def test_should_encode_by_default assert Client.attr_encrypted_options[:encode] end end ruby-attr-encrypted-3.1.0/test/legacy_active_record_test.rb000066400000000000000000000100621327457023100241420ustar00rootroot00000000000000# -*- encoding: utf-8 -*- require_relative 'test_helper' ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:' def create_people_table ActiveRecord::Schema.define(:version => 1) do create_table :legacy_people do |t| t.string :encrypted_email t.string :password t.string :encrypted_credentials t.string :salt end end end # The table needs to exist before defining the class create_people_table ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError) class LegacyPerson < ActiveRecord::Base self.attr_encrypted_options[:insecure_mode] = true self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:mode] = :single_iv_and_salt attr_encrypted :email, :key => 'a secret key' 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 ActiveSupport::Deprecation.silenced = true def after_initialize; end ActiveSupport::Deprecation.silenced = false after_initialize :initialize_salt_and_credentials protected def initialize_salt_and_credentials self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(5)).to_s) self.credentials ||= { :username => 'example', :password => 'test' } rescue ActiveRecord::MissingAttributeError end end class LegacyPersonWithValidation < LegacyPerson validates_presence_of :email validates_uniqueness_of :encrypted_email end class LegacyActiveRecordTest < Minitest::Test def setup drop_all_tables create_people_table end def test_should_decrypt_with_correct_encoding if defined?(Encoding) @person = LegacyPerson.create :email => 'test@example.com' assert_equal 'UTF-8', LegacyPerson.first.email.encoding.name end end def test_should_encrypt_email @person = LegacyPerson.create :email => 'test@example.com' refute_nil @person.encrypted_email refute_equal @person.email, @person.encrypted_email assert_equal @person.email, LegacyPerson.first.email end def test_should_marshal_and_encrypt_credentials @person = LegacyPerson.create refute_nil @person.encrypted_credentials refute_equal @person.credentials, @person.encrypted_credentials assert_equal @person.credentials, LegacyPerson.first.credentials end def test_should_find_by_email @person = LegacyPerson.create(:email => 'test@example.com') assert_equal @person, LegacyPerson.find_by_email('test@example.com') end def test_should_find_by_email_and_password LegacyPerson.create(:email => 'test@example.com', :password => 'invalid') @person = LegacyPerson.create(:email => 'test@example.com', :password => 'test') assert_equal @person, LegacyPerson.find_by_email_and_password('test@example.com', 'test') end def test_should_scope_by_email @person = LegacyPerson.create(:email => 'test@example.com') assert_equal @person, LegacyPerson.scoped_by_email('test@example.com').first rescue NoMethodError end def test_should_scope_by_email_and_password LegacyPerson.create(:email => 'test@example.com', :password => 'invalid') @person = LegacyPerson.create(:email => 'test@example.com', :password => 'test') assert_equal @person, LegacyPerson.scoped_by_email_and_password('test@example.com', 'test').first rescue NoMethodError end def test_should_encode_by_default assert LegacyPerson.attr_encrypted_options[:encode] end def test_should_validate_presence_of_email @person = LegacyPersonWithValidation.new assert !@person.valid? assert !@person.errors[:email].empty? || @person.errors.on(:email) end def test_should_validate_uniqueness_of_email @person = LegacyPersonWithValidation.new :email => 'test@example.com' assert @person.save @person2 = LegacyPersonWithValidation.new :email => @person.email assert !@person2.valid? assert !@person2.errors[:encrypted_email].empty? || @person2.errors.on(:encrypted_email) end end ruby-attr-encrypted-3.1.0/test/legacy_attr_encrypted_test.rb000066400000000000000000000257451327457023100243760ustar00rootroot00000000000000# -*- encoding: utf-8 -*- require_relative 'test_helper' class LegacySillyEncryptor def self.silly_encrypt(options) (options[:value] + options[:some_arg]).reverse end def self.silly_decrypt(options) options[:value].reverse.gsub(/#{options[:some_arg]}$/, '') end end class LegacyUser extend AttrEncrypted self.attr_encrypted_options[:key] = Proc.new { |user| user.class.to_s } # default key self.attr_encrypted_options[:insecure_mode] = true self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:mode] = :single_iv_and_salt attr_encrypted :email, :without_encoding, :key => 'secret key' attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test' attr_encrypted :ssn, :key => :salt, :attribute => 'ssn_encrypted' attr_encrypted :credit_card, :encryptor => LegacySillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test' attr_encrypted :with_encoding, :key => 'secret key', :encode => true attr_encrypted :with_custom_encoding, :key => 'secret key', :encode => 'm' attr_encrypted :with_marshaling, :key => 'secret key', :marshal => true attr_encrypted :with_true_if, :key => 'secret key', :if => true attr_encrypted :with_false_if, :key => 'secret key', :if => false attr_encrypted :with_true_unless, :key => 'secret key', :unless => true attr_encrypted :with_false_unless, :key => 'secret key', :unless => false attr_encrypted :with_if_changed, :key => 'secret key', :if => :should_encrypt attr_encryptor :aliased, :key => 'secret_key' attr_accessor :salt attr_accessor :should_encrypt def initialize self.salt = Time.now.to_i.to_s self.should_encrypt = true end end class LegacyAdmin < LegacyUser attr_encrypted :testing end class LegacySomeOtherClass extend AttrEncrypted def self.call(object) object.class end end class LegacyAttrEncryptedTest < Minitest::Test def test_should_store_email_in_encrypted_attributes assert LegacyUser.encrypted_attributes.include?(:email) end def test_should_not_store_salt_in_encrypted_attributes assert !LegacyUser.encrypted_attributes.include?(:salt) end def test_attr_encrypted_should_return_true_for_email assert LegacyUser.attr_encrypted?('email') end def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line refute_equal LegacyUser.encrypted_attributes[:email][:attribute], LegacyUser.encrypted_attributes[:without_encoding][:attribute] end def test_attr_encrypted_should_return_false_for_salt assert !LegacyUser.attr_encrypted?('salt') end def test_should_generate_an_encrypted_attribute assert LegacyUser.new.respond_to?(:encrypted_email) end def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix assert LegacyUser.new.respond_to?(:crypted_password_test) end def test_should_generate_an_encrypted_attribute_with_the_attribute_option assert LegacyUser.new.respond_to?(:ssn_encrypted) end def test_should_not_encrypt_nil_value assert_nil LegacyUser.encrypt_email(nil) end def test_should_not_encrypt_empty_string assert_equal '', LegacyUser.encrypt_email('') end def test_should_encrypt_email refute_nil LegacyUser.encrypt_email('test@example.com') refute_equal 'test@example.com', LegacyUser.encrypt_email('test@example.com') end def test_should_encrypt_email_when_modifying_the_attr_writer @user = LegacyUser.new assert_nil @user.encrypted_email @user.email = 'test@example.com' refute_nil @user.encrypted_email assert_equal LegacyUser.encrypt_email('test@example.com'), @user.encrypted_email end def test_should_not_decrypt_nil_value assert_nil LegacyUser.decrypt_email(nil) end def test_should_not_decrypt_empty_string assert_equal '', LegacyUser.decrypt_email('') end def test_should_decrypt_email encrypted_email = LegacyUser.encrypt_email('test@example.com') refute_equal 'test@test.com', encrypted_email assert_equal 'test@example.com', LegacyUser.decrypt_email(encrypted_email) end def test_should_decrypt_email_when_reading @user = LegacyUser.new assert_nil @user.email @user.encrypted_email = LegacyUser.encrypt_email('test@example.com') assert_equal 'test@example.com', @user.email end def test_should_encrypt_with_encoding assert_equal LegacyUser.encrypt_with_encoding('test'), [LegacyUser.encrypt_without_encoding('test')].pack('m') end def test_should_decrypt_with_encoding encrypted = LegacyUser.encrypt_with_encoding('test') assert_equal 'test', LegacyUser.decrypt_with_encoding(encrypted) assert_equal LegacyUser.decrypt_with_encoding(encrypted), LegacyUser.decrypt_without_encoding(encrypted.unpack('m').first) end def test_should_decrypt_utf8_with_encoding encrypted = LegacyUser.encrypt_with_encoding("test\xC2\xA0utf-8\xC2\xA0text") assert_equal "test\xC2\xA0utf-8\xC2\xA0text", LegacyUser.decrypt_with_encoding(encrypted) assert_equal LegacyUser.decrypt_with_encoding(encrypted), LegacyUser.decrypt_without_encoding(encrypted.unpack('m').first) end def test_should_encrypt_with_custom_encoding assert_equal LegacyUser.encrypt_with_custom_encoding('test'), [LegacyUser.encrypt_without_encoding('test')].pack('m') end def test_should_decrypt_with_custom_encoding encrypted = LegacyUser.encrypt_with_custom_encoding('test') assert_equal 'test', LegacyUser.decrypt_with_custom_encoding(encrypted) assert_equal LegacyUser.decrypt_with_custom_encoding(encrypted), LegacyUser.decrypt_without_encoding(encrypted.unpack('m').first) end def test_should_encrypt_with_marshaling @user = LegacyUser.new @user.with_marshaling = [1, 2, 3] refute_nil @user.encrypted_with_marshaling assert_equal LegacyUser.encrypt_with_marshaling([1, 2, 3]), @user.encrypted_with_marshaling end def test_should_decrypt_with_marshaling encrypted = LegacyUser.encrypt_with_marshaling([1, 2, 3]) @user = LegacyUser.new assert_nil @user.with_marshaling @user.encrypted_with_marshaling = encrypted assert_equal [1, 2, 3], @user.with_marshaling end def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments assert_equal LegacySillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), LegacyUser.encrypt_credit_card('testing') end def test_should_evaluate_a_key_passed_as_a_symbol @user = LegacyUser.new assert_nil @user.ssn_encrypted @user.ssn = 'testing' refute_nil @user.ssn_encrypted assert_equal Encryptor.encrypt(:value => 'testing', :key => @user.salt, insecure_mode: true, algorithm: 'aes-256-cbc'), @user.ssn_encrypted end def test_should_evaluate_a_key_passed_as_a_proc @user = LegacyUser.new assert_nil @user.crypted_password_test @user.password = 'testing' refute_nil @user.crypted_password_test assert_equal Encryptor.encrypt(:value => 'testing', :key => 'LegacyUser', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.crypted_password_test end def test_should_use_options_found_in_the_attr_encrypted_options_attribute @user = LegacyUser.new assert_nil @user.crypted_password_test @user.password = 'testing' refute_nil @user.crypted_password_test assert_equal Encryptor.encrypt(:value => 'testing', :key => 'LegacyUser', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.crypted_password_test end def test_should_inherit_encrypted_attributes assert_equal [LegacyUser.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, LegacyAdmin.encrypted_attributes.keys.collect { |key| key.to_s }.sort end def test_should_inherit_attr_encrypted_options assert !LegacyUser.attr_encrypted_options.empty? assert_equal LegacyUser.attr_encrypted_options, LegacyAdmin.attr_encrypted_options end def test_should_not_inherit_unrelated_attributes assert LegacySomeOtherClass.attr_encrypted_options.empty? assert LegacySomeOtherClass.encrypted_attributes.empty? end def test_should_evaluate_a_symbol_option assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, :class) end def test_should_evaluate_a_proc_option assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, proc { |object| object.class }) end def test_should_evaluate_a_lambda_option assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, lambda { |object| object.class }) end def test_should_evaluate_a_method_option assert_equal LegacySomeOtherClass, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, LegacySomeOtherClass.method(:call)) end def test_should_return_a_string_option class_string = 'LegacySomeOtherClass' assert_equal class_string, LegacySomeOtherClass.new.send(:evaluate_attr_encrypted_option, class_string) end def test_should_encrypt_with_true_if @user = LegacyUser.new assert_nil @user.encrypted_with_true_if @user.with_true_if = 'testing' refute_nil @user.encrypted_with_true_if assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.encrypted_with_true_if end def test_should_not_encrypt_with_false_if @user = LegacyUser.new assert_nil @user.encrypted_with_false_if @user.with_false_if = 'testing' refute_nil @user.encrypted_with_false_if assert_equal 'testing', @user.encrypted_with_false_if end def test_should_encrypt_with_false_unless @user = LegacyUser.new assert_nil @user.encrypted_with_false_unless @user.with_false_unless = 'testing' refute_nil @user.encrypted_with_false_unless assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.encrypted_with_false_unless end def test_should_not_encrypt_with_true_unless @user = LegacyUser.new assert_nil @user.encrypted_with_true_unless @user.with_true_unless = 'testing' refute_nil @user.encrypted_with_true_unless assert_equal 'testing', @user.encrypted_with_true_unless end def test_should_work_with_aliased_attr_encryptor assert LegacyUser.encrypted_attributes.include?(:aliased) end def test_should_always_reset_options @user = LegacyUser.new @user.with_if_changed = "encrypt_stuff" @user = LegacyUser.new @user.should_encrypt = false @user.with_if_changed = "not_encrypted_stuff" assert_equal "not_encrypted_stuff", @user.with_if_changed assert_equal "not_encrypted_stuff", @user.encrypted_with_if_changed end def test_should_cast_values_as_strings_before_encrypting string_encrypted_email = LegacyUser.encrypt_email('3') assert_equal string_encrypted_email, LegacyUser.encrypt_email(3) assert_equal '3', LegacyUser.decrypt_email(string_encrypted_email) end def test_should_create_query_accessor @user = LegacyUser.new assert !@user.email? @user.email = '' assert !@user.email? @user.email = 'test@example.com' assert @user.email? end end ruby-attr-encrypted-3.1.0/test/legacy_compatibility_test.rb000066400000000000000000000071511327457023100242070ustar00rootroot00000000000000# -*- encoding: utf-8 -*- require_relative 'test_helper' # Test to ensure that existing representations in database do not break on # migrating to new versions of this gem. This ensures that future versions of # this gem will retain backwards compatibility with data generated by earlier # versions. class LegacyCompatibilityTest < Minitest::Test class LegacyNonmarshallingPet < ActiveRecord::Base PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt') PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key' PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt') PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key' self.attr_encrypted_options[:insecure_mode] = true self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:mode] = :single_iv_and_salt attr_encrypted :nickname, :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') } attr_encrypted :birthdate, :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') } end class LegacyMarshallingPet < ActiveRecord::Base PET_NICKNAME_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-nickname-salt') PET_NICKNAME_KEY = 'my-really-really-secret-pet-nickname-key' PET_BIRTHDATE_SALT = Digest::SHA256.hexdigest('my-really-really-secret-pet-birthdate-salt') PET_BIRTHDATE_KEY = 'my-really-really-secret-pet-birthdate-key' self.attr_encrypted_options[:insecure_mode] = true self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:mode] = :single_iv_and_salt attr_encrypted :nickname, :key => proc { Encryptor.encrypt(:value => PET_NICKNAME_SALT, :key => PET_NICKNAME_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true attr_encrypted :birthdate, :key => proc { Encryptor.encrypt(:value => PET_BIRTHDATE_SALT, :key => PET_BIRTHDATE_KEY, insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true end def setup drop_all_tables create_tables end def test_nonmarshalling_backwards_compatibility pet = LegacyNonmarshallingPet.create!( :name => 'Fido', :encrypted_nickname => 'uSUB6KGzta87yxesyVc3DA==', :encrypted_birthdate => 'I3d691B2PtFXLx15kO067g==' ) assert_equal 'Fido', pet.name assert_equal 'Fido the Dog', pet.nickname assert_equal '2011-07-09', pet.birthdate end def test_marshalling_backwards_compatibility pet = LegacyMarshallingPet.create!( :name => 'Fido', :encrypted_nickname => '7RwoT64in4H+fGVBPYtRcN0K4RtriIy1EP4nDojUa8g=', :encrypted_birthdate => 'bSp9sJhXQSp2QlNZHiujtcK4lRVBE8HQhn1y7moQ63bGJR20hvRSZ73ePAmm+wc5' ) assert_equal 'Fido', pet.name assert_equal 'Mummy\'s little helper', pet.nickname assert_equal Date.new(2011, 7, 9), pet.birthdate end private def create_tables ActiveRecord::Schema.define(:version => 1) do create_table :legacy_nonmarshalling_pets do |t| t.string :name t.string :encrypted_nickname t.string :encrypted_birthdate t.string :salt end create_table :legacy_marshalling_pets do |t| t.string :name t.string :encrypted_nickname t.string :encrypted_birthdate t.string :salt end end end end ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:' ruby-attr-encrypted-3.1.0/test/legacy_data_mapper_test.rb000066400000000000000000000033631327457023100236140ustar00rootroot00000000000000require_relative 'test_helper' DataMapper.setup(:default, 'sqlite3::memory:') class LegacyClient include DataMapper::Resource self.attr_encrypted_options[:insecure_mode] = true self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:mode] = :single_iv_and_salt property :id, Serial property :encrypted_email, String property :encrypted_credentials, Text property :salt, String attr_encrypted :email, :key => 'a secret key', mode: :single_iv_and_salt 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 def initialize(attrs = {}) super attrs self.salt ||= Digest::SHA1.hexdigest((Time.now.to_i * rand(5)).to_s) self.credentials ||= { :username => 'example', :password => 'test' } end end DataMapper.auto_migrate! class LegacyDataMapperTest < Minitest::Test def setup LegacyClient.all.each(&:destroy) end def test_should_encrypt_email @client = LegacyClient.new :email => 'test@example.com' assert @client.save refute_nil @client.encrypted_email refute_equal @client.email, @client.encrypted_email assert_equal @client.email, LegacyClient.first.email end def test_should_marshal_and_encrypt_credentials @client = LegacyClient.new assert @client.save refute_nil @client.encrypted_credentials refute_equal @client.credentials, @client.encrypted_credentials assert_equal @client.credentials, LegacyClient.first.credentials assert LegacyClient.first.credentials.is_a?(Hash) end def test_should_encode_by_default assert LegacyClient.attr_encrypted_options[:encode] end end ruby-attr-encrypted-3.1.0/test/legacy_sequel_test.rb000066400000000000000000000033021327457023100226260ustar00rootroot00000000000000require_relative 'test_helper' DB.create_table :legacy_humans do primary_key :id column :encrypted_email, :string column :password, :string column :encrypted_credentials, :string column :salt, :string end class LegacyHuman < Sequel::Model(:legacy_humans) self.attr_encrypted_options[:insecure_mode] = true self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' self.attr_encrypted_options[:mode] = :single_iv_and_salt attr_encrypted :email, :key => 'a secret key', mode: :single_iv_and_salt 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 def after_initialize(attrs = {}) self.salt ||= Digest::SHA1.hexdigest((Time.now.to_i * rand(5)).to_s) self.credentials ||= { :username => 'example', :password => 'test' } end end class LegacySequelTest < Minitest::Test def setup LegacyHuman.all.each(&:destroy) end def test_should_encrypt_email @human = LegacyHuman.new :email => 'test@example.com' assert @human.save refute_nil @human.encrypted_email refute_equal @human.email, @human.encrypted_email assert_equal @human.email, LegacyHuman.first.email end def test_should_marshal_and_encrypt_credentials @human = LegacyHuman.new assert @human.save refute_nil @human.encrypted_credentials refute_equal @human.credentials, @human.encrypted_credentials assert_equal @human.credentials, LegacyHuman.first.credentials assert LegacyHuman.first.credentials.is_a?(Hash) end def test_should_encode_by_default assert LegacyHuman.attr_encrypted_options[:encode] end end ruby-attr-encrypted-3.1.0/test/run.sh000077500000000000000000000004271327457023100175700ustar00rootroot00000000000000#!/usr/bin/env sh -e for RUBY in 1.9.3 2.0.0 2.1 2.2 do for RAILS in 2.3.8 3.0.0 3.1.0 3.2.0 4.0.0 4.1.0 4.2.0 do if [[ $RUBY -gt 1.9.3 && $RAILS -lt 4.0.0 ]]; then continue fi RBENV_VERSION=$RUBY ACTIVERECORD=$RAILS bundle && bundle exec rake done done ruby-attr-encrypted-3.1.0/test/sequel_test.rb000066400000000000000000000027451327457023100213140ustar00rootroot00000000000000require_relative 'test_helper' DB.create_table :humans do primary_key :id column :encrypted_email, :string column :encrypted_email_salt, String column :encrypted_email_iv, :string column :password, :string column :encrypted_credentials, :string column :encrypted_credentials_iv, :string column :encrypted_credentials_salt, String end class Human < Sequel::Model(:humans) self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt attr_encrypted :email, :key => SECRET_KEY attr_encrypted :credentials, :key => SECRET_KEY, :marshal => true def after_initialize(attrs = {}) self.credentials ||= { :username => 'example', :password => 'test' } end end class SequelTest < Minitest::Test def setup Human.all.each(&:destroy) end def test_should_encrypt_email @human = Human.new :email => 'test@example.com' assert @human.save refute_nil @human.encrypted_email refute_equal @human.email, @human.encrypted_email assert_equal @human.email, Human.first.email end def test_should_marshal_and_encrypt_credentials @human = Human.new :credentials => { :username => 'example', :password => 'test' } assert @human.save refute_nil @human.encrypted_credentials refute_equal @human.credentials, @human.encrypted_credentials assert_equal @human.credentials, Human.first.credentials assert Human.first.credentials.is_a?(Hash) end def test_should_encode_by_default assert Human.attr_encrypted_options[:encode] end end ruby-attr-encrypted-3.1.0/test/test_helper.rb000066400000000000000000000030071327457023100212650ustar00rootroot00000000000000require 'simplecov' require 'simplecov-rcov' require "codeclimate-test-reporter" SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::RcovFormatter, CodeClimate::TestReporter::Formatter ] ) SimpleCov.start do add_filter 'test' end CodeClimate::TestReporter.start require 'minitest/autorun' # Rails 4.0.x pins to an old minitest unless defined?(MiniTest::Test) MiniTest::Test = MiniTest::Unit::TestCase end require 'active_record' require 'data_mapper' require 'digest/sha2' require 'sequel' ActiveSupport::Deprecation.behavior = :raise $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $:.unshift(File.dirname(__FILE__)) require 'attr_encrypted' DB = if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby Sequel.jdbc('jdbc:sqlite::memory:') else Sequel.sqlite end # The :after_initialize hook was removed in Sequel 4.0 # and had been deprecated for a while before that: # http://sequel.rubyforge.org/rdoc-plugins/classes/Sequel/Plugins/AfterInitialize.html # This plugin re-enables it. Sequel::Model.plugin :after_initialize SECRET_KEY = SecureRandom.random_bytes(32) def base64_encoding_regex /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/ end def drop_all_tables connection = ActiveRecord::Base.connection tables = (ActiveRecord::VERSION::MAJOR >= 5 ? connection.data_sources : connection.tables) tables.each { |table| ActiveRecord::Base.connection.drop_table(table) } end