The project is in a healthy, maintained state
A narrow production-oriented field encryption layer: AES-256-GCM envelope, HMAC-SHA256 blind indexes, Ruby extension, MySQL UDF source, admin SQL contracts, and migration helpers.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0
 Project Readme

secure_db_fields

secure_db_fields is a narrow field-level encryption layer for Ruby/Rails + MySQL 5.7.

Compatibility target: Ruby >= 2.7.1 and MySQL 5.7. The gem intentionally avoids Ruby 3-only language/API features and the UDF avoids MySQL 8-only features such as functional indexes. It follows the locked design from docs/final_decision.md:

  • AES-256-GCM randomized envelope for confidential values.
  • HMAC-SHA256 blind indexes for exact indexed search.
  • Physical *_bidx columns, not generated columns.
  • MySQL UDFs for admin views/search helpers in DBeaver/DataGrip.
  • Keys outside the database and never passed as SQL arguments.
  • No pq_crypto, no OPE/FPE/range-preserving encryption, no transparent arbitrary LIKE.

Threat model

Protected: raw table / SQL dump / physical column data leaked without key files.

Not protected: DB host compromise, key-file compromise, or authorized admin access to decrypt UDF/views.

Ruby quick start

require "secure_db_fields"

keyring = SecureDBFields::Keyring.new("/etc/secure_db_fields/keys.env")
uid = SecureRandom.random_bytes(16)
phone = "+77771234567"

aad = SecureDBFields::Crypto.aad("clients", "phone", uid)
enc = SecureDBFields::Crypto.encrypt(phone, key: keyring.encryption_key(1), aad: aad, key_id: 1)
bidx = SecureDBFields::Crypto.phone_blind_index(phone, key: keyring.blind_index_key("PHONE"))

SecureDBFields::Crypto.decrypt(enc, key: keyring.encryption_key(1), aad: aad)
# => "+77771234567"

MySQL exact search template

SELECT
  id,
  secure_db_fields_decrypt_field(phone_enc, 'clients', 'phone', secure_row_uid) AS phone,
  created_at
FROM clients
WHERE phone_bidx = secure_phone_bidx('+77771234567');

Do not search by plaintext through the decrypt view:

-- forbidden on large tables: decrypt-scan
SELECT * FROM admin_clients_readable WHERE phone = '+77771234567';

Prefix search

Prefix search is not generic SQL LIKE. It is a named search mode with a physical column:

phone_bidx_p7 BINARY(32), INDEX idx_clients_phone_bidx_p7(phone_bidx_p7)

Only add prefix columns confirmed by query inventory. last4/suffix is intentionally not part of the default schema because it has very low cardinality and high leakage.

MySQL 5.7 DBA handoff bundle

The application developer can build a self-contained database-host archive and pass it to the DBA:

bundle exec secure_db_fields db package mysql --output secure_db_fields-mysql.tar.gz --force

On the MySQL 5.7 server the DBA extracts the archive and runs:

make verify
make doctor
sudo make install
make enable
make status

The command creates or reuses config/secure_db_fields/keys.env and includes that same key contract in the DBA archive. make install installs it on the DB host as /etc/secure_db_fields/keys.env, so the application and MySQL use the same keys without manual HEX copying.

Readable view SQL can be generated from the application side as well:

bundle exec secure_db_fields db view mysql \
  --table app.clients \
  --field phone:phone_enc \
  --uid-column secure_row_uid \
  --view admin.clients_readable \
  --columns id,created_at \
  --output clients_readable.sql