secure_db_fields 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +98 -0
  4. data/Rakefile +16 -0
  5. data/db_deployment/mysql/Makefile +62 -0
  6. data/db_deployment/mysql/README.md +33 -0
  7. data/db_deployment/mysql/bin/common +199 -0
  8. data/db_deployment/mysql/bin/disable +40 -0
  9. data/db_deployment/mysql/bin/doctor +57 -0
  10. data/db_deployment/mysql/bin/enable +56 -0
  11. data/db_deployment/mysql/bin/install +64 -0
  12. data/db_deployment/mysql/bin/status +11 -0
  13. data/db_deployment/mysql/bin/uninstall +46 -0
  14. data/db_deployment/mysql/bin/verify +27 -0
  15. data/db_deployment/mysql/sql/examples.sql +20 -0
  16. data/db_deployment/mysql/sql/install.sql +20 -0
  17. data/db_deployment/mysql/sql/uninstall.sql +8 -0
  18. data/docs/final_decision.md +202 -0
  19. data/docs/implementation_notes.md +63 -0
  20. data/docs/ruby_27_mysql_57.md +33 -0
  21. data/examples/clients_phone_migration.sql +6 -0
  22. data/examples/keys.env.example +6 -0
  23. data/examples/ruby_usage.rb +15 -0
  24. data/exe/secure_db_fields +49 -0
  25. data/ext/secure_db_fields/extconf.rb +21 -0
  26. data/ext/secure_db_fields/secure_db_fields_core.c +723 -0
  27. data/ext/secure_db_fields/secure_db_fields_core.h +140 -0
  28. data/ext/secure_db_fields/secure_db_fields_ext.c +426 -0
  29. data/lib/secure_db_fields/active_record.rb +58 -0
  30. data/lib/secure_db_fields/crypto.rb +115 -0
  31. data/lib/secure_db_fields/db_deployment.rb +285 -0
  32. data/lib/secure_db_fields/keys.rb +69 -0
  33. data/lib/secure_db_fields/phone.rb +17 -0
  34. data/lib/secure_db_fields/version.rb +5 -0
  35. data/lib/secure_db_fields.rb +8 -0
  36. data/mysql_udf/Makefile +25 -0
  37. data/mysql_udf/README.md +51 -0
  38. data/mysql_udf/bin/install +11 -0
  39. data/mysql_udf/bin/uninstall +4 -0
  40. data/mysql_udf/bin/verify +3 -0
  41. data/mysql_udf/sql/examples.sql +20 -0
  42. data/mysql_udf/sql/install.sql +20 -0
  43. data/mysql_udf/sql/uninstall.sql +8 -0
  44. data/mysql_udf/src/mysql_udf_abi_57.h +34 -0
  45. data/mysql_udf/src/secure_db_fields_mysql.c +513 -0
  46. data/mysql_udf/test/run_e2e.sh +216 -0
  47. metadata +137 -0
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ MYSQL="${MYSQL:-mysql}"
6
+ DEFAULTS_FILE=""
7
+ MYSQL_SOCKET="${MYSQL_SOCKET:-}"
8
+
9
+ usage() {
10
+ cat <<'TEXT'
11
+ Usage: sudo MYSQL_SOCKET=/run/mysqld/mysqld.sock ./bin/uninstall [--mysql PATH] [--defaults-extra-file PATH]
12
+
13
+ Removes SecureDBFields-owned UDF registrations and its native library. The
14
+ Makefile requires CONFIRM=REMOVE_SECURE_DB_FIELDS before this script can run.
15
+ TEXT
16
+ }
17
+
18
+ while [ "$#" -gt 0 ]; do
19
+ case "$1" in
20
+ --mysql)
21
+ [ "$#" -ge 2 ] || { echo '--mysql requires a path' >&2; exit 2; }
22
+ MYSQL="$2"; shift 2 ;;
23
+ --defaults-extra-file)
24
+ [ "$#" -ge 2 ] || { echo '--defaults-extra-file requires a path' >&2; exit 2; }
25
+ DEFAULTS_FILE="$2"; shift 2 ;;
26
+ --help|-h)
27
+ usage; exit 0 ;;
28
+ *)
29
+ echo "secure_db_fields mysql uninstall: unknown option: $1" >&2; exit 2 ;;
30
+ esac
31
+ done
32
+
33
+ [ "${EUID:-$(id -u)}" -eq 0 ] || { echo 'secure_db_fields mysql uninstall: run as root' >&2; exit 2; }
34
+
35
+ source "$ROOT/bin/common"
36
+ sdf_resolve_mysql
37
+ sdf_mysql_args
38
+ sdf_require_mysql57
39
+ sdf_require_expected_socket
40
+ sdf_read_udf_state
41
+ sdf_require_clean_owned_state
42
+ PLUGIN_DIR="$(sdf_plugin_dir)"
43
+ sdf_drop_owned_udfs
44
+ rm -f "$PLUGIN_DIR/$SDF_UDF_LIBRARY"
45
+
46
+ echo "SecureDBFields MySQL UDFs disabled and library removed from $PLUGIN_DIR."
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+
7
+ if command -v sha256sum >/dev/null 2>&1; then
8
+ sha256sum -c SHA256SUMS
9
+ elif command -v shasum >/dev/null 2>&1; then
10
+ shasum -a 256 -c SHA256SUMS
11
+ else
12
+ echo 'secure_db_fields mysql verify: sha256sum or shasum is required' >&2
13
+ exit 2
14
+ fi
15
+
16
+ test -f src/secure_db_fields_mysql.c
17
+ test -f src/secure_db_fields_core.c
18
+ test -f src/secure_db_fields_core.h
19
+ test -f src/mysql_udf_abi_57.h
20
+ test -f sql/install.sql
21
+ test -f sql/uninstall.sql
22
+ if find . -name '.DS_Store' | grep -q .; then
23
+ echo 'deployment bundle must not contain .DS_Store' >&2
24
+ exit 2
25
+ fi
26
+ cc -DSDF_MYSQL_UDF_ABI_57 -Isrc -fsyntax-only src/secure_db_fields_mysql.c
27
+ printf 'secure_db_fields mysql bundle verify: ok\n'
@@ -0,0 +1,20 @@
1
+ ALTER TABLE clients
2
+ ADD COLUMN secure_row_uid BINARY(16) NULL,
3
+ ADD COLUMN phone_enc VARBINARY(512) NULL,
4
+ ADD COLUMN phone_bidx BINARY(32) NULL,
5
+ ADD INDEX idx_clients_phone_bidx (phone_bidx);
6
+
7
+
8
+ CREATE OR REPLACE VIEW admin_clients_readable AS
9
+ SELECT
10
+ id,
11
+ secure_db_fields_decrypt_field(phone_enc, 'clients', 'phone', secure_row_uid) AS phone,
12
+ created_at
13
+ FROM clients;
14
+
15
+ SELECT
16
+ id,
17
+ secure_db_fields_decrypt_field(phone_enc, 'clients', 'phone', secure_row_uid) AS phone,
18
+ created_at
19
+ FROM clients
20
+ WHERE phone_bidx = secure_phone_bidx('+77771234567');
@@ -0,0 +1,20 @@
1
+
2
+ DROP FUNCTION IF EXISTS secure_db_fields_version;
3
+ DROP FUNCTION IF EXISTS secure_db_fields_is_valid_envelope;
4
+ DROP FUNCTION IF EXISTS secure_db_fields_envelope_key_id;
5
+ DROP FUNCTION IF EXISTS secure_db_fields_decrypt;
6
+ DROP FUNCTION IF EXISTS secure_db_fields_decrypt_field;
7
+ DROP FUNCTION IF EXISTS secure_db_fields_bidx;
8
+ DROP FUNCTION IF EXISTS secure_phone_bidx;
9
+ DROP FUNCTION IF EXISTS secure_phone_prefix_bidx;
10
+
11
+ CREATE FUNCTION secure_db_fields_version RETURNS STRING SONAME 'secure_db_fields_mysql.so';
12
+ CREATE FUNCTION secure_db_fields_is_valid_envelope RETURNS INTEGER SONAME 'secure_db_fields_mysql.so';
13
+ CREATE FUNCTION secure_db_fields_envelope_key_id RETURNS INTEGER SONAME 'secure_db_fields_mysql.so';
14
+ CREATE FUNCTION secure_db_fields_decrypt RETURNS STRING SONAME 'secure_db_fields_mysql.so';
15
+ CREATE FUNCTION secure_db_fields_decrypt_field RETURNS STRING SONAME 'secure_db_fields_mysql.so';
16
+ CREATE FUNCTION secure_db_fields_bidx RETURNS STRING SONAME 'secure_db_fields_mysql.so';
17
+ CREATE FUNCTION secure_phone_bidx RETURNS STRING SONAME 'secure_db_fields_mysql.so';
18
+ CREATE FUNCTION secure_phone_prefix_bidx RETURNS STRING SONAME 'secure_db_fields_mysql.so';
19
+
20
+ SELECT secure_db_fields_version();
@@ -0,0 +1,8 @@
1
+ DROP FUNCTION IF EXISTS secure_phone_prefix_bidx;
2
+ DROP FUNCTION IF EXISTS secure_phone_bidx;
3
+ DROP FUNCTION IF EXISTS secure_db_fields_bidx;
4
+ DROP FUNCTION IF EXISTS secure_db_fields_decrypt_field;
5
+ DROP FUNCTION IF EXISTS secure_db_fields_decrypt;
6
+ DROP FUNCTION IF EXISTS secure_db_fields_envelope_key_id;
7
+ DROP FUNCTION IF EXISTS secure_db_fields_is_valid_envelope;
8
+ DROP FUNCTION IF EXISTS secure_db_fields_version;
@@ -0,0 +1,202 @@
1
+ # secure_db_fields — финальное решение (locked)
2
+
3
+ Заменяет черновой design-док. Это закрывающее решение с зафиксированными
4
+ границами для MySQL 5.7 + Ruby/Rails. Целевая совместимость: Ruby >= 2.7.1, MySQL 5.7. Крипто-ядро и архитектура заморожены;
5
+ единственный оставшийся вход — инвентаризация запросов (см. §10).
6
+
7
+ ---
8
+
9
+ ## 1. Вердикт
10
+
11
+ Пишем **свой узкий field-encryption слой по архитектуре multi_compress**, без
12
+ собственной криптографии: рандомизированный AEAD + keyed blind index +
13
+ MySQL UDF/view + миграционный контракт. Не готовый продукт, не pq_crypto,
14
+ не property-preserving.
15
+
16
+ Форма: небольшой **явный конфиг для конкретных полей** (телефон, email, ИИН,
17
+ ФИО), а не DSL-генератор. Кодогенерацию/платформу заводить только если впереди
18
+ десятки полей — иначе это лишняя поверхность.
19
+
20
+ ---
21
+
22
+ ## 2. Что отвергнуто и почему (проверено по первоисточникам)
23
+
24
+ | Вариант | Причина отказа |
25
+ |---|---|
26
+ | pq_crypto | KEM/подписи, а не field-encryption. Симметричная задача. Плюс сам гем не аудирован. |
27
+ | MySQL `AES_ENCRYPT` | Дефолт `block_encryption_mode` = `aes-128-ecb` (ECB); не AEAD. |
28
+ | TDE / InnoDB at-rest | Защищает физические файлы, но не логический `mysqldump`/`SELECT INTO OUTFILE`. |
29
+ | Acra CE | По их докам: не хранит search-хеши отдельно от ciphertext → поиск требует functional indexes, которых в MySQL 5.7 нет; LIKE — только Enterprise. |
30
+ | D'Amo / MyDiamo | Заявляют «keeps the order + range search» = property-preserving; вскрывается inference-атаками (Naveed–Kamara–Wright, Lacharité–Minaud–Paterson). Требует security review. |
31
+ | CipherStash | PostgreSQL + proxy/EQL. Не MySQL. |
32
+ | SQL Server Always Encrypted | LIKE/range только через secure enclaves (другой движок + TEE). |
33
+ | MongoDB Queryable Encryption | Другой движок; prefix/suffix/substring — public preview, не для прода. |
34
+ | Lockbox / blind_index / CipherSweet | Хорошая модель (мы её и берём), но не дают MySQL UDF, admin view, DBA bundle. |
35
+ | generated column bidx через UDF | В MySQL 5.7 stored/loadable функции в generated column запрещены (есть verified crash bug). |
36
+ | OPE / FPE / range-preserving | Property-preserving leakage. Вне scope. |
37
+
38
+ ---
39
+
40
+ ## 3. Крипто-ядро
41
+
42
+ ```
43
+ encryption: AES-256-GCM (OpenSSL), рандомизированный nonce, envelope
44
+ search: blind index = HMAC-SHA256(bidx_key, normalized_value)
45
+ keys: отдельные ключи под шифрование и под каждый blind index; HKDF из мастера
46
+ AAD: "table.column:" || secure_row_uid (не хранится, восстанавливается)
47
+ envelope: magic "MCEN" | version | alg_id | key_id | nonce(12) | tag(16) | ciphertext
48
+ ```
49
+
50
+ Nonce: per-column ключ → на объёме одной колонки далеко под лимитом 2³² случайного
51
+ GCM-nonce, отдельный XChaCha/counter не нужен.
52
+
53
+ Общий C-core компилируется в Ruby-ext и в MySQL-UDF (как MCDB в multi_compress).
54
+
55
+ ---
56
+
57
+ ## 4. Схема колонок (пример clients.phone)
58
+
59
+ ```sql
60
+ secure_row_uid BINARY(16) -- генерится в Ruby ДО INSERT, неизменяем, обязателен для AAD
61
+ phone_enc VARBINARY -- envelope
62
+ phone_bidx BINARY(32) -- exact / UNIQUE
63
+ -- prefix/suffix колонки добавляются ТОЛЬКО по инвентаризации (§10), напр.:
64
+ -- phone_bidx_p7 BINARY(32)
65
+ INDEX idx_clients_phone_bidx (phone_bidx)
66
+ ```
67
+
68
+ - Физические колонки, не generated. bidx считает приложение/backfill.
69
+ - `NULL` value → `NULL` bidx (несколько пустых не конфликтуют под UNIQUE).
70
+ - Нормализация коллизий: два ввода, канонизирующихся в один E.164, дадут одно
71
+ UNIQUE-значение — принять семантику заранее.
72
+
73
+ ---
74
+
75
+ ## 5. Контракт поиска + реестр утечек (leakage ledger)
76
+
77
+ Каждый режим поиска несёт именованную, принятую утечку. Ничего сверх объявленного
78
+ не поддерживаем.
79
+
80
+ | Режим | Как | Утечка в дампе БЕЗ ключа | Вердикт |
81
+ |---|---|---|---|
82
+ | exact (полное значение) | `bidx = HMAC(E.164)` | для телефона ≈ нет (значения почти уникальны); видно равенство одинаковых номеров | безопасно |
83
+ | `IN (...)`, `UNIQUE` | тот же bidx | то же | безопасно |
84
+ | `JOIN` по bidx | общий ключ между таблицами | кросс-табличное равенство поля | ок, если задумано |
85
+ | prefix длины N | `bidx_pN = HMAC(первые N цифр)` | низкая кардинальность: видно, какие строки делят N-значный префикс | принять **по каждой длине**, только если реально используется |
86
+ | suffix / last4 | `HMAC(последние 4)` | ОЧЕНЬ низкая кардинальность (≤10⁴ классов): сильный equality/frequency-сигнал | **highest-risk — обосновать или выкинуть** |
87
+ | `LIKE '%x%'` (инфикс) | — | требует ngram/bloom/proxy | **вне scope** |
88
+ | `ORDER BY` / range | — | property-preserving → восстановление plaintext | **вне scope** |
89
+
90
+ Общее: при утечке **ключа** (компрометация хоста) любой bidx перебираем словарём
91
+ (телефон — перечислимое пространство). Вся защита держится на «ключ вне дампа».
92
+
93
+ ---
94
+
95
+ ## 6. Контракт доступа админов (DBeaver/DataGrip)
96
+
97
+ **View — только для просмотра**, фильтр по неприватным колонкам:
98
+ ```sql
99
+ CREATE VIEW admin_clients_readable AS
100
+ SELECT id, secure_decrypt_phone(phone_enc, secure_row_uid) AS phone, created_at
101
+ FROM clients;
102
+ -- SELECT ... WHERE id = 123; -- ок
103
+ ```
104
+ Не давать `SELECT * FROM admin_clients_readable` без фильтра: это массовый decrypt
105
+ (и `INTO OUTFILE` из view выгрузит plaintext). Ограничить грантами/лимитом.
106
+
107
+ **Поиск — только через официальный контракт** (индекс по bidx, decrypt на проекции):
108
+ ```sql
109
+ CALL admin_find_client_by_phone('+77771234567');
110
+ -- или query-template:
111
+ SELECT id, secure_decrypt_phone(phone_enc, secure_row_uid) AS phone
112
+ FROM clients
113
+ WHERE phone_bidx = secure_phone_bidx('+77771234567');
114
+ ```
115
+ `WHERE phone = ...` по decrypt-view — запрещённый паттерн (decrypt-scan; view не
116
+ индексируется, TEMPTABLE не использует индексы базовой таблицы).
117
+
118
+ ---
119
+
120
+ ## 7. Ключи и принятая граница угроз
121
+
122
+ ```
123
+ Ключи: /etc/secure_db_fields/keys.json (owner root, group mysql, mode 0640)
124
+ или keyring/KMS. Вне любой таблицы, НИКОГДА в SQL-аргументе.
125
+ В БД: ciphertext, key_id, bidx, secure_row_uid, metadata. Ключей нет.
126
+ ```
127
+
128
+ **Явно принятая модель (записать в контракт, не подразумевать):**
129
+ - Утёк дамп/таблица БЕЗ ключей → приватные поля не читаются. ✔ (цель)
130
+ - Утёк дамп + файл ключа / компрометация хоста → защита пробита. ✘ (вне модели)
131
+ - Админ с доступом к decrypt-view/UDF → видит plaintext. Это разрешённый пользователь.
132
+ - decrypt-UDF — оракул для любого, кто может его вызвать на живом сервере.
133
+ Граница контроля — доступ к колонкам/view/процедурам/файлу ключа
134
+ (у loadable UDF нет `GRANT EXECUTE`).
135
+
136
+ Ротация: `key_id`/версии в envelope; новые записи под новым ключом, старые
137
+ читаются старым; rewrap лениво.
138
+
139
+ ---
140
+
141
+ ## 8. Нормализация — часть контракта
142
+
143
+ Канонизация в приложении **до** шифрования и bidx; DB-helper принимает уже E.164.
144
+ ```
145
+ 8 777 123 45 67 / +7 (777) 123-45-67 → +77771234567
146
+ secure_phone_bidx('+77771234567') -- ok
147
+ secure_phone_bidx('8 777 ...') -- error, а не «угадывание»
148
+ ```
149
+ Нормализатор — в общем C-core (байт-в-байт одинаково в Ruby и UDF), иначе bidx
150
+ разъедутся. Префиксный поиск нормализуется теми же правилами (иначе `8777%` и
151
+ `+7777%` дадут разные `bidx_pN`), и `bidx_pN` отвечает ровно на длину N.
152
+
153
+ ---
154
+
155
+ ## 9. Миграция (dual-write, zero-downtime)
156
+
157
+ ```
158
+ 1. Инвентаризация запросов (§10) → финальный список bidx-колонок.
159
+ 2. ALTER TABLE: secure_row_uid, *_enc, *_bidx(+ объявленные), индексы. Все NULL.
160
+ 3. Backfill батчами: uid + enc + bidx из открытого значения. Идемпотентно.
161
+ 4. Dual-write: приложение пишет старую колонку И новые; чтение/поиск → на bidx.
162
+ 5. Admin views + search procedures + гранты + отдельные DB-пользователи.
163
+ 6. Верификация: counts, sample-decrypt, паритет старый LIKE vs новый bidx, EXPLAIN.
164
+ 7. Бэкап → DROP старых plaintext-колонок (точка невозврата).
165
+ 8. (опц.) UNIQUE на bidx после чистки дублей/NULL.
166
+ ```
167
+ Backfill обязан проставить `secure_row_uid` всем старым строкам; копирование строки
168
+ без переноса uid ломает AAD.
169
+
170
+ ---
171
+
172
+ ## 10. Единственный оставшийся вход: инвентаризация запросов
173
+
174
+ Схема bidx-колонок — это **функция от реальных запросов**. До её сбора список
175
+ `*_bidx_p*`/`last4` — гадание. Собрать из `general_log`/`slow_log`/кода приложения
176
+ по каждому приватному полю:
177
+
178
+ ```
179
+ phone = ? → exact bidx (безопасно)
180
+ phone IN (...) → exact bidx (безопасно)
181
+ UNIQUE(phone) → exact bidx unique (безопасно)
182
+ JOIN ON phone → общий bidx-домен (равенство кросс-таблиц)
183
+ phone LIKE '777%' → bidx_pN конкретной N (низкокардинальная утечка)
184
+ phone LIKE '%1234' → last4 (highest-risk — обосновать/выкинуть)
185
+ phone LIKE '%777%' → decrypt-scan / вне scope
186
+ ORDER BY / range → вне scope
187
+ ```
188
+
189
+ Итог инвентаризации = точный список колонок и индексов, который лочит §4–§5.
190
+
191
+ ---
192
+
193
+ ## 11. Границы (окончательно)
194
+
195
+ ```
196
+ Поддерживаем: Ruby encrypt/decrypt/search; MySQL decrypt-UDF; admin views;
197
+ exact/IN/UNIQUE через blind index; объявленные prefix/lastN;
198
+ JOIN по bidx-домену; dual-write backfill; key_id/версии; AAD.
199
+ Не поддерживаем: произвольный LIKE '%x%'; ORDER BY/range по plaintext;
200
+ «любой старый SQL работает как раньше»; generated bidx через UDF;
201
+ ключи в БД или в SQL; pq_crypto/KEM; deterministic/OPE/FPE как основа.
202
+ ```
@@ -0,0 +1,63 @@
1
+ # Implementation notes
2
+
3
+ ## Compatibility
4
+
5
+ - Ruby: `>= 2.7.1`. No `Data.define`, no Ruby 3-only syntax, no required keyword-argument forwarding.
6
+ - MySQL: `5.7`. No functional indexes, no generated bidx columns via UDF, no MySQL 8-specific UDF assumptions.
7
+ - OpenSSL: AES-256-GCM via libcrypto EVP APIs available on common MySQL 5.7 hosts.
8
+
9
+ ## Locked scope
10
+
11
+ This package implements the lean version of the final decision:
12
+
13
+ - explicit field configuration in the host app, not a DSL/codegen platform;
14
+ - Ruby API for encryption/decryption/blind indexes;
15
+ - C core shared by Ruby and MySQL UDF;
16
+ - MySQL UDF SQL install/uninstall/examples;
17
+ - physical `*_bidx` columns only.
18
+
19
+ ## Envelope format MCEN1
20
+
21
+ ```text
22
+ magic 4 bytes "MCEN"
23
+ version 1 byte 1
24
+ alg_id 1 byte 1 = AES-256-GCM
25
+ key_id 4 bytes big-endian uint32
26
+ nonce 12 bytes random GCM nonce
27
+ tag 16 bytes GCM auth tag
28
+ ciphertext N bytes
29
+ ```
30
+
31
+ AAD is not stored. It must be reconstructed as:
32
+
33
+ ```text
34
+ <table>.<column>:<16 raw bytes secure_row_uid>
35
+ ```
36
+
37
+ ## Key file format
38
+
39
+ ```env
40
+ SDF_ACTIVE_KEY_ID=1
41
+ SDF_ENC_KEY_1_HEX=<64 hex chars>
42
+ SDF_BIDX_PHONE_KEY_HEX=<64 hex chars>
43
+ SDF_BIDX_PHONE_P7_KEY_HEX=<64 hex chars>
44
+ ```
45
+
46
+ ## Admin SQL boundary
47
+
48
+ Readable views are for projection. Indexed search must use bidx predicates or stored procedures.
49
+
50
+ ## Hot-path implementation notes (0.1.1)
51
+
52
+ The Ruby extension has separate scalar and batch paths:
53
+
54
+ - Scalar `blind_index` keeps the simple shared C-core `sdf_blind_index` path.
55
+ - Batch bidx methods use a fixed HMAC-SHA256 implementation in the Ruby extension:
56
+ the 64-byte ipad/opad SHA256 states are precomputed once per batch, then copied
57
+ per value. This avoids `HMAC_CTX_new/free/reset` and repeated pad hashing.
58
+ - Packed batch methods (`*_many_packed`) return one binary String containing
59
+ concatenated 32-byte digests, avoiding Array + String-per-digest allocation.
60
+ - Ruby encrypt/decrypt paths allocate the destination Ruby String first and call
61
+ `sdf_*_aes_256_gcm_into`, avoiding native `malloc -> rb_str_new copy -> free`.
62
+
63
+ The version remains `0.1.1`; these are implementation-level hot-path optimizations.
@@ -0,0 +1,33 @@
1
+ # Ruby 2.7.1 + MySQL 5.7 compatibility notes
2
+
3
+ This package targets Ruby `>= 2.7.1` and MySQL `5.7`.
4
+
5
+ ## Ruby
6
+
7
+ The Ruby layer intentionally avoids Ruby 3-only constructs:
8
+
9
+ - no `Data.define`;
10
+ - no endless methods;
11
+ - no pattern matching dependency;
12
+ - no anonymous argument forwarding (`...`);
13
+ - no Ruby 3-only keyword-argument behavior assumptions.
14
+
15
+ The native extension uses the stable Ruby C API available in Ruby 2.7.
16
+
17
+ ## MySQL 5.7
18
+
19
+ The MySQL layer intentionally avoids MySQL 8-only features:
20
+
21
+ - no functional indexes;
22
+ - no generated columns backed by stored/loadable functions;
23
+ - bidx/search tokens are physical `BINARY(32)` columns;
24
+ - admin search uses stored procedures/query templates with `WHERE *_bidx = secure_*_bidx(...)`.
25
+
26
+ The `mysql_udf/src/mysql_udf_abi_57.h` shim lets the UDF source compile in a MySQL 5.7-compatible ABI mode when headers are not present:
27
+
28
+ ```bash
29
+ cd mysql_udf
30
+ make abi57
31
+ ```
32
+
33
+ A live MySQL 5.7 server is still required for integration testing and `CREATE FUNCTION` installation.
@@ -0,0 +1,6 @@
1
+ ALTER TABLE clients
2
+ ADD COLUMN secure_row_uid BINARY(16) NULL,
3
+ ADD COLUMN phone_enc VARBINARY(512) NULL,
4
+ ADD COLUMN phone_bidx BINARY(32) NULL,
5
+ ADD INDEX idx_clients_phone_bidx (phone_bidx);
6
+
@@ -0,0 +1,6 @@
1
+ # Example only. Replace with cryptographically random 32-byte keys.
2
+ SDF_ACTIVE_KEY_ID=1
3
+ SDF_ENC_KEY_1_HEX=0000000000000000000000000000000000000000000000000000000000000001
4
+ SDF_BIDX_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000002
5
+ SDF_BIDX_PHONE_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000003
6
+ SDF_BIDX_PHONE_P7_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000004
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "secure_db_fields"
5
+
6
+ keyring = SecureDBFields::Keyring.new(File.expand_path("keys.env.example", __dir__))
7
+ uid = SecureRandom.random_bytes(16)
8
+ phone = "+77771234567"
9
+
10
+ aad = SecureDBFields::Crypto.aad("clients", "phone", uid)
11
+ phone_enc = SecureDBFields::Crypto.encrypt(phone, key: keyring.encryption_key(1), aad: aad, key_id: 1)
12
+ phone_bidx = SecureDBFields::Crypto.phone_blind_index(phone, key: keyring.blind_index_key("PHONE"))
13
+
14
+ puts SecureDBFields::Crypto.decrypt(phone_enc, key: keyring.encryption_key(1), aad: aad)
15
+ puts phone_bidx.unpack1("H*")
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ if ARGV.first == "db"
5
+ require "secure_db_fields/db_deployment"
6
+ exit SecureDBFields::DBDeployment::CLI.run(ARGV.drop(1))
7
+ end
8
+
9
+ require "secure_db_fields"
10
+
11
+ module SecureDBFields
12
+ class CLI
13
+ def self.run(argv)
14
+ new.run(argv)
15
+ end
16
+
17
+ def run(argv)
18
+ case argv.first
19
+ when "--version", "-v"
20
+ puts "secure_db_fields #{SecureDBFields::VERSION}"
21
+ 0
22
+ when "--help", "-h", nil
23
+ puts help
24
+ 0
25
+ else
26
+ warn "secure_db_fields: unknown command #{argv.first.inspect}"
27
+ warn help
28
+ 2
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def help
35
+ <<~TEXT
36
+ Usage:
37
+ secure_db_fields --version
38
+ secure_db_fields db package mysql [--output PATH] [--force]
39
+ secure_db_fields db view mysql --table DB.TABLE --field NAME:ENC_COLUMN \\
40
+ --uid-column secure_row_uid --view DB.VIEW --columns ID,CREATED_AT [--output PATH]
41
+
42
+ `db package mysql` creates a DBA handoff archive. Extract it on the MySQL 5.7
43
+ host and run `make doctor`, `sudo make install`, `make enable`, `make status`.
44
+ TEXT
45
+ end
46
+ end
47
+ end
48
+
49
+ exit SecureDBFields::CLI.run(ARGV)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+
5
+ abort "OpenSSL headers are required" unless have_header("openssl/evp.h")
6
+ abort "OpenSSL libcrypto is required" unless have_library("crypto", "EVP_aes_256_gcm")
7
+
8
+ clang_only_wno = %w[
9
+ -Wno-self-assign
10
+ -Wno-parentheses-equality
11
+ -Wno-constant-logical-operand
12
+ ]
13
+ if RbConfig::CONFIG["CC"].to_s !~ /clang/i
14
+ $warnflags = $warnflags.to_s.split.reject { |flag| clang_only_wno.include?(flag) }.join(" ")
15
+ end
16
+
17
+ $CFLAGS << " -std=c99 -Wall -Wextra -Werror=implicit-function-declaration"
18
+ $CFLAGS << " -DOPENSSL_SUPPRESS_DEPRECATED"
19
+ $CFLAGS << " -fstack-protector-strong -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security"
20
+
21
+ create_makefile("secure_db_fields/secure_db_fields")