- 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>
88 lines
3.0 KiB
PHP
88 lines
3.0 KiB
PHP
<?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()
|
|
{
|
|
$passphrase = $_ENV['IBL_ENCRYPT_KEY'] ?? '';
|
|
$passphrase_s = $_ENV['IBL_ENCRYPT_SEARCH_KEY'] ?? '';
|
|
|
|
if ($passphrase === '' || $passphrase_s === '') {
|
|
log_message('error', 'Ibl_encryptor: IBL_ENCRYPT_KEY atau IBL_ENCRYPT_SEARCH_KEY kosong di .env');
|
|
}
|
|
|
|
// Derive 32-byte key dari passphrase via SHA-256
|
|
$this->key = hash('sha256', $passphrase, true);
|
|
$this->search_key = hash('sha256', $passphrase_s, true);
|
|
}
|
|
|
|
// Enkripsi plaintext → base64(iv[12] + tag[16] + ciphertext)
|
|
public function encrypt($plaintext)
|
|
{
|
|
if ($plaintext === null || $plaintext === '') return null;
|
|
$iv = random_bytes(12);
|
|
$tag = '';
|
|
$ct = openssl_encrypt((string)$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 trigram token untuk kolom _bidx (partial search)
|
|
public function search_bidx($value)
|
|
{
|
|
if ($value === null || $value === '') return null;
|
|
$norm = mb_strtolower(trim((string)$value), 'UTF-8');
|
|
$len = mb_strlen($norm, 'UTF-8');
|
|
$tokens = [];
|
|
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 array trigram token dari query string (untuk WHERE JSON_CONTAINS)
|
|
public function query_tokens($value)
|
|
{
|
|
if ($value === null || $value === '') return [];
|
|
$norm = mb_strtolower(trim((string)$value), 'UTF-8');
|
|
$len = mb_strlen($norm, 'UTF-8');
|
|
$tokens = [];
|
|
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);
|
|
}
|
|
}
|