- 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>
42 KiB
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 sebelumrequire_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
.envke.gitignore
Cek apakah .gitignore sudah ada:
cat .gitignore 2>/dev/null || echo "tidak ada"
Tambahkan baris:
.env
- Step 3: Modifikasi
index.phpuntuk 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.php — add_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.php — search() Pakai Blind Index
Files:
- Modify:
application/controllers/mockup/fo/ibl_registration/Patient.php(fungsisearch())
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 75–188) 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_loglog 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.