Symmetric Encryption Encrypt data at rest. Rotate keys without downtime.

API

Contents

Reference for the methods this gem exposes. For a walk through of how to use them, start with the Guide.

SymmetricEncryption.encrypt

SymmetricEncryption.encrypt(value, random_iv: SymmetricEncryption.randomize_iv?, compress: false,
                            type: :string, version: nil, header: nil)

Encrypts the supplied value and encodes the result.

Returns the encoded, encrypted value. Base64 text for every encoding other than :none, which returns raw binary. Returns nil when the value is nil, and "" when it is an empty string.

Parameter Description
value The value to encrypt. Anything that is not a String has #to_s called on it, unless type: says otherwise.
random_iv Use a new random initialization vector every time, so the same input does not encrypt to the same output. Set to false when the value must be searchable. Default: SymmetricEncryption.randomize_iv?, itself false unless configured otherwise.
compress Compress before encrypting. Worth it only for large values. Default: false
type The type of the value being encrypted. One of SymmetricEncryption::COERCION_TYPES. Default: :string
version Encrypt with the configured cipher that has this version, rather than the primary one. The version is written into the header, so nothing is needed when decrypting. Default: the primary cipher.
header Whether to add the header. Default: the cipher’s always_add_header, itself true.

A header is added regardless of header: whenever it is needed to decrypt the value again: when random_iv or compress is true, or when the cipher is an authenticated one.

SymmetricEncryption.encrypt("Hello World")
# => "QEVuQwAANIuPIXv/ii1IP1dF6T0NpQ=="

SymmetricEncryption.encrypt(21, type: :integer)
SymmetricEncryption.encrypt("Hello World", random_iv: true, compress: true)

Size

An encrypted value is longer than its input. Base64 expands by roughly a third, and the header adds:

  Header size
Header only 6 bytes
With a random IV, aes-256-cbc 24 bytes

random_iv: true adds 2 bytes for the length plus the initialization vector itself, which is 16 bytes for aes-256-cbc. An encrypted key or cipher name in the header adds more again.

SymmetricEncryption.decrypt

SymmetricEncryption.decrypt(encrypted_and_encoded_string, version: nil, type: :string)

Decodes and decrypts the supplied value.

Returns the decrypted value, coerced to type. Returns nil when the value is nil, and "" when it is an empty string.

Parameter Description
encrypted_and_encoded_string The value to decrypt.
version Which cipher to decrypt with, for a value that has no header. Ignored when the value has one, since the header names its own version.
type The type the value was encrypted as. Must match what was supplied to encrypt. Default: :string

Raises OpenSSL::Cipher::CipherError when the value cannot be decrypted, and SymmetricEncryption::CipherError when no configured cipher has the version the header asks for.

Decryption never falls back to another cipher when one fails. Decrypting with the wrong key can quietly succeed and return meaningless data, so a failure is reported rather than guessed at.

SymmetricEncryption.try_decrypt

SymmetricEncryption.try_decrypt(str)

As decrypt, but returns nil instead of raising when the value cannot be decrypted.

Intended for configuration files that hold passwords for several environments, where the current environment holds the key for only one of them:

password: <%= SymmetricEncryption.try_decrypt("QEVuQwJAEACOYREfF1cAXU0B...") %>

The value it returns should not be relied upon, since it is possible to decrypt data with the wrong key and get something back.

SymmetricEncryption.encrypted?

SymmetricEncryption.encrypted?(value)

Returns whether the value was encrypted by this library, by looking for the header. It does not attempt to decrypt, so it is cheap and does not need the right key.

SymmetricEncryption.encrypted?("QEVuQwAANIuPIXv/ii1IP1dF6T0NpQ==")
# => true
SymmetricEncryption.encrypted?("Hello World")
# => false

Only reliable for values that carry a header, which is the default. nil and "" return false.

SymmetricEncryption.with_cipher

SymmetricEncryption.with_cipher(cipher, secondary_ciphers: []) { ... }

Uses the supplied cipher for the duration of the block instead of the configured primary cipher.

