Files
BE_IBL/docs/superpowers/plans/2026-05-31-pdp-patient-encryption.md
sas.fajri c410d7bbd9 FHM31052601IBL - implementasi enkripsi PII pasien dan data medis (UU PDP)
- Tambah .env loader di index.php untuk IBL_ENCRYPT_KEY dan IBL_ENCRYPT_SEARCH_KEY
- Library Ibl_encryptor: AES-256-GCM encrypt/decrypt + trigram blind index untuk partial search
- SQL migration: tambah kolom _enc dan _bidx di 16 tabel (m_patient, m_patientaddress, hasil lab, log)
- Script backup_pdp_tables.sh: backup tabel terdampak sebelum migrasi
- Script migrate_encrypt_patient.php: enkripsi batch 178K data PII pasien
- Script migrate_encrypt_results.php: enkripsi data medis hasil lab dan log
- Patient.php: search via trigram blind index, add_new/edit enkripsi sebelum save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 14:07:42 +07:00

42 KiB
Raw Blame History

PDP Patient Data Encryption Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Enkripsi field PII pasien (m_patient, m_patientaddress) dan data medis hasil lab (12 tabel) menggunakan AES-256-GCM dengan key dari .env. Patient search tetap berjalan via trigram blind index; hasil lab cukup encrypt/decrypt langsung.

Architecture: Key enkripsi disimpan di .env dan di-load di index.php. Library Ibl_encryptor menangani encrypt/decrypt (AES-256-GCM) dan pembuatan trigram token (HMAC-SHA256) untuk pencarian partial. Kolom lama tetap ada selama masa migrasi, kolom _enc menyimpan ciphertext, kolom _bidx menyimpan JSON array token trigram untuk field yang digunakan search.

Tech Stack: PHP CodeIgniter 3, MySQL 5.7+, OpenSSL (AES-256-GCM), hash_hmac SHA-256


Field yang Dienkripsi

one_lab.m_patient

Field Kolom _enc Kolom _bidx (untuk search)
M_PatientName M_PatientName_enc M_PatientName_bidx
M_PatientHP M_PatientHP_enc M_PatientHP_bidx
M_PatientDOB M_PatientDOB_enc M_PatientDOB_bidx
M_PatientEmail M_PatientEmail_enc
M_PatientPhone M_PatientPhone_enc
M_PatientPOB M_PatientPOB_enc
M_PatientIDNumber M_PatientIDNumber_enc
M_PatientNIK M_PatientNIK_enc
M_PatientNIP M_PatientNIP_enc

one_lab.m_patientaddress

Field Kolom _enc Kolom _bidx
M_PatientAddressDescription M_PatientAddressDescription_enc M_PatientAddressDescription_bidx
M_PatientAddressEmail M_PatientAddressEmail_enc
M_PatientAddressPhone M_PatientAddressPhone_enc

File yang Dibuat / Dimodifikasi

File Action Tanggung Jawab
.env Create Menyimpan IBL_ENCRYPT_KEY dan IBL_ENCRYPT_SEARCH_KEY
index.php Modify Load .env ke $_ENV sebelum CI bootstrap
application/libraries/Ibl_encryptor.php Create AES-256-GCM encrypt/decrypt + trigram blind index
sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql Create ALTER TABLE tambah kolom _enc dan _bidx
scripts/migrate_encrypt_patient.php Create Batch migration script enkripsi 178K data lama
application/controllers/mockup/fo/ibl_registration/Patient.php Modify search(), add_new(), edit() pakai enkripsi

Task 1: Buat .env dan Load di index.php

Files:

  • Create: .env

  • Modify: index.php (tambah sebelum require_once BASEPATH.'core/CodeIgniter.php';)

  • Step 1: Buat file .env

IBL_ENCRYPT_KEY=<32-byte-hex-random>
IBL_ENCRYPT_SEARCH_KEY=<32-byte-hex-random>

Generate key-nya dengan:

php -r "echo bin2hex(random_bytes(32)) . PHP_EOL; echo bin2hex(random_bytes(32)) . PHP_EOL;"

Isi .env dengan output dua baris tersebut:

IBL_ENCRYPT_KEY=a1b2c3d4...   # baris 1
IBL_ENCRYPT_SEARCH_KEY=e5f6g7h8...  # baris 2
  • Step 2: Tambahkan .env ke .gitignore

Cek apakah .gitignore sudah ada:

cat .gitignore 2>/dev/null || echo "tidak ada"

Tambahkan baris:

.env
  • Step 3: Modifikasi index.php untuk load .env

Buka index.php. Temukan baris terakhir sebelum:

require_once BASEPATH.'core/CodeIgniter.php';

Tambahkan SEBELUM baris tersebut:

