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
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