Intended for data encrypted with a key of its own, held somewhere other than symmetric-encryption.yml, such as a key per customer read from a database table. See Key Rotation.

SymmetricEncryption.with_cipher(customer.cipher) do
  person.save!
end

Everything encrypted or decrypted inside the block uses that cipher, including Active Record attributes, Mongoid fields, files and streams. The configured ciphers are still searched when decrypting, so data encrypted before the block is still readable inside it.

Threads and fibers started inside the block inherit the scope. A thread that already existed does not, so handing work to a thread pool inside the block runs that work without the scope.

SymmetricEncryption.cipher

SymmetricEncryption.cipher            # The primary cipher
SymmetricEncryption.cipher(3)         # The configured cipher with version 3
SymmetricEncryption.cipher?           # Whether a primary cipher has been set
SymmetricEncryption.secondary_ciphers # Ciphers that are only ever decrypted with

cipher(version) raises SymmetricEncryption::CipherError when no configured cipher has that version, naming the versions that are available.

SymmetricEncryption.load!

SymmetricEncryption.load!(file_name = nil, env = nil)

Loads the configuration. Not needed in Rails, where the railtie does it during boot.

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

Defaults to Rails.root/config/symmetric-encryption.yml and Rails.env, honouring SYMMETRIC_ENCRYPTION_CONFIG and SYMMETRIC_ENCRYPTION_ENV.

SymmetricEncryption.random_password

SymmetricEncryption.random_password(size = 22)

Returns a random URL-safe base64 password.

Settings

SymmetricEncryption.cipher = cipher            # Set the primary cipher directly
SymmetricEncryption.secondary_ciphers = [...]  # Ciphers that are only decrypted with
SymmetricEncryption.randomize_iv = true        # Default for random_iv:. Default: false
SymmetricEncryption.filter_encrypted_attributes = false  # See below. Default: true
SymmetricEncryption.select_cipher { |encoded, decoded| ... }  # For headerless values

filter_encrypted_attributes controls whether Active Record attributes declared :encrypted are added to the model’s filter_attributes. It is applied when the attribute is declared, so it has to be set before the models are loaded. See Rails.

Writer and Reader

Encrypt and decrypt files and IO streams. See Files for the full treatment.

SymmetricEncryption::Writer.open(file_name_or_stream, compress: nil, **args) { |file| ... }
SymmetricEncryption::Writer.write(file_name_or_stream, data, **args)
SymmetricEncryption::Writer.encrypt(source:, target:, **args)

SymmetricEncryption::Reader.open(file_name_or_stream, buffer_size: 16_384, **args) { |file| ... }
SymmetricEncryption::Reader.read(file_name_or_stream, **args)
SymmetricEncryption::Reader.decrypt(source:, target:, **args)
SymmetricEncryption::Reader.empty?(file_name_or_stream)
SymmetricEncryption::Reader.header_present?(file_name)

Writer.encrypt and Reader.decrypt both return the number of unencrypted bytes that passed through them, not the size of the encrypted file.

Writer.new accepts version:, cipher_name:, header:, random_key:, random_iv:, compress: and chunk_size:. Reader.new accepts buffer_size: and version:.

Cipher

Building a cipher by hand, rather than loading one from the configuration file:

SymmetricEncryption::Cipher.new(
  key:               "...",              # Required. Raw binary, exactly the cipher's key length.
  iv:                "...",              # Optional. Raw binary, exactly the cipher's iv length.
  cipher_name:       "aes-256-cbc",      # Default: aes-256-cbc
  version:           0,                  # 0..255. Default: 0
  always_add_header: true,               # Default: true
  encoding:          :base64strict       # Default: :base64strict
)

Encodings: :base64strict, :base64, :base64urlsafe, :base16, :none.

key and iv are raw binary data of an exact length, not text. See Configuration.

Coercion types

SymmetricEncryption::COERCION_TYPES:

Type Ruby class
:string String
:integer Integer
:float Float
:decimal BigDecimal
:datetime DateTime
:time Time
:date Date
:boolean TrueClass or FalseClass
:json Serialized with JSON
:yaml Serialized with YAML

Next steps