// Load .env
$_env_file = __DIR__ . '/.env';
if (file_exists($_env_file)) {
    foreach (file($_env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $_env_line) {
        if (strpos(trim($_env_line), '#') === 0) continue;
        [$_env_k, $_env_v] = array_map('trim', explode('=', $_env_line, 2));
        if ($_env_k !== '') $_ENV[$_env_k] = $_env_v;
    }
    unset($_env_file, $_env_line, $_env_k, $_env_v);
}
  • Step 4: Verifikasi key terbaca
php -r "
\$_ENV = [];
foreach (file('.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as \$l) {
    [\$k,\$v] = explode('=', \$l, 2);
    \$_ENV[trim(\$k)] = trim(\$v);
}
echo 'ENCRYPT_KEY len: ' . strlen(\$_ENV['IBL_ENCRYPT_KEY']) . PHP_EOL;
echo 'SEARCH_KEY len: ' . strlen(\$_ENV['IBL_ENCRYPT_SEARCH_KEY']) . PHP_EOL;
"

Expected:

ENCRYPT_KEY len: 64
SEARCH_KEY len: 64
  • Step 5: Commit
git add index.php .gitignore
git commit -m "TASKCODE - load .env encryption keys di bootstrap"

Ganti TASKCODE dengan kode task dari user. Jangan commit file .env.


Task 2: Library Ibl_encryptor

Files:

  • Create: application/libraries/Ibl_encryptor.php

  • Step 1: Buat library

<?php
defined('BASEPATH') or exit('No direct script access allowed');

class Ibl_encryptor
{
    private $key;
    private $search_key;
    private $cipher = 'aes-256-gcm';
    private $tag_length = 16;

    public function __construct()
    {
        $raw = $_ENV['IBL_ENCRYPT_KEY'] ?? '';
        $raw_s = $_ENV['IBL_ENCRYPT_SEARCH_KEY'] ?? '';
        if (strlen($raw) < 32 || strlen($raw_s) < 32) {
            log_message('error', 'Ibl_encryptor: IBL_ENCRYPT_KEY atau IBL_ENCRYPT_SEARCH_KEY tidak terdefinisi di .env');
        }
        $this->key = hex2bin($raw);
        $this->search_key = hex2bin($raw_s);
    }

    // Enkripsi plaintext → base64(iv + tag + ciphertext)
    public function encrypt($plaintext)
    {
        if ($plaintext === null || $plaintext === '') return null;
        $iv = random_bytes(12);
        $tag = '';
        $ct = openssl_encrypt($plaintext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv, $tag, '', $this->tag_length);
        if ($ct === false) return null;
        return base64_encode($iv . $tag . $ct);
    }

    // Dekripsi base64(iv + tag + ciphertext) → plaintext
    public function decrypt($ciphertext)
    {
        if ($ciphertext === null || $ciphertext === '') return null;
        $raw = base64_decode($ciphertext);
        if (strlen($raw) < 12 + $this->tag_length) return null;
        $iv  = substr($raw, 0, 12);
        $tag = substr($raw, 12, $this->tag_length);
        $ct  = substr($raw, 12 + $this->tag_length);
        $pt  = openssl_decrypt($ct, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv, $tag);
        return $pt === false ? null : $pt;
    }

    // Hasilkan JSON array token trigram (untuk partial search)
    // Input: string value, Output: JSON string dari array HMAC token
    public function search_bidx($value)
    {
        if ($value === null || $value === '') return null;
        $norm = mb_strtolower(trim($value), 'UTF-8');
        $tokens = [];
        $len = mb_strlen($norm, 'UTF-8');
        if ($len <= 3) {
            $tokens[] = $this->_token($norm);
        } else {
            for ($i = 0; $i <= $len - 3; $i++) {
                $tokens[] = $this->_token(mb_substr($norm, $i, 3, 'UTF-8'));
            }
        }
        return json_encode(array_values(array_unique($tokens)));
    }

    // Hasilkan token trigram dari query string (untuk WHERE IN saat search)
    // Kembalikan array token (bukan JSON)
    public function query_tokens($query)
    {
        if ($query === null || $query === '') return [];
        $norm = mb_strtolower(trim($query), 'UTF-8');
        $tokens = [];
        $len = mb_strlen($norm, 'UTF-8');
        if ($len <= 3) {
            $tokens[] = $this->_token($norm);
        } else {
            for ($i = 0; $i <= $len - 3; $i++) {
                $tokens[] = $this->_token(mb_substr($norm, $i, 3, 'UTF-8'));
            }
        }
        return array_values(array_unique($tokens));
    }

    private function _token($str)
    {
        return hash_hmac('sha256', $str, $this->search_key);
    }
}
  • Step 2: Test library via CLI

Buat file sementara scripts/test_encryptor.php:

<?php
define('BASEPATH', true);

// Simulasi .env
$_ENV['IBL_ENCRYPT_KEY']        = bin2hex(str_repeat('a', 32));
$_ENV['IBL_ENCRYPT_SEARCH_KEY'] = bin2hex(str_repeat('b', 32));

require __DIR__ . '/../application/libraries/Ibl_encryptor.php';
$enc = new Ibl_encryptor();

// Test encrypt/decrypt
$plain = 'BUDI SANTOSO';
$ct = $enc->encrypt($plain);
$pt = $enc->decrypt($ct);
assert($pt === $plain, "FAIL: decrypt mismatch");
echo "encrypt/decrypt: OK\n";

// Test null
assert($enc->encrypt(null) === null, "FAIL: null encrypt");
assert($enc->decrypt(null) === null, "FAIL: null decrypt");
echo "null handling: OK\n";

// Test trigram tokens
$bidx = json_decode($enc->search_bidx('BUDI'), true);
$qtok = $enc->query_tokens('udi');  // partial search "udi"
assert(in_array($qtok[0], $bidx), "FAIL: token 'udi' tidak ada di bidx BUDI");
echo "trigram search: OK\n";

echo "Semua test passed.\n";

Jalankan:

php scripts/test_encryptor.php

Expected:

encrypt/decrypt: OK
null handling: OK
trigram search: OK
Semua test passed.
  • Step 3: Hapus file test sementara
rm scripts/test_encryptor.php
  • Step 4: Commit
git add application/libraries/Ibl_encryptor.php
git commit -m "TASKCODE - tambah library Ibl_encryptor AES-256-GCM + trigram blind index"

Task 3: SQL Migration — Tambah Kolom _enc dan _bidx

Files:

  • Create: sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql

  • Step 1: Buat file SQL

-- UU PDP: tambah kolom enkripsi PII pasien
-- Kolom lama TIDAK dihapus (backward compat selama migrasi)
-- Setelah semua data termigrasi dan kode diupdate, kolom lama bisa di-drop di sprint berikutnya

-- m_patient: kolom _enc untuk semua field PII
ALTER TABLE one_lab.m_patient
    ADD COLUMN M_PatientName_enc       TEXT NULL AFTER M_PatientName,
    ADD COLUMN M_PatientName_bidx      MEDIUMTEXT NULL AFTER M_PatientName_enc,
    ADD COLUMN M_PatientHP_enc         TEXT NULL AFTER M_PatientHP,
    ADD COLUMN M_PatientHP_bidx        MEDIUMTEXT NULL AFTER M_PatientHP_enc,
    ADD COLUMN M_PatientDOB_enc        TEXT NULL AFTER M_PatientDOB,
    ADD COLUMN M_PatientDOB_bidx       MEDIUMTEXT NULL AFTER M_PatientDOB_enc,
    ADD COLUMN M_PatientEmail_enc      TEXT NULL AFTER M_PatientEmail,
    ADD COLUMN M_PatientPhone_enc      TEXT NULL AFTER M_PatientPhone,
    ADD COLUMN M_PatientPOB_enc        TEXT NULL AFTER M_PatientPOB,
    ADD COLUMN M_PatientIDNumber_enc   TEXT NULL AFTER M_PatientIDNumber,
    ADD COLUMN M_PatientNIK_enc        TEXT NULL AFTER M_PatientNIK,
    ADD COLUMN M_PatientNIP_enc        TEXT NULL AFTER M_PatientNIP;

-- m_patientaddress: kolom _enc untuk field PII
ALTER TABLE one_lab.m_patientaddress
    ADD COLUMN M_PatientAddressDescription_enc  TEXT NULL AFTER M_PatientAddressDescription,
    ADD COLUMN M_PatientAddressDescription_bidx MEDIUMTEXT NULL AFTER M_PatientAddressDescription_enc,
    ADD COLUMN M_PatientAddressEmail_enc        TEXT NULL AFTER M_PatientAddressEmail,
    ADD COLUMN M_PatientAddressPhone_enc        TEXT NULL AFTER M_PatientAddressPhone;
  • Step 2: Jalankan SQL di devone
ssh devone "mysql one_lab < /home/one/project/one/one-api-lab/sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql"

Atau jika file belum diupload, jalankan langsung:

ssh devone "mysql -e \"
ALTER TABLE one_lab.m_patient
    ADD COLUMN M_PatientName_enc TEXT NULL AFTER M_PatientName,
    ADD COLUMN M_PatientName_bidx MEDIUMTEXT NULL AFTER M_PatientName_enc,
    ADD COLUMN M_PatientHP_enc TEXT NULL AFTER M_PatientHP,
    ADD COLUMN M_PatientHP_bidx MEDIUMTEXT NULL AFTER M_PatientHP_enc,
    ADD COLUMN M_PatientDOB_enc TEXT NULL AFTER M_PatientDOB,
    ADD COLUMN M_PatientDOB_bidx MEDIUMTEXT NULL AFTER M_PatientDOB_enc,
    ADD COLUMN M_PatientEmail_enc TEXT NULL AFTER M_PatientEmail,
    ADD COLUMN M_PatientPhone_enc TEXT NULL AFTER M_PatientPhone,
    ADD COLUMN M_PatientPOB_enc TEXT NULL AFTER M_PatientPOB,
    ADD COLUMN M_PatientIDNumber_enc TEXT NULL AFTER M_PatientIDNumber,
    ADD COLUMN M_PatientNIK_enc TEXT NULL AFTER M_PatientNIK,
    ADD COLUMN M_PatientNIP_enc TEXT NULL AFTER M_PatientNIP;
ALTER TABLE one_lab.m_patientaddress
    ADD COLUMN M_PatientAddressDescription_enc TEXT NULL AFTER M_PatientAddressDescription,
    ADD COLUMN M_PatientAddressDescription_bidx MEDIUMTEXT NULL AFTER M_PatientAddressDescription_enc,
    ADD COLUMN M_PatientAddressEmail_enc TEXT NULL AFTER M_PatientAddressEmail,
    ADD COLUMN M_PatientAddressPhone_enc TEXT NULL AFTER M_PatientAddressPhone;
\""
  • Step 3: Verifikasi kolom terbentuk
ssh devone "mysql -e 'SHOW COLUMNS FROM one_lab.m_patient LIKE \"%_enc\";'"

Expected: 9 baris dengan kolom-kolom _enc.

  • Step 4: Commit SQL file
git add sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql
git commit -m "TASKCODE - SQL: tambah kolom _enc dan _bidx untuk PII pasien"

Task 4: Migration Script — Enkripsi Data Lama (178K rows)

Files:

  • Create: scripts/migrate_encrypt_patient.php

Script ini dijalankan sekali di server via CLI. Proses 500 baris per batch.

  • Step 1: Buat migration script
<?php
/**
 * Batch migration: enkripsi PII m_patient dan m_patientaddress
 * Jalankan via: php scripts/migrate_encrypt_patient.php
 * Aman dijalankan berulang: skip row yang sudah ada _enc-nya
 */

// Load .env
foreach (file(__DIR__ . '/../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $l) {
    [$k, $v] = array_map('trim', explode('=', $l, 2));
    if ($k !== '') $_ENV[$k] = $v;
}

define('BASEPATH', true);
require __DIR__ . '/../application/libraries/Ibl_encryptor.php';
$enc = new Ibl_encryptor();

// DB connection
$cfg = require __DIR__ . '/../application/config/database.php';
$db_cfg = $db['default'];
$dsn = "mysql:host={$db_cfg['hostname']};dbname={$db_cfg['database']};charset=utf8";
$pdo = new PDO($dsn, $db_cfg['username'], $db_cfg['password'], [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

// --- Migrate m_patient ---
echo "=== Migrasi m_patient ===\n";
$offset = 0;
$batch  = 500;
$total  = 0;

while (true) {
    $rows = $pdo->query("SELECT M_PatientID, M_PatientName, M_PatientHP, M_PatientDOB,
        M_PatientEmail, M_PatientPhone, M_PatientPOB, M_PatientIDNumber, M_PatientNIK, M_PatientNIP
        FROM m_patient WHERE M_PatientName_enc IS NULL LIMIT {$batch}")->fetchAll(PDO::FETCH_ASSOC);

    if (empty($rows)) break;

    $stmt = $pdo->prepare("UPDATE m_patient SET
        M_PatientName_enc = ?, M_PatientName_bidx = ?,
        M_PatientHP_enc = ?, M_PatientHP_bidx = ?,
        M_PatientDOB_enc = ?, M_PatientDOB_bidx = ?,
        M_PatientEmail_enc = ?,
        M_PatientPhone_enc = ?,
        M_PatientPOB_enc = ?,
        M_PatientIDNumber_enc = ?,
        M_PatientNIK_enc = ?,
        M_PatientNIP_enc = ?
        WHERE M_PatientID = ?");

    foreach ($rows as $row) {
        $dob = $row['M_PatientDOB'] ? date('d-m-Y', strtotime($row['M_PatientDOB'])) : '';
        $stmt->execute([
            $enc->encrypt($row['M_PatientName']),
            $enc->search_bidx($row['M_PatientName']),
            $enc->encrypt($row['M_PatientHP']),
            $enc->search_bidx($row['M_PatientHP']),
            $enc->encrypt($dob),
            $enc->search_bidx($dob),
            $enc->encrypt($row['M_PatientEmail']),
            $enc->encrypt($row['M_PatientPhone']),
            $enc->encrypt($row['M_PatientPOB']),
            $enc->encrypt($row['M_PatientIDNumber']),
            $enc->encrypt($row['M_PatientNIK']),
            $enc->encrypt($row['M_PatientNIP']),
            $row['M_PatientID'],
        ]);
        $total++;
    }
    echo "  {$total} rows done...\n";
}
echo "m_patient selesai: {$total} rows\n";

// --- Migrate m_patientaddress ---
echo "=== Migrasi m_patientaddress ===\n";
$total = 0;

while (true) {
    $rows = $pdo->query("SELECT M_PatientAddressID, M_PatientAddressDescription,
        M_PatientAddressEmail, M_PatientAddressPhone
        FROM m_patientaddress WHERE M_PatientAddressDescription_enc IS NULL LIMIT {$batch}")->fetchAll(PDO::FETCH_ASSOC);

    if (empty($rows)) break;

    $stmt = $pdo->prepare("UPDATE m_patientaddress SET
        M_PatientAddressDescription_enc = ?, M_PatientAddressDescription_bidx = ?,
        M_PatientAddressEmail_enc = ?,
        M_PatientAddressPhone_enc = ?
        WHERE M_PatientAddressID = ?");

    foreach ($rows as $row) {
        $stmt->execute([
            $enc->encrypt($row['M_PatientAddressDescription']),
            $enc->search_bidx($row['M_PatientAddressDescription']),
            $enc->encrypt($row['M_PatientAddressEmail']),
            $enc->encrypt($row['M_PatientAddressPhone']),
            $row['M_PatientAddressID'],
        ]);
        $total++;
    }
    echo "  {$total} rows done...\n";
}
echo "m_patientaddress selesai: {$total} rows\n";
echo "=== Migrasi selesai ===\n";
  • Step 2: Commit script
git add scripts/migrate_encrypt_patient.php
git commit -m "TASKCODE - migration script enkripsi batch data PII pasien"
  • Step 3: Upload ke server dan jalankan

Jalankan setelah Task 3 (kolom sudah ada) dan Task 2 (library sudah ada di server) selesai diupload.

# Upload dulu via upload script
bash scripts/upload_ibl_committed_files.sh

# Jalankan migration di server
ssh devone "cd /home/one/project/one/one-api-lab && php scripts/migrate_encrypt_patient.php"
  • Step 4: Verifikasi hasil migrasi
ssh devone "mysql -e 'SELECT COUNT(*) total, COUNT(M_PatientName_enc) sudah_enc FROM one_lab.m_patient;'"

Expected: total == sudah_enc (semua baris sudah terenkripsi).


Task 5: Update Patient.phpadd_new() dan edit()

Files:

  • Modify: application/controllers/mockup/fo/ibl_registration/Patient.php

  • Step 1: Load library di __construct

Di __construct() Patient.php, tambahkan setelah parent::__construct():

$this->load->library('ibl_encryptor');
  • Step 2: Update add_new() — enkripsi sebelum insert

Pada add_new(), ganti bagian pembentukan array $ptn dan $add:

// Enkripsi PII sebelum insert
$dob_str = date('d-m-Y', strtotime($prm['M_PatientDOB']));

$ptn = [
    'M_PatientName'            => $patient_name,
    'M_PatientName_enc'        => $this->ibl_encryptor->encrypt($patient_name),
    'M_PatientName_bidx'       => $this->ibl_encryptor->search_bidx($patient_name),
    'M_PatientM_TitleID'       => $prm['M_PatientM_TitleID'],
    'M_PatientPrefix'          => $prm['M_PatientPrefix'],
    'M_PatientSuffix'          => $prm['M_PatientSuffix'],
    'M_PatientM_SexID'         => $prm['M_PatientM_SexID'],
    'M_PatientM_ReligionID'    => $prm['M_PatientM_ReligionID'],
    'M_PatientDOB'             => $prm['M_PatientDOB'],
    'M_PatientDOB_enc'         => $this->ibl_encryptor->encrypt($dob_str),
    'M_PatientDOB_bidx'        => $this->ibl_encryptor->search_bidx($dob_str),
    'M_PatientPOB'             => $prm['M_PatientPOB'],
    'M_PatientPOB_enc'         => $this->ibl_encryptor->encrypt($prm['M_PatientPOB']),
    'M_PatientHP'              => $prm['M_PatientHP'],
    'M_PatientHP_enc'          => $this->ibl_encryptor->encrypt($prm['M_PatientHP']),
    'M_PatientHP_bidx'         => $this->ibl_encryptor->search_bidx($prm['M_PatientHP']),
    'M_PatientPhone'           => $prm['M_PatientPhone'],
    'M_PatientPhone_enc'       => $this->ibl_encryptor->encrypt($prm['M_PatientPhone']),
    'M_PatientEmail'           => $prm['M_PatientEmail'],
    'M_PatientEmail_enc'       => $this->ibl_encryptor->encrypt($prm['M_PatientEmail']),
    'M_PatientM_IdTypeID'      => $M_IdTypeID,
    'M_PatientIDNumber'        => $prm['M_PatientIDNumber'],
    'M_PatientIDNumber_enc'    => $this->ibl_encryptor->encrypt($prm['M_PatientIDNumber']),
    'M_PatientNote'            => $prm['M_PatientNote'],
    'M_PatientUserID'          => $userid,
    'M_PatientCreated'         => date('Y-m-d H:i:s'),
    'M_PatientCreatedUserID'   => $userid
];

Dan untuk $add (address):

$add = [
    'M_PatientAddressM_PatientID'           => $id,
    'M_PatientAddressDescription'           => $address_description,
    'M_PatientAddressDescription_enc'       => $this->ibl_encryptor->encrypt($address_description),
    'M_PatientAddressDescription_bidx'      => $this->ibl_encryptor->search_bidx($address_description),
    'M_PatientAddressUserID'                => $userid,
    'M_PatientAddressRegionalCd'            => $prm['M_PatientAddressRegionalCd'],
    'M_PatientAddressLocation'              => $prm['M_PatientAddressLocation'],
    'M_PatientAddressCity'                  => $prm['M_PatientAddressCity'],
    'M_PatientAddressVillage'               => $prm['M_PatientAddressVillage'],
    'M_PatientAddressDistrict'              => $prm['M_PatientAddressDistrict'],
    'M_PatientAddressState'                 => $prm['M_PatientAddressState'],
    'M_PatientAddressCountry'               => $prm['M_PatientAddressCountry'],
    'M_PatientAddressCountryCode'           => $prm['M_PatientAddressCountryCode'],
    'M_PatientAddressNote'                  => isset($prm['M_PatientAddressNote']) ? $prm['M_PatientAddressNote'] : 'Utama',
    'M_PatientAddressCreated'               => date('Y-m-d H:i:s'),
    'M_PatientAddressCreatedUserID'         => $userid
];
  • Step 3: Update edit() — enkripsi sebelum update

Ganti chain ->set(...) pada edit() untuk patient:

$dob_str = date('d-m-Y', strtotime($prm['M_PatientDOB']));

$this->db_smartone
    ->set('M_PatientName', $patient_name)
    ->set('M_PatientName_enc', $this->ibl_encryptor->encrypt($patient_name))
    ->set('M_PatientName_bidx', $this->ibl_encryptor->search_bidx($patient_name))
    ->set('M_PatientM_TitleID', $prm['M_PatientM_TitleID'])
    ->set('M_PatientPrefix', $prm['M_PatientPrefix'])
    ->set('M_PatientSuffix', $prm['M_PatientSuffix'])
    ->set('M_PatientM_SexID', $prm['M_PatientM_SexID'])
    ->set('M_PatientM_ReligionID', $prm['M_PatientM_ReligionID'])
    ->set('M_PatientDOB', $prm['M_PatientDOB'])
    ->set('M_PatientDOB_enc', $this->ibl_encryptor->encrypt($dob_str))
    ->set('M_PatientDOB_bidx', $this->ibl_encryptor->search_bidx($dob_str))
    ->set('M_PatientPOB', $prm['M_PatientPOB'])
    ->set('M_PatientPOB_enc', $this->ibl_encryptor->encrypt($prm['M_PatientPOB']))
    ->set('M_PatientHP', $prm['M_PatientHP'])
    ->set('M_PatientHP_enc', $this->ibl_encryptor->encrypt($prm['M_PatientHP']))
    ->set('M_PatientHP_bidx', $this->ibl_encryptor->search_bidx($prm['M_PatientHP']))
    ->set('M_PatientPhone', $prm['M_PatientPhone'])
    ->set('M_PatientPhone_enc', $this->ibl_encryptor->encrypt($prm['M_PatientPhone']))
    ->set('M_PatientEmail', $prm['M_PatientEmail'])
    ->set('M_PatientEmail_enc', $this->ibl_encryptor->encrypt($prm['M_PatientEmail']))
    ->set('M_PatientM_IdTypeID', $prm['M_PatientM_IdTypeID'])
    ->set('M_PatientIDNumber', $prm['M_PatientIDNumber'])
    ->set('M_PatientIDNumber_enc', $this->ibl_encryptor->encrypt($prm['M_PatientIDNumber']))
    ->set('M_PatientNote', $prm['M_PatientNote'])
    ->set('M_PatientUserID', $userid)
    ->set('M_PatientLastUpdatedUserID', $userid)
    ->where('M_PatientID', $prm['id'])
    ->update('m_patient');

Dan untuk address di edit():

$this->db_smartone
    ->set('M_PatientAddressRegionalCd', $prm['M_PatientAddressRegionalCd'])
    ->set('M_PatientAddressLocation', $prm['M_PatientAddressLocation'])
    ->set('M_PatientAddressCity', $prm['M_PatientAddressCity'])
    ->set('M_PatientAddressVillage', $prm['M_PatientAddressVillage'])
    ->set('M_PatientAddressDistrict', $prm['M_PatientAddressDistrict'])
    ->set('M_PatientAddressState', $prm['M_PatientAddressState'])
    ->set('M_PatientAddressCountry', $prm['M_PatientAddressCountry'])
    ->set('M_PatientAddressCountryCode', $prm['M_PatientAddressCountryCode'])
    ->set('M_PatientAddressDescription', $address_description)
    ->set('M_PatientAddressDescription_enc', $this->ibl_encryptor->encrypt($address_description))
    ->set('M_PatientAddressDescription_bidx', $this->ibl_encryptor->search_bidx($address_description))
    ->set('M_PatientAddressUserID', $userid)
    ->set('M_PatientAddressLastUpdatedUserID', $userid)
    ->where('M_PatientAddressID', $id_address)
    ->update('m_patientaddress');
  • Step 4: Commit
git add application/controllers/mockup/fo/ibl_registration/Patient.php
git commit -m "TASKCODE - Patient add_new/edit: enkripsi PII sebelum save ke DB"

Task 6: Update Patient.phpsearch() Pakai Blind Index

Files:

  • Modify: application/controllers/mockup/fo/ibl_registration/Patient.php (fungsi search())

Strategi: untuk setiap parameter search yang sebelumnya pakai LIKE, ganti dengan subquery ke kolom _bidx menggunakan trigram token matching. Hasil tetap di-decrypt sebelum dikembalikan ke client.

  • Step 1: Ganti logika search() untuk pakai bidx

Ganti seluruh blok search() (baris 75188) dengan:

public function search()
{
    $prm = $this->sys_input;
    $number_limit  = 10;
    $number_offset = (!isset($prm['current_page']) ? 1 : $prm['current_page'] - 1) * $number_limit;

    // WHERE clauses
    $where_patient  = "WHERE M_PatientIsActive = 'Y'";
    $where_address  = "AND M_PatientAddressIsActive = 'Y'";
    $join_address   = "join m_patientaddress on M_PatientAddressM_PatientID = M_PatientID {$where_address}";
    $where_noreg    = '';
    $where_name     = '';
    $where_hp       = '';
    $where_dob      = '';
    $where_addr     = '';

    if (!empty($prm['noreg'])) {
        $noreg = $this->db_smartone->escape_like_str($prm['noreg']);
        $where_noreg = "AND M_PatientNoReg LIKE '%{$noreg}%'";
    }

    if (!empty($prm['search'])) {
        $e = explode('+', $prm['search']);

        // Search name via trigram bidx
        if (!empty($e[0])) {
            $name_tokens = $this->ibl_encryptor->query_tokens($e[0]);
            if (!empty($name_tokens)) {
                $token_conditions = [];
                foreach ($name_tokens as $tok) {
                    $tok_esc = $this->db_smartone->escape_str($tok);
                    $token_conditions[] = "JSON_CONTAINS(M_PatientName_bidx, '\"$tok_esc\"')";
                }
                $where_name = 'AND (' . implode(' AND ', $token_conditions) . ')';
            }
        }

        // Search HP via trigram bidx
        if (!empty($e[1])) {
            $hp_tokens = $this->ibl_encryptor->query_tokens($e[1]);
            if (!empty($hp_tokens)) {
                $token_conditions = [];
                foreach ($hp_tokens as $tok) {
                    $tok_esc = $this->db_smartone->escape_str($tok);
                    $token_conditions[] = "JSON_CONTAINS(M_PatientHP_bidx, '\"$tok_esc\"')";
                }
                $where_hp = 'AND (' . implode(' AND ', $token_conditions) . ')';
            }
        }

        // Search DOB via trigram bidx
        if (!empty($e[2])) {
            $dob_tokens = $this->ibl_encryptor->query_tokens($e[2]);
            if (!empty($dob_tokens)) {
                $token_conditions = [];
                foreach ($dob_tokens as $tok) {
                    $tok_esc = $this->db_smartone->escape_str($tok);
                    $token_conditions[] = "JSON_CONTAINS(M_PatientDOB_bidx, '\"$tok_esc\"')";
                }
                $where_dob = 'AND (' . implode(' AND ', $token_conditions) . ')';
            }
        }

        // Search address via trigram bidx
        if (!empty($e[3])) {
            $addr_tokens = $this->ibl_encryptor->query_tokens($e[3]);
            if (!empty($addr_tokens)) {
                $token_conditions = [];
                foreach ($addr_tokens as $tok) {
                    $tok_esc = $this->db_smartone->escape_str($tok);
                    $token_conditions[] = "JSON_CONTAINS(M_PatientAddressDescription_bidx, '\"$tok_esc\"')";
                }
                $where_addr = 'AND (' . implode(' AND ', $token_conditions) . ')';
            }
        }
    }

    if (empty($where_addr)) {
        $join_address .= " AND M_PatientAddressNote = 'Utama'";
    }

    $sql = "SELECT 'N' divider, M_PatientID, M_PatientNoReg, M_PatientEmail_enc,
            M_PatientPrefix, M_PatientSuffix,
            concat(M_TitleName,' ',IFNULL(M_PatientPrefix,''),' ',M_PatientName,' ',IFNULL(M_PatientSuffix,'')) M_PatientName_raw,
            M_PatientName_enc,
            M_PatientName M_PatientRealName, M_TitleID, M_TitleName, M_SexID, M_SexName,
            M_PatientHP_enc, M_PatientPOB_enc, M_PatientDOB_enc, M_PatientDOB,
            '' M_PatientAddress,
            M_PatientAddressID,
            M_PatientAddressDescription_enc, M_PatientM_IdTypeID, M_PatientIDNumber_enc,
            M_PatientAddressRegionalCd,
            M_PatientAddressLocation,
            M_PatientAddressCity,
            M_PatientAddressVillage,
            M_PatientAddressDistrict,
            M_PatientAddressState,
            M_PatientAddressCountry,
            M_PatientAddressCountryCode,
            IFNULL(M_PatientNote, '') M_PatientNote, M_PatientPhoto,
            M_PatientPhone_enc hp_phone,
            M_PatientAddressM_KelurahanID M_KelurahanID, 0 M_DistrictID, 0 M_CityID, 0 M_ProvinceID,
            M_PatientM_ReligionID,
            IFNULL(M_ReligionName, '-') M_ReligionName,
            IFNULL(Patient_SignatureUrl, '') image_signature
            FROM m_patient
            join m_title on M_PatientM_TitleID = M_TitleID
            join m_sex on M_PatientM_SexID = M_SexID
            {$join_address}
            left join m_religion on M_PatientM_ReligionID = M_ReligionID
            left join patient_signature on Patient_SignatureM_PatientID = M_PatientID and Patient_SignatureIsActive = 'Y'
            {$where_patient}
            {$where_noreg}
            {$where_name}
            {$where_hp}
            {$where_dob}
            {$where_addr}
            group by M_PatientID
            limit {$number_limit} offset {$number_offset}";

    $query = $this->db_smartone->query($sql);
    if (!$query) {
        $this->sys_error_db("patient search error", $this->db_smartone);
        return;
    }

    $rows = $query->result_array();

    // Dekripsi hasil
    $enc = $this->ibl_encryptor;
    foreach ($rows as &$row) {
        $row['M_PatientName']               = $enc->decrypt($row['M_PatientName_enc']) ?? $row['M_PatientRealName'];
        $row['M_PatientHP']                 = $enc->decrypt($row['M_PatientHP_enc']);
        $row['dob_ina']                     = $enc->decrypt($row['M_PatientDOB_enc']) ?? date('d-m-Y', strtotime($row['M_PatientDOB']));
        $row['M_PatientEmail']              = $enc->decrypt($row['M_PatientEmail_enc']);
        $row['M_PatientIDNumber']           = $enc->decrypt($row['M_PatientIDNumber_enc']);
        $row['M_PatientPOB']                = $enc->decrypt($row['M_PatientPOB_enc']);
        $row['M_PatientAddressDescription'] = $enc->decrypt($row['M_PatientAddressDescription_enc']);
        $row['hp']                          = $enc->decrypt($row['M_PatientPhone_enc']) ?: $row['M_PatientHP'];

        // Bersihkan kolom _enc dari response
        foreach (array_keys($row) as $k) {
            if (substr($k, -4) === '_enc' || substr($k, -5) === '_bidx') unset($row[$k]);
        }
        unset($row['M_PatientRealName'], $row['M_PatientName_raw'], $row['hp_phone']);
    }
    unset($row);

    $this->sys_ok($rows);
}
  • Step 2: Commit
git add application/controllers/mockup/fo/ibl_registration/Patient.php
git commit -m "TASKCODE - Patient search: ganti LIKE ke trigram blind index, decrypt hasil"

Task 7: Upload & Smoke Test di devone

  • Step 1: Upload ke server
bash scripts/upload_ibl_committed_files.sh
  • Step 2: Pastikan .env ada di server
ssh devone "ls -la /home/one/project/one/one-api-lab/.env"

Jika belum ada, buat manual di server (isi dengan key yang sama dengan lokal):

ssh devone "cat > /home/one/project/one/one-api-lab/.env << 'EOF'
IBL_ENCRYPT_KEY=<isi_dengan_key_dari_lokal>
IBL_ENCRYPT_SEARCH_KEY=<isi_dengan_search_key_dari_lokal>
EOF"
  • Step 3: Jalankan migration di server
ssh devone "cd /home/one/project/one/one-api-lab && php scripts/migrate_encrypt_patient.php"
  • Step 4: Smoke test search endpoint

Test dengan nama yang ada di DB (ganti "BUDI" dengan nama pasien yang ada):

curl -s -X POST https://<url-devone>/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 -50

Expected: response {"status":"OK","data":[...]} dengan nama pasien terdekripsi, bukan ciphertext.

  • Step 5: Test add_new + search

Tambah pasien baru via API, kemudian search namanya. Pastikan hasil search menampilkan data yang benar.

  • Step 6: Verifikasi data terenkripsi di DB
ssh devone "mysql -e 'SELECT M_PatientName, M_PatientName_enc FROM one_lab.m_patient LIMIT 3;'"

Expected: M_PatientName masih plaintext (untuk backward compat), M_PatientName_enc berisi base64 ciphertext.



Task 8: SQL Migration — Tambah Kolom _enc untuk Hasil Lab

Files:

  • Modify: sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql (append)

  • Step 1: Append SQL untuk tabel hasil lab

-- t_orderdetail: nilai hasil lab utama
ALTER TABLE one_lab.t_orderdetail
    ADD COLUMN T_OrderDetailResult_enc  TEXT NULL AFTER T_OrderDetailResult,
    ADD COLUMN T_OrderDetailNote_enc    TEXT NULL AFTER T_OrderDetailNote;

-- t_orderheader: diagnosa dokter
ALTER TABLE one_lab.t_orderheader
    ADD COLUMN T_OrderHeaderDiagnose_enc TEXT NULL AFTER T_OrderHeaderDiagnose;

-- so_resultentrydetail: hasil lab standar
ALTER TABLE one_lab.so_resultentrydetail
    ADD COLUMN So_ResultEntryDetailResult_enc TEXT NULL AFTER So_ResultEntryDetailResult;

-- so_resultentrydetail_other: hasil lab nonstandar
ALTER TABLE one_lab.so_resultentrydetail_other
    ADD COLUMN So_ResultEntryDetailOtherResult_enc       TEXT NULL AFTER So_ResultEntryDetailOtherResult,
    ADD COLUMN So_ResultEntryDetailOtherResultBefore_enc TEXT NULL AFTER So_ResultEntryDetailOtherResultBefore;

-- so_resultentry_fisik_umum: JSON pemeriksaan fisik
ALTER TABLE one_lab.so_resultentry_fisik_umum
    ADD COLUMN So_ResultEntryFisikUmumDetails_enc TEXT NULL AFTER So_ResultEntryFisikUmumDetails;

-- so_resultentry_fisik_summary: ringkasan fisik
ALTER TABLE one_lab.so_resultentry_fisik_summary
    ADD COLUMN So_ResultEntryFisikSummaryValue_enc  TEXT NULL AFTER So_ResultEntryFisikSummaryValue,
    ADD COLUMN So_ResultEntryFisikSummaryValue2_enc TEXT NULL AFTER So_ResultEntryFisikSummaryValue2;

-- so_resultentry_other: catatan hasil
ALTER TABLE one_lab.so_resultentry_other
    ADD COLUMN So_ResultEntryOtherNote_enc TEXT NULL AFTER So_ResultEntryOtherNote;

-- so_resultentry_fisioterapi: detail fisioterapi
ALTER TABLE one_lab.so_resultentry_fisioterapi
    ADD COLUMN So_ResultEntdyFisioterapiDetails_enc TEXT NULL AFTER So_ResultEntdyFisioterapiDetails;

-- so_resultentry_smwt: hasil 6MWT (numerik → enkripsi sebagai teks)
ALTER TABLE one_lab.so_resultentry_smwt
    ADD COLUMN So_ResultentrySmwtWeight_enc              TEXT NULL AFTER So_ResultentrySmwtWeight,
    ADD COLUMN So_ResultentrySmwtHeight_enc              TEXT NULL AFTER So_ResultentrySmwtHeight,
    ADD COLUMN So_ResultentrySmwtBMI_enc                 TEXT NULL AFTER So_ResultentrySmwtBMI,
    ADD COLUMN So_ResultentrySmwtPreTensi_enc            TEXT NULL AFTER So_ResultentrySmwtPreTensi,
    ADD COLUMN So_ResultentrySmwtPreSPO2_enc             TEXT NULL AFTER So_ResultentrySmwtPreSPO2,
    ADD COLUMN So_ResultentrySmwtPreNadi_enc             TEXT NULL AFTER So_ResultentrySmwtPreNadi,
    ADD COLUMN So_ResultentrySmwtPostTensi_enc           TEXT NULL AFTER So_ResultentrySmwtPostTensi,
    ADD COLUMN So_ResultentrySmwtPostSPO2_enc            TEXT NULL AFTER So_ResultentrySmwtPostSPO2,
    ADD COLUMN So_ResultentrySmwtPostNadi_enc            TEXT NULL AFTER So_ResultentrySmwtPostNadi,
    ADD COLUMN So_ResultentrySmwtVOMax_enc               TEXT NULL AFTER So_ResultentrySmwtVOMax,
    ADD COLUMN So_ResultentrySmwtKategoriKebugaran_enc   TEXT NULL AFTER So_ResultentrySmwtKategoriKebugaran;

-- so_resultentry_srq29_conclusion: hasil SRQ-29 (kesehatan jiwa)
ALTER TABLE one_lab.so_resultentry_srq29_conclusion
    ADD COLUMN So_ResultentrySrq29ConclusionResult_enc TEXT NULL AFTER So_ResultentrySrq29ConclusionResult;

-- so_resultentrysdsinterpretation: interpretasi SDS
ALTER TABLE one_lab.so_resultentrysdsinterpretation
    ADD COLUMN So_ResultEntrySDSInterpretationDisplay_enc TEXT NULL AFTER So_ResultEntrySDSInterpretationDisplay;

-- member_eligible: data BPJS/asuransi
ALTER TABLE one_lab.member_eligible
    ADD COLUMN Member_EligibleDescription_enc TEXT NULL AFTER Member_EligibleDescription;
  • Step 2: Jalankan di devone
ssh devone "mysql one_lab" < sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql

Verifikasi salah satu:

ssh devone "mysql -e 'SHOW COLUMNS FROM one_lab.t_orderdetail LIKE \"%_enc\";'"

Expected: 2 baris (T_OrderDetailResult_enc, T_OrderDetailNote_enc).

  • Step 3: Commit
git add sql/manual_changes/2026-05-31-pdp-encrypt-m-patient.sql
git commit -m "TASKCODE - SQL: tambah kolom _enc untuk tabel hasil lab"

Task 9: Migration Script — Enkripsi Data Hasil Lab

Files:

  • Create: scripts/migrate_encrypt_results.php

  • Step 1: Buat script

<?php
/**
 * Batch migration: enkripsi data medis hasil lab
 * Jalankan via: php scripts/migrate_encrypt_results.php
 * Aman dijalankan berulang (skip row yang sudah ada _enc)
 */

foreach (file(__DIR__ . '/../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $l) {
    [$k, $v] = array_map('trim', explode('=', $l, 2));
    if ($k !== '') $_ENV[$k] = $v;
}

define('BASEPATH', true);
require __DIR__ . '/../application/libraries/Ibl_encryptor.php';
$enc = new Ibl_encryptor();

$cfg = require __DIR__ . '/../application/config/database.php';
$db  = $cfg['default'];
$pdo = new PDO(
    "mysql:host={$db['hostname']};dbname={$db['database']};charset=utf8",
    $db['username'], $db['password'],
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

function migrate_simple($pdo, $enc, $table, $pk, $fields, $check_field) {
    $total = 0;
    $cols_select = implode(', ', array_merge([$pk], $fields));
    $sets = implode(', ', array_map(fn($f) => "{$f}_enc = ?", $fields));
    while (true) {
        $rows = $pdo->query("SELECT {$cols_select} FROM {$table} WHERE {$check_field}_enc IS NULL LIMIT 500")->fetchAll(PDO::FETCH_ASSOC);
        if (empty($rows)) break;
        $stmt = $pdo->prepare("UPDATE {$table} SET {$sets} WHERE {$pk} = ?");
        foreach ($rows as $row) {
            $params = array_map(fn($f) => $enc->encrypt((string)($row[$f] ?? '')), $fields);
            $params[] = $row[$pk];
            $stmt->execute($params);
            $total++;
        }
        echo "  {$table}: {$total} rows...\n";
    }
    echo "{$table}: selesai {$total} rows\n";
}

// t_orderdetail
migrate_simple($pdo, $enc, 't_orderdetail', 'T_OrderDetailID',
    ['T_OrderDetailResult', 'T_OrderDetailNote'], 'T_OrderDetailResult');

// t_orderheader
migrate_simple($pdo, $enc, 't_orderheader', 'T_OrderHeaderID',
    ['T_OrderHeaderDiagnose'], 'T_OrderHeaderDiagnose');

// so_resultentrydetail
migrate_simple($pdo, $enc, 'so_resultentrydetail', 'So_ResultEntryDetailID',
    ['So_ResultEntryDetailResult'], 'So_ResultEntryDetailResult');

// so_resultentrydetail_other
migrate_simple($pdo, $enc, 'so_resultentrydetail_other', 'So_ResultEntryDetailOtherID',
    ['So_ResultEntryDetailOtherResult', 'So_ResultEntryDetailOtherResultBefore'], 'So_ResultEntryDetailOtherResult');

// so_resultentry_fisik_umum
migrate_simple($pdo, $enc, 'so_resultentry_fisik_umum', 'So_ResultEntryFisikUmumID',
    ['So_ResultEntryFisikUmumDetails'], 'So_ResultEntryFisikUmumDetails');

// so_resultentry_fisik_summary
migrate_simple($pdo, $enc, 'so_resultentry_fisik_summary', 'So_ResultEntryFisikSummaryID',
    ['So_ResultEntryFisikSummaryValue', 'So_ResultEntryFisikSummaryValue2'], 'So_ResultEntryFisikSummaryValue');

// so_resultentry_other
migrate_simple($pdo, $enc, 'so_resultentry_other', 'So_ResultEntryOtherID',
    ['So_ResultEntryOtherNote'], 'So_ResultEntryOtherNote');

// so_resultentry_fisioterapi
migrate_simple($pdo, $enc, 'so_resultentry_fisioterapi', 'So_ResultEntdyFisioterapiID',
    ['So_ResultEntdyFisioterapiDetails'], 'So_ResultEntdyFisioterapiDetails');

// so_resultentry_smwt
migrate_simple($pdo, $enc, 'so_resultentry_smwt', 'So_ResultentrySmwtID', [
    'So_ResultentrySmwtWeight', 'So_ResultentrySmwtHeight', 'So_ResultentrySmwtBMI',
    'So_ResultentrySmwtPreTensi', 'So_ResultentrySmwtPreSPO2', 'So_ResultentrySmwtPreNadi',
    'So_ResultentrySmwtPostTensi', 'So_ResultentrySmwtPostSPO2', 'So_ResultentrySmwtPostNadi',
    'So_ResultentrySmwtVOMax', 'So_ResultentrySmwtKategoriKebugaran'
], 'So_ResultentrySmwtWeight');

// so_resultentry_srq29_conclusion
migrate_simple($pdo, $enc, 'so_resultentry_srq29_conclusion', 'So_ResultentrySrq29ConclusionID',
    ['So_ResultentrySrq29ConclusionResult'], 'So_ResultentrySrq29ConclusionResult');

// so_resultentrysdsinterpretation
migrate_simple($pdo, $enc, 'so_resultentrysdsinterpretation', 'So_ResultEntrySDSInterpretationID',
    ['So_ResultEntrySDSInterpretationDisplay'], 'So_ResultEntrySDSInterpretationDisplay');

// member_eligible
migrate_simple($pdo, $enc, 'member_eligible', 'Member_EligibleID',
    ['Member_EligibleDescription'], 'Member_EligibleDescription');

echo "=== Migrasi hasil lab selesai ===\n";
  • Step 2: Commit
git add scripts/migrate_encrypt_results.php
git commit -m "TASKCODE - migration script enkripsi batch data hasil lab"
  • Step 3: Upload dan jalankan di server
bash scripts/upload_ibl_committed_files.sh
ssh devone "cd /home/one/project/one/one-api-lab && php scripts/migrate_encrypt_results.php"
  • Step 4: Verifikasi
ssh devone "mysql -e 'SELECT COUNT(*), COUNT(T_OrderDetailResult_enc) FROM one_lab.t_orderdetail;'"

Expected: kedua angka sama (semua row terenkripsi).


Catatan Penting

  • Kolom lama tidak dihapus di sprint ini. Penghapusan kolom plaintext dilakukan di sprint terpisah setelah semua consumer API sudah update.
  • Key rotation: Jika key perlu diganti, seluruh data harus di-re-enkripsi via migration script.
  • Masking (tampilkan 081****5678): ditambahkan di sprint berikutnya sebagai layer presentation di atas decrypt.
  • one_lab_log log tables: log_patient (377 rows), log_fo (86 rows), log_resultentry (0 rows) — termasuk dalam Task 8 SQL dan Task 9 migration.
  • Key management: Key WAJIB dibackup ke password manager DAN file lokal terenkripsi sebelum dijalankan ke production. Key hilang = data tidak bisa didekripsi.