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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 31ac55f3c53d7f1c6755f96f294cceacbc6f972891b46e809c1e4cc8222f5615
|
|
4
|
+
data.tar.gz: 2e918c0bbdffc6d95a60c6eb39e7c8f2a3345939e71c2d6d85c9745139cccb92
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a872b172252d8df672af874f3dbacad680951fe6a178f428e4e7cad399a2d8b31d8bb62c9d5138ea354b26841b2e98b8c2e6a2c43fd37685d2bb4ee24b808727
|
|
7
|
+
data.tar.gz: efcdc09a8877b2a790b991a6f55bd399df7c29babc811ff1d10da8b5f0e2eaa584cd85dc3056798e68c69cd84852253f0c8dbff53647ba7e32a79f3e4b6191cf
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roman Khaidarov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# secure_db_fields
|
|
2
|
+
|
|
3
|
+
`secure_db_fields` is a narrow field-level encryption layer for Ruby/Rails + MySQL 5.7.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
It follows the locked design from `docs/final_decision.md`:
|
|
7
|
+
|
|
8
|
+
- AES-256-GCM randomized envelope for confidential values.
|
|
9
|
+
- HMAC-SHA256 blind indexes for exact indexed search.
|
|
10
|
+
- Physical `*_bidx` columns, not generated columns.
|
|
11
|
+
- MySQL UDFs for admin views/search helpers in DBeaver/DataGrip.
|
|
12
|
+
- Keys outside the database and never passed as SQL arguments.
|
|
13
|
+
- No `pq_crypto`, no OPE/FPE/range-preserving encryption, no transparent arbitrary `LIKE`.
|
|
14
|
+
|
|
15
|
+
## Threat model
|
|
16
|
+
|
|
17
|
+
Protected: raw table / SQL dump / physical column data leaked without key files.
|
|
18
|
+
|
|
19
|
+
Not protected: DB host compromise, key-file compromise, or authorized admin access to decrypt UDF/views.
|
|
20
|
+
|
|
21
|
+
## Ruby quick start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "secure_db_fields"
|
|
25
|
+
|
|
26
|
+
keyring = SecureDBFields::Keyring.new("/etc/secure_db_fields/keys.env")
|
|
27
|
+
uid = SecureRandom.random_bytes(16)
|
|
28
|
+
phone = "+77771234567"
|
|
29
|
+
|
|
30
|
+
aad = SecureDBFields::Crypto.aad("clients", "phone", uid)
|
|
31
|
+
enc = SecureDBFields::Crypto.encrypt(phone, key: keyring.encryption_key(1), aad: aad, key_id: 1)
|
|
32
|
+
bidx = SecureDBFields::Crypto.phone_blind_index(phone, key: keyring.blind_index_key("PHONE"))
|
|
33
|
+
|
|
34
|
+
SecureDBFields::Crypto.decrypt(enc, key: keyring.encryption_key(1), aad: aad)
|
|
35
|
+
# => "+77771234567"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## MySQL exact search template
|
|
39
|
+
|
|
40
|
+
```sql
|
|
41
|
+
SELECT
|
|
42
|
+
id,
|
|
43
|
+
secure_db_fields_decrypt_field(phone_enc, 'clients', 'phone', secure_row_uid) AS phone,
|
|
44
|
+
created_at
|
|
45
|
+
FROM clients
|
|
46
|
+
WHERE phone_bidx = secure_phone_bidx('+77771234567');
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Do not search by plaintext through the decrypt view:
|
|
50
|
+
|
|
51
|
+
```sql
|
|
52
|
+
-- forbidden on large tables: decrypt-scan
|
|
53
|
+
SELECT * FROM admin_clients_readable WHERE phone = '+77771234567';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Prefix search
|
|
57
|
+
|
|
58
|
+
Prefix search is not generic SQL `LIKE`. It is a named search mode with a physical column:
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
phone_bidx_p7 BINARY(32), INDEX idx_clients_phone_bidx_p7(phone_bidx_p7)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Only add prefix columns confirmed by query inventory. `last4`/suffix is intentionally not part of
|
|
65
|
+
the default schema because it has very low cardinality and high leakage.
|
|
66
|
+
|
|
67
|
+
## MySQL 5.7 DBA handoff bundle
|
|
68
|
+
|
|
69
|
+
The application developer can build a self-contained database-host archive and pass it to the DBA:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bundle exec secure_db_fields db package mysql --output secure_db_fields-mysql.tar.gz --force
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
On the MySQL 5.7 server the DBA extracts the archive and runs:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
make verify
|
|
79
|
+
make doctor
|
|
80
|
+
sudo make install
|
|
81
|
+
make enable
|
|
82
|
+
make status
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Keys are intentionally not included in the archive. Provision `/etc/secure_db_fields/keys.env` on the DB host before using decrypt/search UDFs.
|
|
86
|
+
|
|
87
|
+
Readable view SQL can be generated from the application side as well:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bundle exec secure_db_fields db view mysql \
|
|
91
|
+
--table app.clients \
|
|
92
|
+
--field phone:phone_enc \
|
|
93
|
+
--uid-column secure_row_uid \
|
|
94
|
+
--view admin.clients_readable \
|
|
95
|
+
--columns id,created_at \
|
|
96
|
+
--output clients_readable.sql
|
|
97
|
+
```
|
|
98
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
require "rake/extensiontask"
|
|
5
|
+
|
|
6
|
+
Rake::ExtensionTask.new("secure_db_fields", Gem::Specification.load("secure_db_fields.gemspec")) do |ext|
|
|
7
|
+
ext.lib_dir = "lib/secure_db_fields"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
|
11
|
+
t.libs << "test"
|
|
12
|
+
t.libs << "lib"
|
|
13
|
+
t.pattern = "test/**/*_test.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task default: %i[compile test]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
SHELL := /bin/bash
|
|
2
|
+
|
|
3
|
+
CC ?= cc
|
|
4
|
+
CFLAGS ?= -O2 -fPIC -Wall -Wextra -Wformat -Wformat-security -fstack-protector-strong -D_FORTIFY_SOURCE=2 -std=c99
|
|
5
|
+
LDFLAGS ?= -shared
|
|
6
|
+
LIBS ?= -lcrypto
|
|
7
|
+
MYSQL_CFLAGS ?= $(shell mysql_config --cflags 2>/dev/null)
|
|
8
|
+
UDF_SO = secure_db_fields_mysql.so
|
|
9
|
+
SRC = src/secure_db_fields_mysql.c src/secure_db_fields_core.c
|
|
10
|
+
|
|
11
|
+
MYSQL ?= mysql
|
|
12
|
+
MYSQL_DEFAULTS_FILE ?=
|
|
13
|
+
MYSQL_SOCKET ?=
|
|
14
|
+
CONFIRM ?=
|
|
15
|
+
|
|
16
|
+
.PHONY: help all abi57 verify doctor install enable disable status uninstall clean
|
|
17
|
+
|
|
18
|
+
help:
|
|
19
|
+
@printf '%s\n' \
|
|
20
|
+
'SecureDBFields MySQL 5.7 deployment bundle' \
|
|
21
|
+
'' \
|
|
22
|
+
' make verify' \
|
|
23
|
+
' make doctor [MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf] [MYSQL_SOCKET=/run/mysqld/mysqld.sock]' \
|
|
24
|
+
' sudo make install [MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf] [MYSQL_SOCKET=/run/mysqld/mysqld.sock]' \
|
|
25
|
+
' make enable [MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf] [MYSQL_SOCKET=/run/mysqld/mysqld.sock]' \
|
|
26
|
+
' make status [MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf] [MYSQL_SOCKET=/run/mysqld/mysqld.sock]' \
|
|
27
|
+
' make disable CONFIRM=DISABLE_SECURE_DB_FIELDS [MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf] [MYSQL_SOCKET=/run/mysqld/mysqld.sock]' \
|
|
28
|
+
' sudo make uninstall CONFIRM=REMOVE_SECURE_DB_FIELDS [MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf] [MYSQL_SOCKET=/run/mysqld/mysqld.sock]'
|
|
29
|
+
|
|
30
|
+
all: abi57
|
|
31
|
+
|
|
32
|
+
$(UDF_SO): $(SRC) src/secure_db_fields_core.h src/mysql_udf_abi_57.h
|
|
33
|
+
$(CC) $(CFLAGS) $(MYSQL_CFLAGS) -Isrc $(LDFLAGS) -o $@ $(SRC) $(LIBS)
|
|
34
|
+
|
|
35
|
+
abi57: $(SRC) src/secure_db_fields_core.h src/mysql_udf_abi_57.h
|
|
36
|
+
$(CC) $(CFLAGS) -DSDF_MYSQL_UDF_ABI_57 -Isrc $(LDFLAGS) -o $(UDF_SO) $(SRC) $(LIBS)
|
|
37
|
+
|
|
38
|
+
verify:
|
|
39
|
+
./bin/verify
|
|
40
|
+
|
|
41
|
+
doctor:
|
|
42
|
+
MYSQL_SOCKET="$(MYSQL_SOCKET)" ./bin/doctor --mysql "$(MYSQL)" $(if $(MYSQL_DEFAULTS_FILE),--defaults-extra-file "$(MYSQL_DEFAULTS_FILE)")
|
|
43
|
+
|
|
44
|
+
install: verify
|
|
45
|
+
MYSQL_SOCKET="$(MYSQL_SOCKET)" ./bin/install --mysql "$(MYSQL)" $(if $(MYSQL_DEFAULTS_FILE),--defaults-extra-file "$(MYSQL_DEFAULTS_FILE)")
|
|
46
|
+
|
|
47
|
+
enable:
|
|
48
|
+
MYSQL_SOCKET="$(MYSQL_SOCKET)" ./bin/enable --mysql "$(MYSQL)" $(if $(MYSQL_DEFAULTS_FILE),--defaults-extra-file "$(MYSQL_DEFAULTS_FILE)")
|
|
49
|
+
|
|
50
|
+
disable:
|
|
51
|
+
@test "$(CONFIRM)" = "DISABLE_SECURE_DB_FIELDS" || { echo 'disable requires CONFIRM=DISABLE_SECURE_DB_FIELDS' >&2; exit 2; }
|
|
52
|
+
MYSQL_SOCKET="$(MYSQL_SOCKET)" ./bin/disable --mysql "$(MYSQL)" $(if $(MYSQL_DEFAULTS_FILE),--defaults-extra-file "$(MYSQL_DEFAULTS_FILE)")
|
|
53
|
+
|
|
54
|
+
status:
|
|
55
|
+
MYSQL_SOCKET="$(MYSQL_SOCKET)" ./bin/status --mysql "$(MYSQL)" $(if $(MYSQL_DEFAULTS_FILE),--defaults-extra-file "$(MYSQL_DEFAULTS_FILE)")
|
|
56
|
+
|
|
57
|
+
uninstall:
|
|
58
|
+
@test "$(CONFIRM)" = "REMOVE_SECURE_DB_FIELDS" || { echo 'uninstall requires CONFIRM=REMOVE_SECURE_DB_FIELDS' >&2; exit 2; }
|
|
59
|
+
MYSQL_SOCKET="$(MYSQL_SOCKET)" ./bin/uninstall --mysql "$(MYSQL)" $(if $(MYSQL_DEFAULTS_FILE),--defaults-extra-file "$(MYSQL_DEFAULTS_FILE)")
|
|
60
|
+
|
|
61
|
+
clean:
|
|
62
|
+
rm -f $(UDF_SO) *.o src/*.o
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# secure_db_fields MySQL DBA bundle
|
|
2
|
+
|
|
3
|
+
This archive is produced on an application host with:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
secure_db_fields db package mysql --output secure_db_fields-mysql.tar.gz --force
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Copy the archive to the MySQL 5.7 host, extract it, then run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
make verify
|
|
13
|
+
make doctor
|
|
14
|
+
sudo make install
|
|
15
|
+
make enable
|
|
16
|
+
make status
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keys are not included in this archive. The DBA must provision `/etc/secure_db_fields/keys.env`
|
|
20
|
+
(or set `SECURE_DB_FIELDS_KEY_FILE` for mysqld) before using decrypt/search UDFs.
|
|
21
|
+
|
|
22
|
+
Installed SQL functions:
|
|
23
|
+
|
|
24
|
+
- `secure_db_fields_version()`
|
|
25
|
+
- `secure_db_fields_is_valid_envelope(blob)`
|
|
26
|
+
- `secure_db_fields_envelope_key_id(blob)`
|
|
27
|
+
- `secure_db_fields_decrypt(blob, aad)`
|
|
28
|
+
- `secure_db_fields_decrypt_field(blob, table_name, column_name, row_uid)`
|
|
29
|
+
- `secure_db_fields_bidx(value, domain)`
|
|
30
|
+
- `secure_phone_bidx(e164)`
|
|
31
|
+
- `secure_phone_prefix_bidx(e164, prefix_digits)`
|
|
32
|
+
|
|
33
|
+
The readable-view pattern is display-only. Indexed search must use physical bidx columns.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
SDF_UDF_LIBRARY='secure_db_fields_mysql.so'
|
|
4
|
+
SDF_UDF_NAMES=(
|
|
5
|
+
secure_db_fields_version
|
|
6
|
+
secure_db_fields_is_valid_envelope
|
|
7
|
+
secure_db_fields_envelope_key_id
|
|
8
|
+
secure_db_fields_decrypt
|
|
9
|
+
secure_db_fields_decrypt_field
|
|
10
|
+
secure_db_fields_bidx
|
|
11
|
+
secure_phone_bidx
|
|
12
|
+
secure_phone_prefix_bidx
|
|
13
|
+
)
|
|
14
|
+
SDF_KEY_FILE="${SECURE_DB_FIELDS_KEY_FILE:-/etc/secure_db_fields/keys.env}"
|
|
15
|
+
|
|
16
|
+
sdf_die() {
|
|
17
|
+
echo "secure_db_fields mysql: $*" >&2
|
|
18
|
+
exit 2
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sdf_resolve_mysql() {
|
|
22
|
+
if [[ "$MYSQL" == */* ]]; then
|
|
23
|
+
[ -x "$MYSQL" ] || sdf_die "mysql client is not executable: $MYSQL"
|
|
24
|
+
else
|
|
25
|
+
MYSQL="$(command -v "$MYSQL" || true)"
|
|
26
|
+
[ -n "$MYSQL" ] || sdf_die 'mysql client was not found'
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sdf_mysql_args() {
|
|
31
|
+
MYSQL_ARGS=(--batch --skip-column-names --raw --protocol=SOCKET)
|
|
32
|
+
if [ -n "${DEFAULTS_FILE:-}" ]; then
|
|
33
|
+
[ -f "$DEFAULTS_FILE" ] || sdf_die "defaults file does not exist: $DEFAULTS_FILE"
|
|
34
|
+
MYSQL_ARGS=("--defaults-extra-file=$DEFAULTS_FILE" "${MYSQL_ARGS[@]}")
|
|
35
|
+
fi
|
|
36
|
+
if [ -n "${MYSQL_SOCKET:-}" ]; then
|
|
37
|
+
MYSQL_ARGS+=("--socket=$MYSQL_SOCKET")
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sdf_query() {
|
|
42
|
+
"$MYSQL" "${MYSQL_ARGS[@]}" -e "$1"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sdf_exec_file() {
|
|
46
|
+
"$MYSQL" "${MYSQL_ARGS[@]}" < "$1"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sdf_server_version() {
|
|
50
|
+
sdf_query 'SELECT VERSION();' | tr -d '\r\n'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
sdf_require_mysql57() {
|
|
54
|
+
local version
|
|
55
|
+
version="$(sdf_server_version)"
|
|
56
|
+
case "$version" in
|
|
57
|
+
5.7.*) ;;
|
|
58
|
+
*) sdf_die "this bundle targets MySQL 5.7; server reports $version" ;;
|
|
59
|
+
esac
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
sdf_plugin_dir() {
|
|
63
|
+
sdf_query 'SELECT @@plugin_dir;' | tr -d '\r\n'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sdf_server_socket() {
|
|
67
|
+
sdf_query 'SELECT @@socket;' | tr -d '\r\n'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sdf_server_hostname() {
|
|
71
|
+
sdf_query 'SELECT @@hostname;' | tr -d '\r\n'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sdf_require_expected_socket() {
|
|
75
|
+
local actual_socket
|
|
76
|
+
actual_socket="$(sdf_server_socket)"
|
|
77
|
+
[ -n "$actual_socket" ] || sdf_die 'MySQL returned an empty @@socket'
|
|
78
|
+
if [ -n "${MYSQL_SOCKET:-}" ] && [ "$MYSQL_SOCKET" != "$actual_socket" ]; then
|
|
79
|
+
sdf_die "connected server socket $actual_socket does not match MYSQL_SOCKET $MYSQL_SOCKET"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
sdf_read_udf_state() {
|
|
84
|
+
local rows name dl candidate expected_name
|
|
85
|
+
|
|
86
|
+
SDF_EXPECTED_OWNED=0
|
|
87
|
+
SDF_FOREIGN_EXPECTED=0
|
|
88
|
+
SDF_UNEXPECTED_LIBRARY=0
|
|
89
|
+
SDF_REGISTERED_ROWS=()
|
|
90
|
+
|
|
91
|
+
local names_sql=""
|
|
92
|
+
local expected=()
|
|
93
|
+
for name in "${SDF_UDF_NAMES[@]}"; do
|
|
94
|
+
expected+=("'${name}'")
|
|
95
|
+
done
|
|
96
|
+
names_sql="$(IFS=,; printf '%s' "${expected[*]}")"
|
|
97
|
+
|
|
98
|
+
rows="$(sdf_query "SELECT name, dl FROM mysql.func WHERE dl = '${SDF_UDF_LIBRARY}' OR name IN (${names_sql}) ORDER BY name;")"
|
|
99
|
+
while IFS=$'\t' read -r name dl; do
|
|
100
|
+
[ -n "$name" ] || continue
|
|
101
|
+
SDF_REGISTERED_ROWS+=("$name"$'\t'"$dl")
|
|
102
|
+
expected_name=0
|
|
103
|
+
for candidate in "${SDF_UDF_NAMES[@]}"; do
|
|
104
|
+
if [ "$candidate" = "$name" ]; then expected_name=1; break; fi
|
|
105
|
+
done
|
|
106
|
+
if [ "$expected_name" -eq 1 ]; then
|
|
107
|
+
if [ "$dl" = "$SDF_UDF_LIBRARY" ]; then
|
|
108
|
+
SDF_EXPECTED_OWNED=$((SDF_EXPECTED_OWNED + 1))
|
|
109
|
+
else
|
|
110
|
+
SDF_FOREIGN_EXPECTED=$((SDF_FOREIGN_EXPECTED + 1))
|
|
111
|
+
fi
|
|
112
|
+
elif [ "$dl" = "$SDF_UDF_LIBRARY" ]; then
|
|
113
|
+
SDF_UNEXPECTED_LIBRARY=$((SDF_UNEXPECTED_LIBRARY + 1))
|
|
114
|
+
fi
|
|
115
|
+
done <<< "$rows"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sdf_state_is_clean_absent() {
|
|
119
|
+
[ "$SDF_EXPECTED_OWNED" -eq 0 ] && [ "$SDF_FOREIGN_EXPECTED" -eq 0 ] && [ "$SDF_UNEXPECTED_LIBRARY" -eq 0 ]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sdf_state_is_clean_enabled() {
|
|
123
|
+
[ "$SDF_EXPECTED_OWNED" -eq "${#SDF_UDF_NAMES[@]}" ] && [ "$SDF_FOREIGN_EXPECTED" -eq 0 ] && [ "$SDF_UNEXPECTED_LIBRARY" -eq 0 ]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sdf_require_clean_owned_state() {
|
|
127
|
+
if [ "$SDF_FOREIGN_EXPECTED" -ne 0 ]; then
|
|
128
|
+
sdf_die 'one or more expected UDF names are registered to a foreign SONAME; refusing to modify them'
|
|
129
|
+
fi
|
|
130
|
+
if [ "$SDF_UNEXPECTED_LIBRARY" -ne 0 ]; then
|
|
131
|
+
sdf_die "unexpected registrations reference ${SDF_UDF_LIBRARY}; refusing to modify the shared library"
|
|
132
|
+
fi
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
sdf_register_expected_udfs() {
|
|
136
|
+
sdf_exec_file "$ROOT/sql/install.sql"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
sdf_drop_owned_udfs() {
|
|
140
|
+
local name
|
|
141
|
+
sdf_read_udf_state
|
|
142
|
+
sdf_require_clean_owned_state
|
|
143
|
+
for name in "${SDF_UDF_NAMES[@]}"; do
|
|
144
|
+
if sdf_query "SELECT COUNT(*) FROM mysql.func WHERE name = '${name}' AND dl = '${SDF_UDF_LIBRARY}';" | tr -d '\r\n' | grep -qx '1'; then
|
|
145
|
+
sdf_query "DROP FUNCTION ${name};"
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
sdf_smoke_check() {
|
|
151
|
+
local version
|
|
152
|
+
version="$(sdf_query 'SELECT secure_db_fields_version();' | tr -d '\r\n')"
|
|
153
|
+
case "$version" in
|
|
154
|
+
*'secure_db_fields'*) ;;
|
|
155
|
+
*) sdf_die "version() smoke check failed: $version" ;;
|
|
156
|
+
esac
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
sdf_library_checksum() {
|
|
160
|
+
local library="$1"
|
|
161
|
+
if [ ! -f "$library" ]; then
|
|
162
|
+
printf '%s\n' 'missing'
|
|
163
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
164
|
+
sha256sum "$library" | awk '{print $1}'
|
|
165
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
166
|
+
shasum -a 256 "$library" | awk '{print $1}'
|
|
167
|
+
else
|
|
168
|
+
printf '%s\n' 'unavailable (sha256sum or shasum is required)'
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
sdf_check_key_file_permissions() {
|
|
173
|
+
local file="${1:-$SDF_KEY_FILE}"
|
|
174
|
+
local mode
|
|
175
|
+
[ -f "$file" ] || sdf_die "key file does not exist: $file"
|
|
176
|
+
mode="$(stat -c '%a' "$file" 2>/dev/null || true)"
|
|
177
|
+
[ -n "$mode" ] || sdf_die "cannot stat key file permissions: $file"
|
|
178
|
+
if [ $((8#$mode & 0027)) -ne 0 ]; then
|
|
179
|
+
sdf_die "key file permissions are too open: $file"
|
|
180
|
+
fi
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
sdf_prepare_key_file() {
|
|
184
|
+
local dir
|
|
185
|
+
dir="$(dirname "$SDF_KEY_FILE")"
|
|
186
|
+
install -d -m 0750 -o root -g mysql "$dir"
|
|
187
|
+
if [ ! -e "$SDF_KEY_FILE" ]; then
|
|
188
|
+
install -m 0640 -o root -g mysql /dev/null "$SDF_KEY_FILE"
|
|
189
|
+
fi
|
|
190
|
+
chown root:mysql "$SDF_KEY_FILE"
|
|
191
|
+
chmod 0640 "$SDF_KEY_FILE"
|
|
192
|
+
sdf_check_key_file_permissions "$SDF_KEY_FILE"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
sdf_build_udf() {
|
|
196
|
+
cd "$ROOT"
|
|
197
|
+
make clean >/dev/null 2>&1 || true
|
|
198
|
+
make abi57
|
|
199
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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: MYSQL_SOCKET=/run/mysqld/mysqld.sock ./bin/disable [--mysql PATH] [--defaults-extra-file PATH]
|
|
12
|
+
|
|
13
|
+
Removes only SecureDBFields-owned UDF registrations and leaves the native library
|
|
14
|
+
in place.
|
|
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 disable: unknown option: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
source "$ROOT/bin/common"
|
|
34
|
+
sdf_resolve_mysql
|
|
35
|
+
sdf_mysql_args
|
|
36
|
+
sdf_require_mysql57
|
|
37
|
+
sdf_require_expected_socket
|
|
38
|
+
sdf_drop_owned_udfs
|
|
39
|
+
|
|
40
|
+
echo 'SecureDBFields MySQL UDFs are disabled.'
|
|
@@ -0,0 +1,57 @@
|
|
|
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: MYSQL_SOCKET=/run/mysqld/mysqld.sock ./bin/doctor [--mysql PATH] [--defaults-extra-file PATH]
|
|
12
|
+
|
|
13
|
+
Run this on the MySQL 5.7 host. The bundle uses a local Unix socket so the UDF
|
|
14
|
+
library is installed into the plugin_dir of that same mysqld.
|
|
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 doctor: unknown option: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
source "$ROOT/bin/common"
|
|
34
|
+
sdf_resolve_mysql
|
|
35
|
+
sdf_mysql_args
|
|
36
|
+
command -v make >/dev/null 2>&1 || sdf_die 'make is required'
|
|
37
|
+
command -v cc >/dev/null 2>&1 || sdf_die 'a C compiler is required (cc/gcc/clang)'
|
|
38
|
+
sdf_require_mysql57
|
|
39
|
+
sdf_require_expected_socket
|
|
40
|
+
|
|
41
|
+
PLUGIN_DIR="$(sdf_plugin_dir)"
|
|
42
|
+
SERVER_SOCKET="$(sdf_server_socket)"
|
|
43
|
+
SERVER_HOSTNAME="$(sdf_server_hostname)"
|
|
44
|
+
[ -n "$PLUGIN_DIR" ] || sdf_die 'MySQL returned an empty @@plugin_dir'
|
|
45
|
+
[ -d "$PLUGIN_DIR" ] || sdf_die "@@plugin_dir is not a local directory: $PLUGIN_DIR"
|
|
46
|
+
if [ -e "$SDF_KEY_FILE" ]; then
|
|
47
|
+
sdf_check_key_file_permissions "$SDF_KEY_FILE"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
printf '%s\n' \
|
|
51
|
+
"MySQL: $(sdf_server_version)" \
|
|
52
|
+
"mysql client: $MYSQL" \
|
|
53
|
+
"server hostname: $SERVER_HOSTNAME" \
|
|
54
|
+
"server socket: $SERVER_SOCKET" \
|
|
55
|
+
"plugin directory: $PLUGIN_DIR" \
|
|
56
|
+
"bundle: $ROOT" \
|
|
57
|
+
'doctor: OK'
|
|
@@ -0,0 +1,56 @@
|
|
|
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: MYSQL_SOCKET=/run/mysqld/mysqld.sock ./bin/enable [--mysql PATH] [--defaults-extra-file PATH]
|
|
12
|
+
|
|
13
|
+
Registers the SecureDBFields UDFs once. Existing healthy registrations are left
|
|
14
|
+
untouched; this command never drops functions automatically.
|
|
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 enable: unknown option: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
source "$ROOT/bin/common"
|
|
34
|
+
sdf_resolve_mysql
|
|
35
|
+
sdf_mysql_args
|
|
36
|
+
sdf_require_mysql57
|
|
37
|
+
sdf_require_expected_socket
|
|
38
|
+
sdf_read_udf_state
|
|
39
|
+
|
|
40
|
+
if sdf_state_is_clean_absent; then
|
|
41
|
+
sdf_register_expected_udfs
|
|
42
|
+
sdf_read_udf_state
|
|
43
|
+
sdf_state_is_clean_enabled || sdf_die 'registration did not create the expected UDF state'
|
|
44
|
+
sdf_smoke_check
|
|
45
|
+
echo 'SecureDBFields MySQL UDFs are enabled. Apply generated admin-view SQL separately.'
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if sdf_state_is_clean_enabled; then
|
|
50
|
+
sdf_smoke_check
|
|
51
|
+
echo 'SecureDBFields MySQL UDFs are already enabled; no changes made.'
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
sdf_require_clean_owned_state
|
|
56
|
+
sdf_die 'UDF registrations are partial; run make status and resolve the state before enabling'
|
|
@@ -0,0 +1,64 @@
|
|
|
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/install [--mysql PATH] [--defaults-extra-file PATH]
|
|
12
|
+
|
|
13
|
+
Builds and installs a fresh MySQL 5.7 UDF library. It refuses to replace a
|
|
14
|
+
library while any expected UDF is registered; use ./bin/uninstall first.
|
|
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 install: unknown option: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
[ "${EUID:-$(id -u)}" -eq 0 ] || { echo 'secure_db_fields mysql install: run as root: sudo ./bin/install ...' >&2; exit 2; }
|
|
34
|
+
DOCTOR_ARGS=(--mysql "$MYSQL")
|
|
35
|
+
if [ -n "$DEFAULTS_FILE" ]; then
|
|
36
|
+
DOCTOR_ARGS+=(--defaults-extra-file "$DEFAULTS_FILE")
|
|
37
|
+
fi
|
|
38
|
+
MYSQL_SOCKET="$MYSQL_SOCKET" "$ROOT/bin/doctor" "${DOCTOR_ARGS[@]}"
|
|
39
|
+
|
|
40
|
+
source "$ROOT/bin/common"
|
|
41
|
+
sdf_resolve_mysql
|
|
42
|
+
sdf_mysql_args
|
|
43
|
+
sdf_prepare_key_file
|
|
44
|
+
sdf_read_udf_state
|
|
45
|
+
if ! sdf_state_is_clean_absent; then
|
|
46
|
+
sdf_die 'fresh install refuses to replace a library while UDF registrations exist; run status or uninstall first'
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
PLUGIN_DIR="$(sdf_plugin_dir)"
|
|
50
|
+
TEMP_LIBRARY="$PLUGIN_DIR/.${SDF_UDF_LIBRARY}.$$.$RANDOM.tmp"
|
|
51
|
+
trap 'rm -f "$TEMP_LIBRARY"' EXIT
|
|
52
|
+
|
|
53
|
+
sdf_build_udf
|
|
54
|
+
install -d -m 0755 "$PLUGIN_DIR"
|
|
55
|
+
install -m 0755 "$ROOT/$SDF_UDF_LIBRARY" "$TEMP_LIBRARY"
|
|
56
|
+
mv -f "$TEMP_LIBRARY" "$PLUGIN_DIR/$SDF_UDF_LIBRARY"
|
|
57
|
+
trap - EXIT
|
|
58
|
+
|
|
59
|
+
printf '%s\n' \
|
|
60
|
+
'SecureDBFields MySQL UDF library installed.' \
|
|
61
|
+
" library: $PLUGIN_DIR/$SDF_UDF_LIBRARY" \
|
|
62
|
+
'' \
|
|
63
|
+
'Register the UDFs once on this MySQL server:' \
|
|
64
|
+
' make enable'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
source "$(dirname "$0")/common"
|
|
4
|
+
|
|
5
|
+
printf 'plugin_dir=%s\n' "$(plugin_dir)"
|
|
6
|
+
printf 'registered_functions=%s\n' "$(function_count)"
|
|
7
|
+
if mysql_cmd -e 'SELECT secure_db_fields_version() AS version;' >/dev/null 2>&1; then
|
|
8
|
+
mysql_cmd -e 'SELECT secure_db_fields_version() AS version;'
|
|
9
|
+
else
|
|
10
|
+
echo 'secure_db_fields_version() is not enabled'
|
|
11
|
+
fi
|