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,216 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ DEFAULT_MYSQL_IMAGE="mysql:5.7.44@sha256:dab0a802b44617303694fb17d166501de279c3031ddeb28c56ecf7fcab5ef0da"
5
+ IMAGE="${SDF_MYSQL_IMAGE:-$DEFAULT_MYSQL_IMAGE}"
6
+ PLATFORM="${SDF_MYSQL_PLATFORM:-linux/amd64}"
7
+ WAIT_SECONDS="${SDF_MYSQL_WAIT_SECONDS:-120}"
8
+ CONTAINER="sdf-mysql57-e2e-${RANDOM}-${RANDOM}"
9
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
10
+ TMPDIR="$(mktemp -d)"
11
+ ARCHIVE="$TMPDIR/secure_db_fields-mysql.tar.gz"
12
+ PACKED="$TMPDIR/packed"
13
+ BUNDLE_PARENT="/opt"
14
+ KEY_HEX_A="6161616161616161616161616161616161616161616161616161616161616161"
15
+ KEY_HEX_B="6262626262626262626262626262626262626262626262626262626262626262"
16
+ KEY_HEX_C="6363636363636363636363636363636363636363636363636363636363636363"
17
+
18
+ cleanup() {
19
+ docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
20
+ rm -rf "$TMPDIR"
21
+ }
22
+ trap cleanup EXIT
23
+
24
+ ensure_ruby_extension() {
25
+ (cd "$ROOT" && bundle exec rake compile)
26
+ }
27
+
28
+ prepare_packaged_gem() {
29
+ rm -rf "$PACKED"
30
+ mkdir -p "$PACKED"
31
+ (cd "$ROOT" && gem build secure_db_fields.gemspec >/dev/null)
32
+ gem unpack "$ROOT/secure_db_fields-0.1.1.gem" --target "$PACKED" >/dev/null
33
+ }
34
+
35
+ ruby_from_packaged() {
36
+ ruby -I "$PACKED/secure_db_fields-0.1.1/lib" "$PACKED/secure_db_fields-0.1.1/exe/secure_db_fields" "$@"
37
+ }
38
+
39
+ build_deployment_bundle() {
40
+ ruby_from_packaged db package mysql --output "$ARCHIVE" --force
41
+ test -s "$ARCHIVE"
42
+ tar -tzf "$ARCHIVE" | grep -Fx 'secure_db_fields-mysql-0.1.1/Makefile' >/dev/null
43
+ tar -tzf "$ARCHIVE" | grep -Fx 'secure_db_fields-mysql-0.1.1/bin/install' >/dev/null
44
+ if tar -tzf "$ARCHIVE" | grep -E '/\.DS_Store$' >/dev/null; then
45
+ echo 'deployment bundle must not contain .DS_Store' >&2
46
+ exit 2
47
+ fi
48
+ }
49
+
50
+ install_mysql_build_tools() {
51
+ docker exec -u 0 "$CONTAINER" sh -ceu '
52
+ if command -v yum >/dev/null 2>&1; then
53
+ yum install -y gcc make binutils tar gzip openssl-devel
54
+ elif command -v microdnf >/dev/null 2>&1; then
55
+ microdnf install -y gcc make binutils tar gzip openssl-devel
56
+ elif command -v apt-get >/dev/null 2>&1; then
57
+ apt-get update
58
+ DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential tar gzip libssl-dev
59
+ else
60
+ echo "no supported package manager in MySQL container" >&2
61
+ exit 2
62
+ fi
63
+ command -v cc
64
+ command -v make
65
+ test -f /usr/include/openssl/evp.h || find /usr/include -name evp.h | head -1 | grep -q .
66
+ '
67
+ }
68
+
69
+ install_bundle_on_db_host() {
70
+ local bundle_root="$BUNDLE_PARENT/secure_db_fields-mysql-0.1.1"
71
+ docker cp "$ARCHIVE" "$CONTAINER:/tmp/secure_db_fields-mysql.tar.gz"
72
+ docker exec -u 0 "$CONTAINER" sh -ceu '
73
+ rm -rf /opt/secure_db_fields-mysql-0.1.1
74
+ tar -xzf /tmp/secure_db_fields-mysql.tar.gz -C /opt
75
+ cd /opt/secure_db_fields-mysql-0.1.1
76
+ make verify
77
+ make doctor
78
+ make install
79
+ make enable
80
+ make status
81
+ '
82
+ }
83
+
84
+ write_key_file() {
85
+ docker exec -u 0 "$CONTAINER" sh -ceu "
86
+ install -d -m 0750 -o root -g mysql /etc/secure_db_fields
87
+ cat > /etc/secure_db_fields/keys.env <<'EOF'
88
+ SDF_ACTIVE_KEY_ID=1
89
+ SDF_ENC_KEY_1_HEX=$KEY_HEX_A
90
+ SDF_BIDX_KEY_HEX=$KEY_HEX_B
91
+ SDF_BIDX_PHONE_KEY_HEX=$KEY_HEX_B
92
+ SDF_BIDX_PHONE_P7_KEY_HEX=$KEY_HEX_C
93
+ EOF
94
+ chown root:mysql /etc/secure_db_fields/keys.env
95
+ chmod 0640 /etc/secure_db_fields/keys.env
96
+ "
97
+ }
98
+
99
+ make_fixture_sql() {
100
+ ruby -I "$ROOT/lib" <<'RUBY' > "$TMPDIR/fixture.env"
101
+ require "securerandom"
102
+ require "secure_db_fields"
103
+ enc_key = "a" * 32
104
+ phone_key = "b" * 32
105
+ p7_key = "c" * 32
106
+ uid = SecureRandom.random_bytes(16)
107
+ phone = "+77771234567"
108
+ aad = SecureDBFields::Crypto.aad("clients", "phone", uid)
109
+ envelope = SecureDBFields::Crypto.encrypt(phone, key: enc_key, aad: aad, key_id: 1)
110
+ puts "UID_HEX=#{uid.unpack1("H*")}"
111
+ puts "PHONE=#{phone}"
112
+ puts "ENC_HEX=#{envelope.unpack1("H*")}"
113
+ puts "BIDX_HEX=#{SecureDBFields::Crypto.phone_blind_index(phone, key: phone_key).unpack1("H*")}"
114
+ puts "P7_HEX=#{SecureDBFields::Crypto.phone_prefix_blind_index(phone, prefix_digits: 7, key: p7_key).unpack1("H*")}"
115
+ RUBY
116
+ source "$TMPDIR/fixture.env"
117
+ cat > "$TMPDIR/app.sql" <<SQL
118
+ DROP DATABASE IF EXISTS app;
119
+ DROP DATABASE IF EXISTS admin;
120
+ CREATE DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
121
+ CREATE DATABASE admin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
122
+ USE app;
123
+ CREATE TABLE clients (
124
+ id INT PRIMARY KEY,
125
+ secure_row_uid BINARY(16) NOT NULL,
126
+ phone_enc LONGBLOB NOT NULL,
127
+ phone_bidx BINARY(32) NOT NULL,
128
+ phone_bidx_p7 BINARY(32) NOT NULL,
129
+ created_at DATETIME NOT NULL,
130
+ INDEX idx_clients_phone_bidx(phone_bidx),
131
+ INDEX idx_clients_phone_bidx_p7(phone_bidx_p7)
132
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
133
+ INSERT INTO clients VALUES (1, UNHEX('$UID_HEX'), UNHEX('$ENC_HEX'), UNHEX('$BIDX_HEX'), UNHEX('$P7_HEX'), '2026-07-08 12:00:00');
134
+ SQL
135
+ ruby_from_packaged db view mysql \
136
+ --table app.clients \
137
+ --field phone:phone_enc \
138
+ --uid-column secure_row_uid \
139
+ --view admin.clients_readable \
140
+ --columns id,created_at \
141
+ --output "$TMPDIR/view.sql" >/dev/null
142
+ }
143
+
144
+ q() {
145
+ docker exec "$CONTAINER" mysql --default-character-set=utf8mb4 --batch --skip-column-names --raw -e "$1"
146
+ }
147
+
148
+ assert_eq() {
149
+ local label="$1"
150
+ local expected="$2"
151
+ local actual="$3"
152
+ if [ "$actual" != "$expected" ]; then
153
+ echo "FAIL $label: expected [$expected], got [$actual]" >&2
154
+ exit 1
155
+ fi
156
+ echo "ok - $label"
157
+ }
158
+
159
+ assert_contains() {
160
+ local label="$1"
161
+ local haystack="$2"
162
+ local needle="$3"
163
+ if ! printf '%s' "$haystack" | grep -Fq "$needle"; then
164
+ echo "FAIL $label: missing [$needle] in [$haystack]" >&2
165
+ exit 1
166
+ fi
167
+ echo "ok - $label"
168
+ }
169
+
170
+ ensure_ruby_extension
171
+ prepare_packaged_gem
172
+ build_deployment_bundle
173
+
174
+ docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
175
+ docker run --platform "$PLATFORM" -d --name "$CONTAINER" -e MYSQL_ALLOW_EMPTY_PASSWORD=1 "$IMAGE" \
176
+ --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci >/dev/null
177
+
178
+ echo "waiting for mysqld in $IMAGE..."
179
+ deadline=$(( $(date +%s) + WAIT_SECONDS ))
180
+ until docker exec "$CONTAINER" mysqladmin ping --silent 2>/dev/null; do
181
+ if [ "$(date +%s)" -ge "$deadline" ]; then
182
+ echo "mysqld did not become ready within ${WAIT_SECONDS}s" >&2
183
+ docker logs "$CONTAINER" >&2 || true
184
+ exit 1
185
+ fi
186
+ sleep 2
187
+ done
188
+
189
+ install_mysql_build_tools
190
+ write_key_file
191
+ install_bundle_on_db_host
192
+ make_fixture_sql
193
+
194
+ docker exec -i "$CONTAINER" mysql --default-character-set=utf8mb4 < "$TMPDIR/app.sql"
195
+ docker exec -i "$CONTAINER" mysql --default-character-set=utf8mb4 < "$TMPDIR/view.sql"
196
+
197
+ version="$(q 'SELECT secure_db_fields_version();')"
198
+ assert_contains 'version' "$version" 'secure_db_fields 0.1.1'
199
+ assert_eq 'valid envelope' '1' "$(q 'SELECT secure_db_fields_is_valid_envelope(phone_enc) FROM app.clients WHERE id=1;')"
200
+ assert_eq 'envelope key id' '1' "$(q 'SELECT secure_db_fields_envelope_key_id(phone_enc) FROM app.clients WHERE id=1;')"
201
+ assert_eq 'decrypt field' '+77771234567' "$(q "SELECT secure_db_fields_decrypt_field(phone_enc, 'clients', 'phone', secure_row_uid) FROM app.clients WHERE id=1;")"
202
+ assert_eq 'admin view decrypt' '+77771234567' "$(q 'SELECT phone FROM admin.clients_readable WHERE id=1;')"
203
+ assert_eq 'exact bidx indexed search' '1' "$(q "SELECT COUNT(*) FROM app.clients WHERE phone_bidx = secure_phone_bidx('+77771234567');")"
204
+ assert_eq 'prefix bidx indexed search' '1' "$(q "SELECT COUNT(*) FROM app.clients WHERE phone_bidx_p7 = secure_phone_prefix_bidx('+77771234567', 7);")"
205
+ assert_eq 'invalid phone bidx is null' '1' "$(q "SELECT secure_phone_bidx('87771234567') IS NULL;")"
206
+ assert_eq 'wrong aad decrypt is null' '1' "$(q "SELECT secure_db_fields_decrypt_field(phone_enc, 'clients', 'email', secure_row_uid) IS NULL FROM app.clients WHERE id=1;")"
207
+
208
+ explain_exact="$(q "EXPLAIN SELECT id FROM app.clients WHERE phone_bidx = secure_phone_bidx('+77771234567');")"
209
+ assert_contains 'EXPLAIN uses exact bidx index' "$explain_exact" 'idx_clients_phone_bidx'
210
+ explain_prefix="$(q "EXPLAIN SELECT id FROM app.clients WHERE phone_bidx_p7 = secure_phone_prefix_bidx('+77771234567', 7);")"
211
+ assert_contains 'EXPLAIN uses prefix bidx index' "$explain_prefix" 'idx_clients_phone_bidx_p7'
212
+
213
+ docker exec -u 0 "$CONTAINER" sh -ceu 'cd /opt/secure_db_fields-mysql-0.1.1 && CONFIRM=REMOVE_SECURE_DB_FIELDS make uninstall'
214
+ assert_eq 'functions removed' '0' "$(q "SELECT COUNT(*) FROM mysql.func WHERE dl = 'secure_db_fields_mysql.so';")"
215
+
216
+ echo 'secure_db_fields MySQL 5.7 UDF e2e: ok'
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secure_db_fields
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Roman Khaidarov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-07-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake-compiler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ description: 'A narrow production-oriented field encryption layer: AES-256-GCM envelope,
56
+ HMAC-SHA256 blind indexes, Ruby extension, MySQL UDF source, admin SQL contracts,
57
+ and migration helpers.'
58
+ email:
59
+ - romnhajdarov@gmail.com
60
+ executables:
61
+ - secure_db_fields
62
+ extensions:
63
+ - ext/secure_db_fields/extconf.rb
64
+ extra_rdoc_files: []
65
+ files:
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - db_deployment/mysql/Makefile
70
+ - db_deployment/mysql/README.md
71
+ - db_deployment/mysql/bin/common
72
+ - db_deployment/mysql/bin/disable
73
+ - db_deployment/mysql/bin/doctor
74
+ - db_deployment/mysql/bin/enable
75
+ - db_deployment/mysql/bin/install
76
+ - db_deployment/mysql/bin/status
77
+ - db_deployment/mysql/bin/uninstall
78
+ - db_deployment/mysql/bin/verify
79
+ - db_deployment/mysql/sql/examples.sql
80
+ - db_deployment/mysql/sql/install.sql
81
+ - db_deployment/mysql/sql/uninstall.sql
82
+ - docs/final_decision.md
83
+ - docs/implementation_notes.md
84
+ - docs/ruby_27_mysql_57.md
85
+ - examples/clients_phone_migration.sql
86
+ - examples/keys.env.example
87
+ - examples/ruby_usage.rb
88
+ - exe/secure_db_fields
89
+ - ext/secure_db_fields/extconf.rb
90
+ - ext/secure_db_fields/secure_db_fields_core.c
91
+ - ext/secure_db_fields/secure_db_fields_core.h
92
+ - ext/secure_db_fields/secure_db_fields_ext.c
93
+ - lib/secure_db_fields.rb
94
+ - lib/secure_db_fields/active_record.rb
95
+ - lib/secure_db_fields/crypto.rb
96
+ - lib/secure_db_fields/db_deployment.rb
97
+ - lib/secure_db_fields/keys.rb
98
+ - lib/secure_db_fields/phone.rb
99
+ - lib/secure_db_fields/version.rb
100
+ - mysql_udf/Makefile
101
+ - mysql_udf/README.md
102
+ - mysql_udf/bin/install
103
+ - mysql_udf/bin/uninstall
104
+ - mysql_udf/bin/verify
105
+ - mysql_udf/sql/examples.sql
106
+ - mysql_udf/sql/install.sql
107
+ - mysql_udf/sql/uninstall.sql
108
+ - mysql_udf/src/mysql_udf_abi_57.h
109
+ - mysql_udf/src/secure_db_fields_mysql.c
110
+ - mysql_udf/test/run_e2e.sh
111
+ homepage: https://github.com/roman-haidarov/secure_db_fields
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ homepage_uri: https://github.com/roman-haidarov/secure_db_fields
116
+ source_code_uri: https://github.com/roman-haidarov/secure_db_fields/tree/main
117
+ changelog_uri: https://github.com/roman-haidarov/secure_db_fields/blob/main/CHANGELOG.md
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 2.7.1
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.4.22
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Field-level encryption for Ruby + MySQL 5.7 with blind indexes and DB UDFs
137
+ test_files: []