Mongoid

Contents

Symmetric Encryption adds an encrypted: option to Mongoid fields. Declaring it generates a getter, a setter, and a _changed? method for the decrypted value, so the document is worked with in the clear while MongoDB only ever holds ciphertext.

Encrypting Active Record attributes instead? See Rails.

Step 1: Encrypt a field

The field holds the encrypted value, so it is declared type: String and named with an encrypted_ prefix:

class User
  include Mongoid::Document

  field :name,                             type: String
  field :encrypted_bank_account_number,    type: String, encrypted: true
end

Symmetric Encryption generates accessors for the name with the prefix removed:

user = User.create!(name: "Jack", bank_account_number: "12345")

user.bank_account_number
# => "12345"

MongoDB holds only the encrypted value:

{
  "name" : "Jack",
  "encrypted_bank_account_number" : "QEVuQwJAEACOYREfF1cAXU0B8Xre7bISBCV415agBWeiX6cF1boT2g=="
}

Reading the field is what decrypts it. A document that is loaded but never asked for the decrypted value never decrypts anything, and the decrypted value is cached until the encrypted one changes.

Naming the accessors yourself

The field name must start with encrypted_, or :decrypt_as must say what to call the accessors:

field :ssn_encrypted, type: String, encrypted: {decrypt_as: :ssn}

Without either, declaring the field raises an ArgumentError.

Step 2: Pass options

encrypted: true accepts the defaults. Supply a Hash to change them:

class User
  include Mongoid::Document

  field :name,                             type: String
  field :encrypted_bank_account_number,    type: String, encrypted: true
  field :encrypted_social_security_number, type: String, encrypted: true
  field :encrypted_life_history,           type: String, encrypted: {compress: true, random_iv: true}
  field :encrypted_age,                    type: String, encrypted: {type: :integer}
end

Note that random_iv defaults to false here, the opposite of the Active Record attribute type. Turn it on for any field you do not query:

field :encrypted_life_history, type: String, encrypted: {random_iv: true}

The field is stored as a String whatever type: says, because the encrypted value is always text. type: only decides what the getter coerces the decrypted value back into, so user.age returns an Integer.

Step 3: Query an encrypted field

Only the encrypted field exists in MongoDB, so queries use the encrypted name and the encrypted value:

user = User.where(encrypted_bank_account_number: SymmetricEncryption.encrypt("12345")).first

This works only when the field was encrypted with random_iv: false, the default here. With a random initialization vector the same input encrypts to a different value every time, so nothing matches.

Querying by the decrypted name finds nothing, and does not raise:

User.where(bank_account_number: "12345").first
# => nil, always. There is no such field in MongoDB.

Only equality works. Ranges, ordering, and regular expressions operate on ciphertext and are meaningless.

Validations

To ensure a value is encrypted before it is saved:

class User
  include Mongoid::Document

  field :encrypted_ssn, type: String, encrypted: true

  validates :encrypted_ssn, symmetric_encryption: true
end

Standalone Mongoid

Outside Rails, initialize both libraries yourself:

require "mongoid"
require "symmetric-encryption"

Mongoid.logger = Logger.new($stdout)
Mongoid.load!("config/mongoid.yml")

SymmetricEncryption.load!("config/symmetric-encryption.yml", "production")

Next steps