AttrEncrypter

Gem Version Test Status

Encrypts/Decrypts, with key rotation, attributes on classes, using RbNaCl.

This library was developed for- and extracted from HireFire.

The documentation can be found on RubyDoc.

Compatibility

  • Ruby 2.5+
  • RbNaCl 7.0+

Installation

Ensure that you've installed libsodium, then add the gem to your Gemfile and run bundle.

gem "attr_encrypter", "~> 1"

Example

class Account
  include AttrEncrypter::Accessors

  # Defines the token accessor that seamlessly encrypts and decrypts
  # data stored in @token_digest.
  #
  # Note: This won't define a @token instance variable, as state is
  #       managed by the token_digest accessor using @token_digest.
  #
  # Note: You can add multiple attributes in a single attr_encrypter call.
  #       e.g. `attr_encrypter ENV["KEYCHAIN"], :attr_a, :attr_b, :attr_c`
  #
  attr_encrypter ENV["KEYCHAIN"], :token

  # Defines the token_digest accessor for storing and retrieving encrypted data.
  # You don't directly use this accessor. Both read and write operations should
  # always go through the token/token= methods provided by the attr_encrypter method.
  #
  # Consider making this accessor private. We'll leave it public for this demo.
  #
  attr_accessor :token_digest
end

The KEYCHAIN environment variable must contain one or more keys. To generate a key, use the following function.

AttrEncrypter::Generator.generate_key

Place the generated key in the KEYCHAIN environment variable.

The key consists of two segments, <version>.<secret>. The initial key has a version of 1. Versions determine which secrets will be used to encrypt and decrypt data.

With the key in place you'll be able to store and retrieve data using the dynamically defined token accessor. Data assigned with token= will automatically be encrypted and stored with token_digest= in @token_digest.

account                 = Account.new
account.token           = "my_secret_token"
account.token_digest # => "1.yCZGH0QEk8+OPT0ad29wVmCkvr/7NXZjZtxu23v2j9HKfehndH0qv9MsF4ME\n"
account.token        # => "my_secret_token"

account.instance_variable_get("@token")        # => nil
account.instance_variable_get("@token_digest") # => "1.yCZGH0QEk8+OPT0ad29wVmCkvr/7NXZjZtxu23v2j9HKfehndH0qv9MsF4ME\n"

The @token_digest value, like the keychain, also consists of two segments, <version>.<data>. The version matches the key version used to encrypt the data. This way, whenever data is accessed via token, it knows which secret to use to decrypt the data.

To clear the token, simply assign nil to it.

account.token           = nil
account.token_digest # => nil

Active Record

This is a general purpose library, and while its only hard dependency is rbnacl (libsodium), it was designed to work seamlessly with Active Record.

class User < ActiveRecord::Base
  include AttrEncrypter::Accessors
  attr_encrypter ENV["KEYCHAIN"], :token
end

This assumes that you have a users table in the database, containing a column named token_digest of type text.

Key Rotation

You'll want to rotate your keys at some point.

First, add a second key to the keychain (ENV["KEYCHAIN"] in this case).

To generate version 2 you can again use the following function, but this time using the version argument.

AttrEncrypter::Generator.generate_key 2 # or 3, 4, 5...

The keychain format allows any kind of whitespace to delimit keys.

ENV["KEYCHAIN"] = "<version>.<secret> <version>.<secret> <version>.<secret>"

For example.

ENV["KEYCHAIN"] = <<-EOS
  1.c1dbb0dd094d4f40916c9cc0d8c974151949f105c499366b2acd9da76de7e5e9
  2.3054563b2d80ae13c6e115405d6263e70be004f81eb3982f4a61ff9e9821e7d4
EOS

Then simply reassign the token to re-encrypt the token with key version 2.

User.find_each do |user|
  user.token = user.token
  user.save
end

The user.token (reader) decrypts the existing encrypted token from token_digest back to its unencrypted form using key version 1. It then uses the highest key version in the keychain, version 2, to re-encrypt the token with user.token= which overwrites the key version 1-encrypted token in token_digest with the key version 2-encrypted token. We then persist the key version 2-encrypted token to the database.

Quick Facts

  • The order of the keys in the keychain is irrelevant.
  • The key with the highest version in the keychain is always used for encryption.
  • The key version used to encrypt data is stored along with the encrypted data (<version>.<secret>).
  • The version of the encrypted data determines which key's secret to use in order to decrypt data.
  • Keys can be safely removed from the keychain when none of the encrypted data requires it for decryption.
  • It's recommended that you increment key versions by 1 when generating a new key.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/mrrooijen/attr_encrypter.

First, install libsodium on your machine, and then install the remaining development dependencies:

$ bundle

To open an interactive console:

$ bundle console

To run the tests:

$ bundle exec rake

To view the code coverage (generated after each test run):

$ open coverage/index.html

To run the local documentation server:

$ bundle exec rake doc

To build a gem:

$ bundle exec rake build

To build and install a gem on your local machine:

$ bundle exec rake install

For a list of available tasks:

$ bundle exec rake --tasks

Author / License

Released under the MIT License by Michael van Rooijen.