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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/Rakefile +16 -0
- data/db_deployment/mysql/Makefile +62 -0
- data/db_deployment/mysql/README.md +33 -0
- data/db_deployment/mysql/bin/common +199 -0
- data/db_deployment/mysql/bin/disable +40 -0
- data/db_deployment/mysql/bin/doctor +57 -0
- data/db_deployment/mysql/bin/enable +56 -0
- data/db_deployment/mysql/bin/install +64 -0
- data/db_deployment/mysql/bin/status +11 -0
- data/db_deployment/mysql/bin/uninstall +46 -0
- data/db_deployment/mysql/bin/verify +27 -0
- data/db_deployment/mysql/sql/examples.sql +20 -0
- data/db_deployment/mysql/sql/install.sql +20 -0
- data/db_deployment/mysql/sql/uninstall.sql +8 -0
- data/docs/final_decision.md +202 -0
- data/docs/implementation_notes.md +63 -0
- data/docs/ruby_27_mysql_57.md +33 -0
- data/examples/clients_phone_migration.sql +6 -0
- data/examples/keys.env.example +6 -0
- data/examples/ruby_usage.rb +15 -0
- data/exe/secure_db_fields +49 -0
- data/ext/secure_db_fields/extconf.rb +21 -0
- data/ext/secure_db_fields/secure_db_fields_core.c +723 -0
- data/ext/secure_db_fields/secure_db_fields_core.h +140 -0
- data/ext/secure_db_fields/secure_db_fields_ext.c +426 -0
- data/lib/secure_db_fields/active_record.rb +58 -0
- data/lib/secure_db_fields/crypto.rb +115 -0
- data/lib/secure_db_fields/db_deployment.rb +285 -0
- data/lib/secure_db_fields/keys.rb +69 -0
- data/lib/secure_db_fields/phone.rb +17 -0
- data/lib/secure_db_fields/version.rb +5 -0
- data/lib/secure_db_fields.rb +8 -0
- data/mysql_udf/Makefile +25 -0
- data/mysql_udf/README.md +51 -0
- data/mysql_udf/bin/install +11 -0
- data/mysql_udf/bin/uninstall +4 -0
- data/mysql_udf/bin/verify +3 -0
- data/mysql_udf/sql/examples.sql +20 -0
- data/mysql_udf/sql/install.sql +20 -0
- data/mysql_udf/sql/uninstall.sql +8 -0
- data/mysql_udf/src/mysql_udf_abi_57.h +34 -0
- data/mysql_udf/src/secure_db_fields_mysql.c +513 -0
- data/mysql_udf/test/run_e2e.sh +216 -0
- 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
|
+
# 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")
|