Gem Version Build Status Test Coverage Code Climate security

attr_redactor

Generates attr_accessors that transparently redact a hash attribute by removing, digesting or encrypting certain keys.

This code is based off of the attr_encrypted/attr_encrypted code base.

Helper classes for ActiveRecord,

DataMapper, and Sequel helpers have been retained, but have not been successfully tested.

Installation

Add attr_redactor to your gemfile:

  gem "attr_redactor"

Then install the gem:

  bundle install

Usage

If you're using an ORM like ActiveRecord using attr_redactor is easy:

  class User
    attr_redactor :user_data, redact: { :ssn => :remove, :email => :encrypt }
  end

If you're using a PORO, you have to do a little bit more work by extending the class:

  class User
    extend AttrRedactor
    attr_accessor :name
    attr_redactor :user_data, redact: { :ssn => :remove, :email => :encrypt }, encryption_key: 'YOUR ENCRYPTION KEY'

    def load
      # loads the stored data
    end

    def save
      # saves the :name and :redacted_user_data attributes somewhere (e.g. filesystem, database, etc)
    end
  end

  user = User.new
  user.user_data = { ssn: '123-45-6789', email: '[email protected]' }
  user.redacted_data[:encrypted_email] # returns the encrypted version of :ssn
  user.redacted_data.has_key?(:email) # false
  user.save

  user = User.load
  user.data { email: '[email protected]' }

When you set a redacted attribute the data is immediately redacted and the attribute replaced. This is to avoid confusion or inconsistency when passing a record around that might be freshly created or loaded from the DB.

  user = User.new user_data: { ssn: '123-45-6789', email: '[email protected]' }

  user.user_data[:ssn] # nil

Note on Updating Data

Changes within the hash may not be saved

To ensure ActiveRecord saves changed data, you should always update the hash entirely, not keys within the hash.

  user = User.new user_data: { ssn: '123-45-6789', email: '[email protected]' }

  user.user_data[:email] = '[email protected]'
  user.save!

  user.reload
  user.user_data[:email] # '[email protected]'

Using a dynamic redact hash

The value of redact can be a symbol, lambda or proc. If it's a symbol, that method will be called on the object itself. This allows you to create custom redact hashes on a per record basis.

  class User < ActiveRecord::Base
    belongs_to :company
    attr_redactor :user_data, :redact => :redact_hash

    def redact_hash
      company.redact_settings
    end
  end

  user = User.new user_data: { ssn: '123-45-6789', email: '[email protected]' }

  # What get's redacted in user_data depends on the company settings for that user

attr_redacted with database persistence

By default, attr_redacted stores the redacted data in :redacted_<attribute>.

Create or modify the table that your model uses to add a column with the redacted_ prefix (which can be modified, see below), e.g. redacted_ssn via a migration like the following:

  create_table :users do |t|
    t.string :name
    t.jsonb :redacted_user_data
    t.timestamps
  end

Specifying the redacted attribute name

By default, the redacted attribute name is redacted_#{attribute} (e.g. attr_redacted :data would create an attribute named redacted_data). So, if you're storing the redacted attribute in the database, you need to make sure the redacted_#{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_redacted options

Options are evaluated

All options will be evaluated at the instance level.

Default options

The following are the default options used by attr_redacted:

  prefix:            'redacted_',
  suffix:            '',
  marshal:           false,
  marshaler:         Marshal,
  dump_method:       'dump',
  load_method:       'load',

Additionally, you can specify default options for all redacted attributes in your class. Instead of having to define your class like this:

  class User
    attr_redacted :email, prefix: '', suffix: '_redacted'
    attr_redacted :ssn, prefix: '', suffix: '_redacted'
    attr_redacted :credit_card, prefix: '', suffix: '_redacted'
  end

You can simply define some default options like so:

  class User
    attr_redacted_options.merge!(prefix: '', :suffix => '_crypted')
    attr_redacted :email
    attr_redacted :ssn
    attr_redacted :credit_card
  end

This should help keep your classes clean and DRY.

The :attribute option

You can simply pass the name of the redacted attribute as the :attribute option:

  class User
    attr_redacted :data, attribute: 'obfuscated_data'
  end

This would generate an attribute named obfuscated_data

The :prefix and :suffix options

If you don't like the redacted_#{attribute} naming convention then you can specify your own:

  class User
    attr_redacted :data, prefix: 'secret_', suffix: '_hidden'
  end

This would generate the attribute: secret_data_hidden.

The :encryption_key option

Specifies the encryption key to use for encrypted data in the hash. This must be present if you use encryption.

The :digest_salt option

Specifies a salt to use when digesting. If not present then your data will be hashed without a salt which makes it less secure.

The :encode, :encode_iv, and :default_encoding options

You're probably going to be storing your redacted attributes somehow (e.g. filesystem, database, etc). You can pass the :encode option to automatically encode/decode when encrypting/hashing/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.

  class User
    attr_redacted :data, encode: true, encode_iv: true
  end

The default encoding is m (base64). You can change this by setting encode: 'some encoding'. See Arrary#pack for more encoding options.

ORMs

ActiveRecord

If you're using this gem with ActiveRecord, you get a few extra features:

Things to consider before using attr_redacted

Data gone immediately

Obviously, anything you decide to digest or remove, that will be done immediately and you will have no way to recover that data (except keeping a copy of the original hash)

Searching, joining, etc

You cannot search encrypted or hashed data (or, obviously, removed data), and because you can't search it, you can't index it either. You also can't use joins on the redacted 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. Since redacting uses a hash that is comparable, you could still index digested columns

Data leaks

Please also consider where your data leaks. If you're using attr_redacted 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 Please also consider other possible leak points.

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.

Testing

To verify you've configured redaction properly in your tests, use attr_redacted? and attr_redact_hash

Given class:

  class User
    attr_redacted :data, redact: { :ssn => :remove, :email => :encrypt }
  end

Minitest

  def test_should_redact_dataa
    expected_hash = { :ssn => :remove, :email => :encrypt }
    assert User.new.attr_redacted?(:data)
    assert_equal expected_hash, User.new.data_redact_hash
  end

RSpec

  it "should redact data"
    expect(User.new.attr_redacted?(:data)).to be_truthy
    expect(User.new.data_redact_hash).to eq({ :ssn => :remove, :email => :encrypt })
  end

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.