476 lines
15 KiB
Markdown
476 lines
15 KiB
Markdown
# Runbook: Implementasi Enkripsi PII Pasien (UU PDP) — Production
|
|
|
|
> Dibuat: 2026-05-31 | Task: FHM31052601IBL
|
|
> Teknologi: AES-256-GCM, Trigram Blind Index, PHP CodeIgniter 3, MariaDB
|
|
|
|
---
|
|
|
|
## Arsitektur
|
|
|
|
```
|
|
.env (passphrase)
|
|
│
|
|
▼
|
|
Ibl_encryptor library
|
|
├── encrypt/decrypt → kolom _enc (AES-256-GCM)
|
|
└── search_bidx → kolom _bidx (HMAC-SHA256 trigram, untuk search)
|
|
|
|
m_patient (plaintext kolom lama = MASKED)
|
|
├── M_PatientName → "FAJRI H*******" (masked)
|
|
├── M_PatientName_enc → "base64ciphertext" (encrypted, real value)
|
|
├── M_PatientName_bidx→ ["tok1","tok2",...] (search index)
|
|
└── ...field lainnya
|
|
|
|
Search patient: nama + HP + DOB + NIK via JSON_CONTAINS(_bidx)
|
|
Read patient : decrypt _enc di PHP sebelum return ke client
|
|
```
|
|
|
|
---
|
|
|
|
## Pre-flight Checklist
|
|
|
|
```bash
|
|
# 1. Pastikan disk minimal 10GB free
|
|
df -h /
|
|
|
|
# 2. Catat ukuran DB sebelum (untuk verifikasi)
|
|
mysql -e "SELECT table_name, ROUND((data_length+index_length)/1024/1024,1) MB
|
|
FROM information_schema.tables WHERE table_schema='one_lab'
|
|
ORDER BY (data_length+index_length) DESC LIMIT 15;"
|
|
|
|
# 3. Pastikan tidak ada proses berat berjalan
|
|
pgrep -a php
|
|
```
|
|
|
|
> ⚠️ **Disk Space**: Enkripsi menambah ~1-2GB ke database (kolom _enc + _bidx).
|
|
> Jika disk penuh, lihat bagian **Troubleshooting** di bawah.
|
|
|
|
---
|
|
|
|
## Urutan Eksekusi di Production
|
|
|
|
### Step 1 — Buat file `.env`
|
|
|
|
```bash
|
|
cat > /path/to/one-api-lab/.env << 'EOF'
|
|
IBL_ENCRYPT_KEY=<passphrase-kamu>
|
|
IBL_ENCRYPT_SEARCH_KEY=<passphrase-search-kamu>
|
|
EOF
|
|
chmod 600 /path/to/one-api-lab/.env
|
|
```
|
|
|
|
> ⚠️ **WAJIB** simpan kedua passphrase di password manager sebelum lanjut.
|
|
> Key hilang = data di `_enc` tidak bisa didekripsi selamanya.
|
|
|
|
---
|
|
|
|
### Step 2 — Backup Database
|
|
|
|
```bash
|
|
bash scripts/backup_pdp_tables.sh
|
|
# Backup tersimpan di: ~/backup_pdp_YYYY_MM_DD_HHMMSS/
|
|
# Verifikasi backup ada dan tidak kosong
|
|
ls -lh ~/backup_pdp_*/
|
|
```
|
|
|
|
---
|
|
|
|
### Step 3 — Jalankan SQL Migration (tambah kolom + update trigger)
|
|
|
|
```bash
|
|
# Tambah kolom _enc dan _bidx
|
|
mysql one_lab < sql/manual_changes/2026-05-31-pdp-encrypt-columns.sql
|
|
|
|
# Update trigger m_patient & m_patientaddress (pakai _enc di log JSON)
|
|
mysql one_lab < sql/manual_changes/2026-05-31-pdp-update-triggers-enc.sql
|
|
|
|
# Ubah M_PatientDOB dari DATE ke VARCHAR(20) agar masked value tersimpan
|
|
mysql one_lab < sql/manual_changes/2026-06-11-alter-m-patient-dob-to-varchar.sql
|
|
|
|
# Ubah Mcu_PreregisterPatientsDOB dari DATE ke VARCHAR(20) (sama)
|
|
mysql one_lab < sql/manual_changes/2026-06-11-alter-mcu-preregister-dob-to-varchar.sql
|
|
|
|
# Verifikasi kolom terbentuk
|
|
mysql -e "SHOW COLUMNS FROM one_lab.m_patient LIKE '%_enc';"
|
|
mysql -e "SHOW COLUMNS FROM one_lab.m_patient LIKE '%_bidx';"
|
|
mysql -e "SHOW COLUMNS FROM one_lab.m_patientaddress LIKE '%_enc';"
|
|
mysql -e "SHOW COLUMNS FROM one_lab.m_patient WHERE Field = 'M_PatientDOB'\G"
|
|
```
|
|
|
|
---
|
|
|
|
### Step 4 — Encrypt Data Pasien (m_patient)
|
|
|
|
```bash
|
|
# Estimasi: 30-60 menit untuk 178K rows
|
|
php scripts/migrate_encrypt_patient.php
|
|
|
|
# Verifikasi
|
|
mysql -e "SELECT COUNT(*) total, COUNT(M_PatientName_enc) done
|
|
FROM one_lab.m_patient;"
|
|
# Expected: total == done
|
|
```
|
|
|
|
---
|
|
|
|
### Step 5 — Populate NIK Bidx
|
|
|
|
```bash
|
|
# Isi search index untuk NIK (dari _enc yang sudah ada)
|
|
# Estimasi: 5-10 menit
|
|
php scripts/migrate_nik_bidx.php
|
|
|
|
# Verifikasi
|
|
mysql -e "SELECT COUNT(*) total, COUNT(M_PatientNIK_bidx) done
|
|
FROM one_lab.m_patient WHERE M_PatientNIK_enc IS NOT NULL;"
|
|
```
|
|
|
|
---
|
|
|
|
### Step 6 — Encrypt Alamat Pasien (m_patientaddress)
|
|
|
|
```bash
|
|
# Enkripsi alamat TANPA bidx (hemat disk)
|
|
# Estimasi: 15-30 menit untuk 133K rows
|
|
php scripts/migrate_address_enc.php
|
|
|
|
# Verifikasi
|
|
mysql -e "SELECT COUNT(*) total, COUNT(M_PatientAddressDescription_enc) done
|
|
FROM one_lab.m_patientaddress;"
|
|
# Expected: total == done
|
|
```
|
|
|
|
---
|
|
|
|
### Step 6b — Encrypt Address (m_patientaddress)
|
|
|
|
```bash
|
|
# 357 baris tersisa di dev — jalankan sampai selesai di prod
|
|
php scripts/migrate_address_enc.php
|
|
|
|
# Verifikasi
|
|
mysql -e "SELECT COUNT(*) total, COUNT(M_PatientAddressDescription_enc) done FROM one_lab.m_patientaddress;"
|
|
```
|
|
|
|
---
|
|
|
|
### Step 7 — Encrypt Tujuan Pengiriman Hasil (t_orderdelivery)
|
|
|
|
```bash
|
|
# HANYA t_orderdelivery — berisi email/HP pasien (PII nyata, bisa dimasking)
|
|
# Tabel hasil lab (t_orderdetail, so_resultentry*, dll) TIDAK dienkripsi —
|
|
# lihat bagian "Keputusan Arsitektur" di bawah
|
|
php scripts/migrate_encrypt_orderdelivery.php
|
|
|
|
# Verifikasi
|
|
mysql -e "SELECT COUNT(*) total, COUNT(T_OrderDeliveryDestination_enc) done
|
|
FROM one_lab.t_orderdelivery;"
|
|
```
|
|
|
|
---
|
|
|
|
### Step 8 — Masking Kolom Plaintext
|
|
|
|
```bash
|
|
# Masking semua kolom PII lama di m_patient & m_patientaddress
|
|
# Format nama: "FAJRI H*******" (kata pertama penuh + inisial)
|
|
php scripts/mask_patient_plaintext.php
|
|
|
|
# Re-mask nama dengan format terbaru (jika sudah pernah dimasking sebelumnya)
|
|
php scripts/remask_patient_name.php
|
|
|
|
# Verifikasi: cek beberapa baris
|
|
mysql -e "SELECT M_PatientID, M_PatientName, M_PatientHP, M_PatientEmail
|
|
FROM one_lab.m_patient ORDER BY RAND() LIMIT 5;"
|
|
# Expected: tampil "BUDI S******", "0812*****890", "bu***@gmail.com"
|
|
```
|
|
|
|
---
|
|
|
|
### Step 8b — Buat patient_print_cache (untuk BIRT decrypt)
|
|
|
|
```bash
|
|
mysql one_lab < sql/manual_changes/2026-05-31-pdp-birt-sp-cache-join.sql
|
|
```
|
|
|
|
Tabel ini dibutuhkan oleh:
|
|
- 6 SP header BIRT (sp_rpt_hasil_header, _2, _eng, sp_rpt_fo_001, sp_rpt_card_patient, sp_rpt_t_002)
|
|
- Birt_proxy.php controller
|
|
- Ibl_patient_decrypt library
|
|
- Qr_report_uploader, Ibl_merge_report_gateway, send_email.php
|
|
|
|
---
|
|
|
|
### Step 8c — Update sp_rpt_t_002_eng (jika dipakai)
|
|
|
|
```bash
|
|
# Cek apakah sp_rpt_t_002_eng ada
|
|
mysql -e "SHOW PROCEDURE STATUS WHERE Db='one_lab' AND Name='sp_rpt_t_002_eng'\G"
|
|
# Jika ada, update manual dengan LEFT JOIN ke patient_print_cache
|
|
# (sama seperti sp_rpt_t_002 di 2026-05-31-pdp-birt-sp-cache-join.sql)
|
|
```
|
|
|
|
---
|
|
|
|
### Catatan Disk Space untuk Production
|
|
|
|
> ⚠️ **Sangat penting**: Pastikan disk minimal **10GB free** sebelum mulai.
|
|
> Trigger m_patient + m_patientaddress nulis ke log_patient setiap UPDATE.
|
|
> Untuk migration + masking 178K+133K rows → log bisa 2-3GB.
|
|
>
|
|
> **Strategi aman**:
|
|
> 1. DROP trigger dulu (`vm_patient_ai`, `vm_patient_bu`, `m_patientaddress_ai`, `m_patientaddress_bu`)
|
|
> 2. Jalankan semua migration + masking scripts
|
|
> 3. Recreate trigger: `mysql one_lab < sql/manual_changes/2026-05-31-pdp-update-triggers-enc.sql`
|
|
>
|
|
> File trigger SQL ada di: `sql/manual_changes/2026-05-31-pdp-update-triggers-enc.sql`
|
|
|
|
---
|
|
|
|
### Step 9 — Truncate Log Lama (Opsional tapi Direkomendasikan)
|
|
|
|
```bash
|
|
# log_patient berisi JSON plaintext PII dari sebelum enkripsi
|
|
# Truncate meningkatkan compliance (hapus data PII lama yang tidak terenkripsi)
|
|
mysql one_lab_log -e 'TRUNCATE TABLE log_patient;'
|
|
|
|
# Verifikasi
|
|
mysql -e "SELECT COUNT(*) FROM one_lab_log.log_patient;"
|
|
# Expected: 0
|
|
```
|
|
|
|
---
|
|
|
|
### Step 10 — Verifikasi End-to-End
|
|
|
|
```bash
|
|
# 1. Cek search patient berjalan
|
|
curl -s -X POST https://[SERVER]/mockup/fo/ibl_registration/patient/search \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"token":"[VALID_TOKEN]","search":"BUDI","noreg":"","current_page":1}' \
|
|
| python3 -m json.tool | head -20
|
|
|
|
# 2. Cek data terdekripsi dengan benar (nama muncul lengkap, bukan masked)
|
|
# Expected di response: "M_PatientName": "BUDI SANTOSO" (bukan "BUDI S******")
|
|
|
|
# 3. Cek disk usage akhir
|
|
df -h /
|
|
|
|
# 4. Cek MySQL masih sehat
|
|
mysql -e "SHOW STATUS LIKE 'Threads_connected';"
|
|
```
|
|
|
|
---
|
|
|
|
## Field yang Dienkripsi
|
|
|
|
### `one_lab.m_patient`
|
|
| Field | Tipe kolom plain | `_enc` | `_bidx` (search) | Catatan |
|
|
|-------|:---:|:------:|:----------------:|---------|
|
|
| M_PatientName | VARCHAR | ✅ | ✅ | |
|
|
| M_PatientHP | VARCHAR | ✅ | ✅ | |
|
|
| M_PatientDOB | **VARCHAR(20)** | ✅ | ✅ | Diubah dari DATE → VARCHAR agar mask `**-**-YYYY` tersimpan |
|
|
| M_PatientNIK | VARCHAR | ✅ | ✅ | ⚠️ `M_PatientNIK_bidx` diisi dari **`M_PatientIDNumber`**, bukan NIK |
|
|
| M_PatientEmail | VARCHAR | ✅ | — | |
|
|
| M_PatientPhone | VARCHAR | ✅ | — | |
|
|
| M_PatientPOB | VARCHAR | ✅ | — | |
|
|
| M_PatientIDNumber | VARCHAR | ✅ | — | Masked plain, bidx-nya lewat `M_PatientNIK_bidx` |
|
|
| M_PatientNIP | VARCHAR | ✅ | — | |
|
|
|
|
### `one_lab.m_patientaddress`
|
|
| Field | `_enc` | `_bidx` |
|
|
|-------|:------:|:-------:|
|
|
| M_PatientAddressDescription | ✅ | — (dihapus, hemat disk) |
|
|
| M_PatientAddressEmail | ✅ | — |
|
|
| M_PatientAddressPhone | ✅ | — |
|
|
|
|
### Tujuan Pengiriman Hasil (PII nyata)
|
|
| Tabel | Field |
|
|
|-------|-------|
|
|
| `t_orderdelivery` | T_OrderDeliveryDestination (email/HP) |
|
|
|
|
### Log
|
|
| Tabel | Field |
|
|
|-------|-------|
|
|
| `one_lab_log.log_patient` | Log_PatientJsonBefore/After (**di-truncate di production**) |
|
|
| `one_lab_log.log_fo` | Log_FoJson |
|
|
| `one_lab_log.log_resultentry` | Log_ResultEntryJSONBefore/After |
|
|
|
|
### TIDAK Dienkripsi (keputusan disengaja)
|
|
| Tabel | Alasan |
|
|
|-------|--------|
|
|
| `t_orderdetail`, `t_orderheader` | Nilai hasil lab bukan PII tanpa identitas pasien. Trigger butuh plaintext untuk flag H/L/N. |
|
|
| `so_resultentry_*`, `member_eligible` | Nilai klinis, bukan PII langsung. Plaintext dibutuhkan proses operasional. |
|
|
| `mcu_resume_results` | JSON nilai lab tanpa PII. Enkripsi memberatkan global MCU report. |
|
|
|
|
**Perlindungan hasil lab** tetap via: identitas pasien terenkripsi di `m_patient` + access control + audit log.
|
|
|
|
---
|
|
|
|
## Format Masking Kolom Plaintext
|
|
|
|
| Field | Format | Contoh |
|
|
|-------|--------|--------|
|
|
| Nama (multi kata) | Kata pertama penuh + inisial+bintang per kata | `FAJRI H******* M****` |
|
|
| Nama (satu kata) | 2 karakter pertama + bintang sisanya | `SI**` / `BI*****` |
|
|
| HP/Phone | 4 digit pertama + bintang + 3 digit akhir | `0812*****890` |
|
|
| Email | 2 huruf pertama + *** + @domain | `fa***@gmail.com` |
|
|
| NIK/IDNumber | 4 digit pertama + *** + 2 digit akhir | `3201***01` |
|
|
| POB | 2 huruf pertama + *** | `JA***` |
|
|
| Alamat | 5 karakter pertama + *** | `Jl. S***` |
|
|
|
|
---
|
|
|
|
## Search Pasien
|
|
|
|
Parameter via `+` separator di field `search`:
|
|
```
|
|
search=NAMA+HP+DOB+NIK
|
|
```
|
|
| Posisi | Field | Contoh |
|
|
|--------|-------|--------|
|
|
| `e[0]` | Nama (min 3 karakter) | `BUD` |
|
|
| `e[1]` | HP (min 3 karakter) | `081` |
|
|
| `e[2]` | DOB format dd-mm-yyyy | `25-0` |
|
|
| `e[3]` | NIK (min 3 karakter) | `320` |
|
|
|
|
---
|
|
|
|
## BIRT Report & FPDF
|
|
|
|
### Strategi Decrypt untuk Report
|
|
|
|
**BIRT Reports** (`print_transaction`):
|
|
- PHP `Birt_proxy.php` → decrypt PII → INSERT `patient_print_cache` → call BIRT
|
|
- 6 SP header yang diupdate dengan LEFT JOIN ke cache: `sp_rpt_hasil_header`, `sp_rpt_hasil_header_2`, `sp_rpt_hasil_header_eng`, `sp_rpt_fo_001`, `sp_rpt_card_patient`, `sp_rpt_t_002`
|
|
- SP signature tidak berubah — `.rptdesign` tidak perlu diupdate
|
|
- Cache TTL: 5 menit, auto-cleanup di request berikutnya
|
|
|
|
**Endpoint Birt_proxy:**
|
|
- `POST /tools/birt_proxy/stream` — return PDF binary langsung
|
|
- `POST /tools/birt_proxy/get_url` — return URL untuk buka di browser
|
|
|
|
**FPDF Controllers** (`tools/`):
|
|
- `Inform_consent.php`, `Medical_checkup_report.php` — decrypt langsung dari `_enc` (direct SQL)
|
|
- `Kartu_kontrol.php`, `Rpt_t_002.php`, `Rpt_t_002_eng.php` — populate cache → call SP → delete cache
|
|
|
|
**SQL produksi yang perlu dijalankan:**
|
|
```bash
|
|
mysql one_lab < sql/manual_changes/2026-05-31-pdp-birt-sp-cache-join.sql
|
|
```
|
|
|
|
---
|
|
|
|
## Controller yang Sudah Diupdate (Decrypt + Encrypt)
|
|
|
|
| Controller | Fungsi |
|
|
|-----------|--------|
|
|
| `mockup/fo/ibl_registration/Patient.php` | Search, add, edit pasien FO |
|
|
| `mockup/fo/ibl_registration/Order.php` | Order management (nama, email, HP) |
|
|
| `mockup/fo/ibl_registration/Payment.php` | Kasir (nama pasien) |
|
|
| `mockup/fo/ibl_registration/History.php` | History delivery (email/HP) |
|
|
| `mockup/fo/ibl_registration/Delivery.php` | Pengiriman hasil (email/HP) |
|
|
| `mockup/fo/ibl_registration/Order copy.php` | Order MCU |
|
|
| `mockup/masterdata/Patientv4.php` | Masterdata pasien — tampil data **lengkap** |
|
|
| `klinik/Registrationv3.php` | Registrasi & edit pasien klinik (`save()`, `newpatient()`) |
|
|
| `mockup/setupmcuoffline-ibl/Preregister.php` | MCU offline IBL — batch CSV & newpatient |
|
|
| `mockup/mcuoffline/Preregisterapp.php` | MCU offline app — batch CSV, search, newpatient |
|
|
|
|
## mcu_preregister_patients — Masking
|
|
|
|
Data PII yang dimasking saat INSERT ke staging table `mcu_preregister_patients`:
|
|
|
|
| Field | Tipe kolom | Mask |
|
|
|-------|:---:|------|
|
|
| `*PatientName` | VARCHAR | `_mask_name()` |
|
|
| `*KTP` | VARCHAR | `_mask_id()` |
|
|
| `*NIP` / `*NIK` | VARCHAR | `_mask_id()` |
|
|
| `*Email` | VARCHAR | `_mask_email()` |
|
|
| `*Hp` | VARCHAR | `_mask_phone()` |
|
|
| `*DOB` | **VARCHAR(20)** | `_mask_dob()` — diubah dari DATE → VARCHAR agar mask `**-**-YYYY` tersimpan |
|
|
|
|
Tidak ada kolom `_enc` di tabel ini — data asli bisa diambil dari `m_patient` via `*M_PatientID`.
|
|
|
|
4 lokasi INSERT yang sudah diupdate: `savecsv()` & `save()` di setupmcuoffline-ibl, `save()` & `savenewform()` di mcuoffline/Preregisterapp.
|
|
|
|
## one_lab_dashboard.mcu_patient — Enkripsi via SP
|
|
|
|
Kolom `Mcu_PatientName` dan `Mcu_PatientDOB` diubah ke `TEXT` dan menyimpan
|
|
nilai `_enc` dari `one_lab.m_patient` (JOIN via `Mcu_PreregisterPatientsM_PatientID`).
|
|
|
|
SP yang diupdate:
|
|
- `sp_upsert_mcu_patient_by_preregister_id`
|
|
- `sp_upsert_mcu_patient_by_mgm_mcuid`
|
|
|
|
SQL: `sql/manual_changes/2026-05-31-pdp-mcu-patient-dashboard-enc-sp.sql`
|
|
|
|
```bash
|
|
mysql one_lab < sql/manual_changes/2026-05-31-pdp-mcu-patient-dashboard-enc-sp.sql
|
|
```
|
|
|
|
## Controller yang Belum Diupdate (Tampil Data Masked)
|
|
|
|
Semua ~300+ controller lain otomatis tampilkan data termasking karena kolom
|
|
plaintext sudah dimasking. Tidak perlu update satu-satu untuk compliance dasar.
|
|
|
|
**Sprint berikutnya** — update controller prioritas yang butuh data lengkap:
|
|
- Sampling (samplinglab-vvii, samplingelectromedisnew)
|
|
- Result entry (resultentrysoothers-v20, resultentrysoxray-v8, resultentrysoelectromedis-v8)
|
|
- Result verification
|
|
- MCU resume (resumeindividufacelift)
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Disk Penuh Saat Migration
|
|
|
|
```bash
|
|
# Cek pemakai disk terbesar
|
|
du -sh /home/one/* /tmp/* 2>/dev/null | sort -rh | head -20
|
|
|
|
# Bersihkan file lama yang aman dihapus:
|
|
# - /home/one/project/one/dump_*.sql (backup lama)
|
|
# - /home/one/project/one/*.tar.gz (archive lama)
|
|
# - /tmp/intelephense (cache IDE)
|
|
|
|
# Bersihkan journal (butuh sudo)
|
|
sudo journalctl --vacuum-size=300M
|
|
sudo truncate -s 0 /var/log/btmp
|
|
```
|
|
|
|
### MySQL Crash (Disk Penuh)
|
|
|
|
```bash
|
|
# Restart MySQL setelah disk dibebaskan
|
|
sudo systemctl start mariadb
|
|
# atau
|
|
sudo service mysql start
|
|
```
|
|
|
|
### Migration Lambat (Trigger Overhead)
|
|
|
|
Trigger `m_patientaddress_bu` nulis ke `log_patient` setiap UPDATE.
|
|
Jika log_patient sangat besar (>1GB) dan disk hampir penuh:
|
|
|
|
```bash
|
|
# Truncate log lama (aman — data PII lama di log justru harus dihapus untuk compliance)
|
|
mysql one_lab_log -e 'TRUNCATE TABLE log_patient;'
|
|
```
|
|
|
|
### Restore jika Ada Masalah
|
|
|
|
```bash
|
|
mysql one_lab < ~/backup_pdp_YYYY_MM_DD_HHMMSS/one_lab_tables.sql
|
|
mysql one_lab_log < ~/backup_pdp_YYYY_MM_DD_HHMMSS/one_lab_log_tables.sql
|
|
```
|
|
|
|
---
|
|
|
|
## Catatan Key Management
|
|
|
|
- Key disimpan di `.env` — **JANGAN commit ke git** (sudah ada di `.gitignore`)
|
|
- Backup passphrase di: password manager + file enkripsi di lokasi terpisah dari server
|
|
- Key rotation di masa depan: perlu re-encrypt semua data (decrypt lama → encrypt baru)
|
|
- Tidak ada recovery jika key hilang
|