0.0
No commit activity in last 3 years
No release in over 3 years
Generates yattr_accessors that encrypt and decrypt attributes transparently. Based on attr_encrypted by Sean Huber [https://github.com/shuber]
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
 Project Readme

YattrEncrypted

Version: 0.1.7 (but you should check lib/yattr_encrypted/version.rb to be sure)

Applicability

This code has been tested

  • using ruby 1.9.2-p290
  • stand alone
  • Rails 3.1.4

Description

This is based on code stolen from yattr_encrypted and encryptor - both by github.com/shuber - and both very fine gems.

So why another dumb attribute encryptor gem?

shuber's gems don't do what I want and they appear to be dormant.

The primary difference between shuber's gems and this one is a matter of flexibility and simplicity. yattr_encrypted is simple, easy to use and (should be) pretty secure. This comes at the expense of flexibility.

In yattr_encrypted you do not have a choice of algorithm, encoding, additional encrypt/decrypt methods, conditional encryption, or underlying data mapper. You also have to use Rails 3.1+.

In more detail, here is where they differ:

yattr_encrypted does not support:

  • DataMapper
  • Sequel
  • ActiveRecord find_by*** methods for encrypted fields
  • alternate encryption methods
  • alternate encryption algorithms
  • String#encrypt, #encrypt!, #decrypt, and #decrypt!
  • most of the options yattr_encrypted supports
  • conditional encrypting - yattr_encrypted supports conditionally encrypting fields based on some logic. I don't have a use case for this, so yattr_encrypted does not support it.

yattr_encrypted is also self contained - only relies on the openssl (part of the Ruby Standard Library), whereas the shuber gem depends on encryptor.

What yattr_encrypted does support:

  • yattr_encrypted ONLY works with ActiveRecord
  • random initial values for each encrypted attribute. This is done by creating a random iv and including it in the encrypted data. See openssl documentation for details [OpenSSL::Cipher]
  • detects when fields are modified by actions other than assignment. This supports encrypting complex types - such as hashes and arrays. This is implemented by adding
  • supports special field processing and initialization by use of :read_filter and :write_filter options which define Proc's or methods which are run on during read and write accessors for plaintext versions of the encrypted fields. a before_save calleback to the private method yattr_update_encrypted_values
  • Rails 3.1 & Rails 3.2 - doesn't pretend to support anything lower (but it might work)
  • adds encrypted fields to both attributes_protected (to avoid mass assignment) and Rails.application.config.filter_parameters
  • adds plaintext fields to Rails.application.config.filter_parameters - which is not needed inasmuch as they are not db fields, so they don't show up in the log anyway. (but it can't hurt - just in case)

Installation

Either

gem install yattr_encrypted

Or add to your Gemfile

gem yattr_encrypted

Usage

Name each field you want to encrypt in the yattr_encrypted macro.

For example, assume that you want to encrypt a field named foo. Then create a field in your migration named foo_encrypted.

class Foo < ActiveRecord::Base
  yattr_encrypted :foo
end

This will add accessor methods foo and foo= to your model. You do not use the actual foo_encrypted field directly.

NOTE: foo is not useable for search. If you want to search on encrypted fields, use attr_encrypted. yattr_encrypted does not create searchable fields because it automatically generates random initial values so it is not possible to generate matching encrypted values without retrieving the encrypted data.

Options

yattr_encrypted accepts five options:

  • :prefix - the prefix which is prepended to attribute to form the encrypted attribute name. Defaults to ''
  • :suffix - the suffix which appended to attribute to form the encrypted attribute name. Defaults to '_encrypted'
  • :key - allows attribute specific encryption keys. The default - if not specified - is Rails.application.config.secret_token
  • :read_filter - a LAMBDA (or Proc) which is called to modify the clear-text value of the attribute prior to returning it.
  • :write_filter - a LAMBDA (or Proc) which is called on the assigned value prior to assigning it to the attribute

These options are discussed in some detail below in the applicable section.

Encrypted Attribute Name

The encrypted field name defaults to <field>_encrypted. You can change this on a field by field basis using the :prefix and :suffix - which define strings which are prefixed and suffixed to the field name to create the encrypted field name. They default to:

:prefix = ''
:suffix = '_encrypted'

Notice that the underscore ('_') must be included in :prefix and :suffix, if you want them.

Encryption keys

The default encryption key is the value of <application>::Application.config.secret_token which is in config/initializers/secret_token.rb.

If you want to use some other key - on a field by field basis, you can specify the key on a field by field basis using the :key option.

NOTE: all encryption uses ase-256-cbc with random initial values. For some reason this triggers a key length check in openssl which raises an exception if your key is too short. I don't know what the required key length is, but secrete_token is long enough and 'this is a very long secret key' is not.

If you supply your own key, it can be a String or a Proc which returns a String.

Special Attribute Processing: read_filter and write_filter

Special processing for attribute values can be implemented by using the :read_filter and :write_filter options of the yattr_encrypted macro.

Both options take either a Proc, lambda, or instance method name. In both cases, the callable must take a single argument. The argument will be the plaintext value of the field or value being assigned.

A :read_filter is called on the value of the attribute before being returned by the clear text attribute accessor. The processed value is saved in the instance variable used to support the plaintext version of the attribute.

NOTE: The read filter is called every time the attribute is read via the attribute reader. It should be idempotent in the sense that:

read_filter(attribute) == read_filter(read_filter(attribute)).

NOTE: Because the read filter result is saved in the instance variable which supports the plain text version of the attribute, it can be used to set the attribute to a default value - such as an empty Hash.

yattr_encrypted :bag, :read_filter => lambda { |val| val.is_a?(Hash) ? val : {} }

A :write_filter is a proc, lambda, or instance method which accepts a single argument. It is called on the value passed to the attribute writer prior to any other action in the writer.

NOTE: the write filter is called on every value which is assigned to the attribute.

You can use a :write_filter to do some standard preprocessing on values. For example, if you want to normalize some string of text to lower case, with uniform whitespace, you might do something like:

yattr_encrypted :str_value, :write_filter => lambda { |val| val.to_s.sub(/\s+/, ' ').downcase.strip }

Encription Initial Values

As stated everywhere - random initial values are automatically generated for all fields. They are prepended to the actual encrypted data and stripped during decryption. You can't override this, nor can you provide your own initial values.

Encryption Method

yattr_encrypted only uses aes-256-cbc. If you want variety, use attr_encrypted, which supports the entire gamete supplied by openssl

Encoding of Encrypted Data and Database Compatibility

All data saved in the database is base64 encode. All fields are further serialized as JSON objects. This is to avoid dealing with any database idiodicy and to transparently handle complex data types being used in database fields [such as Hashes and Arrays].

The encoding code for a field is:

cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.encrypt
cipher.key = key
iv = cipher.random_iv   # ask OpenSSL for a new, random initial value

# jsonify data
value_marshalled = Marshal.dump value

# encrypt data
result = cipher.update value_marshalled
result << cipher.final

# return encrypted data and iv
Base64.encode64(("%04d" % iv.length) + iv + result)

The decoding code is:

 # initialize decryptor
cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.decrypt
cipher.key = key

# extract encrypted_value
encrypted_value = Base64.decode64 marshalled_value

# extract and set iv
iv_end = encrypted_value[0..3].to_i + 3
cipher.iv = encrypted_value[4..(iv_end)]
encrypted_value = encrypted_value[(iv_end+1)..-1]

# derypte and return
result = cipher.update(encrypted_value)
result << cipher.final

Marshal.load result