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. abefore_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