jatimprovcsirt / panda-php
PANDA (Private And Secure Data Access) SDK for PHP
Requires
- php: ^8.1
- infisical/infisical-php-sdk: ^1.0.0
- psr/log: ^2.0 || ^3.0
- symfony/console: ^6.0 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PANDA (Private And Secure Data Access) — AES-256-GCM field-level encryption SDK for PHP applications.
Requirements
- PHP ≥ 8.1 with
ext-openssl - Composer
Installation
Install the package via Composer:
composer require jatimprovcsirt/panda-php
1. PANDA Master Key Initialization (Langkah Pertama yang Wajib)
Sebelum menulis kode integrasi apapun, Anda harus menghasilkan kunci master lokal untuk aplikasi Anda. Ini adalah gerbang keamanan satu kali sebelum PANDA bisa beroperasi.
Langkah 1: Dapatkan PANDA Access Token
Anda harus memiliki PANDA Access Token untuk menginisialisasi kunci master aplikasi Anda.
Daftar aplikasi dan dapatkan token di: https://csirt.jatimprov.go.id/panda
Note
Setiap aplikasi memerlukan token unik. Token digunakan sebagai gerbang keamanan satu kali untuk inisialisasi kunci.
Langkah 2: Jalankan Perintah Init
Jalankan perintah berikut di direktori root proyek Anda. Ganti YOUR_TOKEN dengan PANDA Access Token yang diperoleh dari portal (panjang: 48 atau 64 karakter hex):
# Menggunakan binary CLI dari vendor (setelah composer install)
php vendor/bin/panda init --token YOUR_TOKEN
Setelah menjalankan perintah ini, wizard akan:
- Memvalidasi token Anda
- Meminta Anda memasukkan Key Identifier (KID) untuk kunci pertama
- Menghasilkan kunci 256-bit yang aman secara kriptografis secara lokal
- Menambahkan kunci dalam format base64 ke file
.envAnda (misal:PANDA_KEY_OPD_DUKCAPIL_KEY_1=...)
Note
Untuk pengembangan lokal, Anda dapat menggunakan demo token bawaan:
panda_demo_76d08573d453cdbdb4bf5704d13f8f54d57cd3c47c9b4be2
Langkah 2: Apa yang Terjadi?
Perintah init melakukan hal berikut:
- Memvalidasi format token Anda.
- Menghasilkan kunci 256-bit yang aman secara kriptografis secara lokal (token tidak pernah menerima kunci ini).
- Secara otomatis menambahkan kunci dalam format base64 ke file
.envAnda (misal:PANDA_KEY_OPD_DUKCAPIL_KEY_1=...).
Langkah 3: Restart Server/Terminal (Penting!)
Important
Karena PHP membaca variabel environment dari .env saat startup:
- Jika menjalankan web server (misal
php artisan serve, Apache, Nginx, Docker, php-fpm), Anda HARUS merestart server/container agar variabel environment baru terbaca. - Jika menjalankan perintah di terminal/CMD, Anda HARUS membuka terminal baru atau reload variabel environment.
Menambahkan KID/Kunci Tambahan
Jika aplikasi Anda membutuhkan lebih dari satu kunci (misalnya untuk bidang data yang berbeda atau rotasi secara bertahap), gunakan perintah generate untuk menambahkan kunci tambahan tanpa memerlukan token:
# Kunci kedua: untuk field nomor HP php vendor/bin/panda generate opd-dukcapil-key-2 # Kunci untuk blind index (suffix -idx wajib dipisah dari kunci enkripsi) php vendor/bin/panda generate opd-dukcapil-key-1-idx
Note
Perintah generate hanya dapat digunakan setelah inisialisasi awal dengan init. Tidak perlu token lagi untuk menambahkan kunci baru. Setelah kunci tersimpan di .env, operasi enkripsi/dekripsi sehari-hari tidak pernah menghubungi portal lagi.
Rotasi Kunci (Tanpa Portal)
Untuk merotasi kunci pada KID yang sudah ada, gunakan rotate-key. Perintah ini berjalan sepenuhnya lokal tanpa memerlukan token:
# Rotasi: buat kunci baru dan set sebagai default KID aktif
php vendor/bin/panda rotate-key --new-kid opd-dukcapil-key-2
Menghapus Kunci (Crypto-Shred)
# Hapus kunci secara permanen # PERINGATAN: Data yang dienkripsi dengan kunci ini tidak bisa didekripsi lagi! php vendor/bin/panda shred opd-dukcapil-key-old
Referensi CLI
Mode Non-Interaktif (Flags untuk CI/CD)
Semua perintah CLI dapat digunakan dalam mode non-interaktif dengan menggunakan flag. Ini sangat berguna untuk:
- CI/CD pipelines - Otomatisasi tanpa interaksi pengguna
- Docker containers - Inisialisasi otomatis saat container start
- Infrastructure as Code - Integrasi dengan Terraform, Ansible, dll
- Automated testing - Setup dan teardown otomatis
Perintah Init - Mode Non-Interaktif
# Quick init dengan Local provider (mode tercepat) php vendor/bin/panda init --quick --token <token> --kid default-key # Init tanpa generate kunci pertama (untuk automation lanjutan) php vendor/bin/panda init --quick --token <token> --skip-key-gen # Reinitialize paksa (overwrite konfigurasi yang sudah ada) php vendor/bin/panda init --quick --token <token> --reinit
Flag init yang tersedia:
| Flag | Short | Default | Deskripsi |
|---|---|---|---|
--token <value> |
-t |
- | PANDA access token (atau set PANDA_ACCESS_TOKEN env) |
--kid <value> |
-k |
default-key |
Key Identifier untuk kunci pertama |
--quick |
-q |
false |
Skip wizard, gunakan quick setup dengan local provider |
--reinit |
- | false |
Reinitialize meskipun sudah pernah di-init sebelumnya |
--skip-key-gen |
- | false |
Skip generate kunci pertama |
Perintah Generate - Mode Non-Interaktif
# Generate kunci dengan KID langsung (non-interactive) php vendor/bin/panda generate my-new-key # Generate dengan description php vendor/bin/panda generate nik-key --description "Key for NIK encryption"
Flag generate yang tersedia:
| Flag | Short | Default | Deskripsi |
|---|---|---|---|
--description <text> |
-d |
- | Deskripsi untuk kunci |
Perintah Shred - Mode Non-Interaktif
# Hapus kunci dengan konfirmasi otomatis (hati-hati!)
php vendor/bin/panda shred old-deprecated-key --force
Flag shred yang tersedia:
| Flag | Short | Default | Deskripsi |
|---|---|---|---|
--force |
-f |
false |
Skip konfirmasi prompt (irreversible!) |
Perintah Migrate - Mode Non-Interaktif
# Migrasi batch processing dengan flags php vendor/bin/panda migrate citizens nik default-key \ --batch-size 500 \ --delay 200 \ --pk id \ --dry-run # Migrasi PostgreSQL dengan batch size custom php vendor/bin/panda migrate users email email-key --batch-size 1000 # Migrasi dengan custom primary key php vendor/bin/panda migrate transactions transaction_id txn-key --pk uuid
Flag migrate yang tersedia:
| Flag | Short | Default | Deskripsi |
|---|---|---|---|
--batch-size <n> |
-b |
100 |
Jumlah baris per chunk |
--delay <ms> |
-d |
100 |
Delay antar chunk (ms) |
--pk <col> |
-p |
id |
Nama kolom primary key |
--dry-run |
- | false |
Estimasi scope tanpa mengubah data |
Tabel Lengkap Referensi CLI
| Perintah | Memerlukan Token | Mode Non-Interaktif | Deskripsi |
|---|---|---|---|
php vendor/bin/panda init --token <token> |
✅ Ya | ✅ --quick |
Inisialisasi PANDA (wizard atau quick mode) |
php vendor/bin/panda generate <kid> |
❌ Tidak | ✅ (default non-interactive) | Generate kunci tambahan |
php vendor/bin/panda status |
❌ Tidak | ✅ (default non-interactive) | Tampilkan status konfigurasi |
php vendor/bin/panda config |
❌ Tidak | ✅ (default non-interactive) | Tampilkan detail konfigurasi lengkap |
php vendor/bin/panda deinit |
❌ Tidak | ✅ (default non-interactive) | Hapus konfigurasi PANDA |
php vendor/bin/panda rotate-key --new-kid <kid> |
❌ Tidak | ✅ (default non-interactive) | Rotasi kunci ke KID baru |
php vendor/bin/panda shred <kid> |
❌ Tidak | ✅ --force |
Hapus kunci permanen |
php vendor/bin/panda migrate <table> <col> <kid> |
❌ Tidak | ✅ (default non-interactive) | Migrasi plaintext → encrypted |
Infisical Setup (Production-Ready Alternative)
Untuk deployment produksi, disarankan menggunakan Infisical sebagai KeyProvider alih-alih LocalKeyProvider (env vars).
Setup Infisical Cloud
-
Buat Akun Infisical
- Daftar di https://infisical.com (free tier tersedia)
- Buat project baru
- Buat environment (dev, staging, prod)
-
Buat Machine Identity
- Buka "Machine Identities" di project Anda
- Klik "Create Machine Identity"
- Pilih environment yang diakses
- Copy Client ID dan Client Secret
-
Upload Kunci Enkripsi
- Buka "Secrets" di project
- Pilih environment
- Klik "Add Secret"
- Secret Key:
nik-key(atau KID Anda) - Secret Value:
<base64-encoded-32-byte-key>(generate denganopenssl rand -base64 32)
-
Konfigurasi Aplikasi
# .env PANDA_KEY_PROVIDER_DRIVER=infisical PANDA_INFISICAL_SITE_URL=https://app.infisical.com PANDA_INFISICAL_CLIENT_ID=<client-id> PANDA_INFISICAL_CLIENT_SECRET=<client-secret> PANDA_INFISICAL_PROJECT_ID=<project-id> PANDA_INFISICAL_ENVIRONMENT=dev PANDA_INFISICAL_CACHE_TTL=300
Menggunakan InfisicalKeyProvider dalam Kode
use Panda\Crypto\InfisicalKeyProvider; use Panda\Crypto\FieldCipher; // Baca dari environment variables atau config $keyProvider = new InfisicalKeyProvider( siteUrl: 'https://app.infisical.com', clientId: $_ENV['PANDA_INFISICAL_CLIENT_ID'], clientSecret: $_ENV['PANDA_INFISICAL_CLIENT_SECRET'], projectId: $_ENV['PANDA_INFISICAL_PROJECT_ID'], environment: $_ENV['PANDA_INFISICAL_ENVIRONMENT'], cacheTTL: 300 // optional ); $cipher = new FieldCipher($keyProvider); // Sisa kode tetap sama $envelope = $cipher->encrypt("3201012501990001", "nik-key");
Keuntungan Infisical:
- ✅ Production-ready dengan auto-renewal (tanpa token expiration)
- ✅ UI modern yang mudah digunakan
- ✅ Free tier untuk project kecil
- ✅ Cloud atau self-hosted options
Untuk setup lengkap Infisical, lihat Infisical Setup Guide.
2. Vanilla PHP Integration Guide
Follow these steps to integrate PANDA in a standard PHP project without any frameworks.
Step 1: Load Environment Variables
Make sure your project loads the .env file (using a library like vlucas/phpdotenv or similar):
require_once __DIR__ . '/vendor/autoload.php'; $dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv->load();
Step 2: Initialize KeyProvider and FieldCipher
Instantiate the service classes:
use Panda\Crypto\LocalKeyProvider; use Panda\Crypto\FieldCipher; // 1. KeyProvider reads PANDA_KEY_... from the environment $keyProvider = new LocalKeyProvider(); // 2. FieldCipher handles encryption, decryption, and auditing $cipher = new FieldCipher($keyProvider);
Step 3: Encrypt Data (Before DB Storage)
Encrypt plaintext before writing it to a TEXT database column:
$rawNik = '3201012501990001'; $kid = 'opd-dukcapil-key-1'; $envelope = $cipher->encrypt($rawNik, $kid); $jsonForDatabase = $envelope->toJson(); // Store $jsonForDatabase in your database!
Step 4: Decrypt Data (From DB Query)
Retrieve the JSON envelope from the database and decrypt it:
use Panda\Crypto\Envelope; $row = $db->query("SELECT nik FROM citizens WHERE id = 1")->fetch(); $envelope = Envelope::fromJson($row['nik']); // A. Decrypt with default masking (for safe displaying/logs) $masked = $cipher->decrypt($envelope); echo $masked; // Output: "32************01" // B. Decrypt raw unmasked plaintext (for exports/audited API use) $raw = $cipher->decryptRaw($envelope); echo $raw; // Output: "3201012501990001"
Step 5: Setup Blind Index for Search Lookups
To query encrypted columns, you must compute and store a blind index hash in a separate database column (e.g. nik_bidx VARCHAR(64) indexed):
use Panda\BlindIndex\BlindIndexer; // Note: The blind index key is distinct and uses the "-idx" suffix $blindIndexKid = 'opd-dukcapil-key-1-idx'; $blindIndexKey = $keyProvider->getKey($blindIndexKid); // 1. Compute blind index during INSERT/UPDATE $blindIndexValue = BlindIndexer::generate($rawNik, $blindIndexKey, 'digits'); // Store both $jsonForDatabase in 'nik' AND $blindIndexValue in 'nik_bidx'! // 2. Querying by NIK $searchQuery = '3201012501990001'; $searchHash = BlindIndexer::generate($searchQuery, $blindIndexKey, 'digits'); // Execute SQL lookup: $stmt = $pdo->prepare("SELECT * FROM citizens WHERE nik_bidx = :hash"); $stmt->execute(['hash' => $searchHash]); $results = $stmt->fetchAll();
3. Laravel Integration Guide
Laravel integration provides automation using Eloquent casts, traits, and validation rules.
Step 1: Install Package and Publish Configuration
First, register configuration placeholders and set up local environment variables:
php artisan panda:install
This:
- Copies
config/panda.phpto your application config directory. - Appends
PANDA_DEFAULT_KID=default-keyandPANDA_ACCESS_TOKEN=to your.envfile.
Step 2: Initialize Key
Run the init command to generate your local master key (as detailed in Section 1):
php vendor/bin/panda init --token YOUR_TOKEN
The wizard will prompt you for your Key Identifier (KID). Enter default-key when prompted.
Important
Restart your Laravel development server (e.g. stop and restart php artisan serve or restart Docker containers) so Laravel can read the new environment keys.
Step 3: Generate Database Columns
Create a migration for your encrypted field and its blind index column using the helper command:
php artisan panda:make-field Citizen nik
This creates a migration file in database/migrations/ which alters the target column to TEXT and adds a nik_bidx index column. Apply it:
php artisan migrate
Step 4: Configure the Eloquent Model
Open your Model file (e.g. app/Models/Citizen.php) and add the HasEncryptedFields trait and EncryptedCast cast:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Panda\Integrations\Laravel\EncryptedCast; use Panda\Integrations\Laravel\HasEncryptedFields; class Citizen extends Model { use HasEncryptedFields; protected $casts = [ // Specify the key identifier (KID) to encrypt this column 'nik' => EncryptedCast::class . ':default-key', ]; // Configure the automatic blind indexing for search lookups: protected $encryptedFields = [ 'nik' => [ 'kid' => 'default-key', 'blind_index' => true, // Automatically syncs 'nik_bidx' 'normalization' => 'digits', // Normalization function (e.g. digits, lowercase, trim) ], ]; }
Step 5: Save and Query Data
Now, encryption, decryption, and blind indexing are completely automated!
Saving
$citizen = new Citizen(); $citizen->nik = '3201012501990001'; $citizen->save(); // Automatically saves encrypted envelope in 'nik' and blind index in 'nik_bidx'
Reading
$citizen = Citizen::first(); echo $citizen->nik; // Output: "32************01" (Masked by default) // Get raw decrypted value echo $citizen->getRawEncrypted('nik'); // Output: "3201012501990001"
Querying (Search)
// Use the whereEncrypted query scope $citizens = Citizen::whereEncrypted('nik', '3201012501990001')->get();
Controller Uniqueness Validation
Validate that an encrypted column is unique before saving using the UniqueEncrypted rule:
use Panda\Integrations\Laravel\Rules\UniqueEncrypted; $request->validate([ 'nik' => ['required', new UniqueEncrypted('citizens', 'nik')], ]);
4. CodeIgniter 4 Integration Guide
Follow these steps to integrate PANDA in a CodeIgniter 4 application.
Step 1: Initialize Key & Restart
Initialize your local master key using the CLI command:
php vendor/bin/panda init --token YOUR_TOKEN
The wizard will prompt you for your Key Identifier (KID). Enter opd-dukcapil-key-1 when prompted.
Then, restart your development server (e.g. spark serve or php-fpm container).
Step 2: Access the Service Container
In CodeIgniter 4, PANDA registers as an optional service. Load the FieldCipher instance:
use Panda\Integrations\CodeIgniter\PandaService; $cipher = PandaService::fieldCipher();
Step 3: Insert Encrypted Data with Blind Index
When saving a record, encrypt the plaintext field and generate the blind index manually before saving:
use Panda\BlindIndex\BlindIndexer; use Panda\Crypto\LocalKeyProvider; $rawNik = '3201012501990001'; $kid = 'opd-dukcapil-key-1'; // 1. Encrypt raw text $envelope = $cipher->encrypt($rawNik, $kid); $encryptedJson = $envelope->toJson(); // 2. Generate blind index hash $keyProvider = new LocalKeyProvider(); $blindIndexKey = $keyProvider->getKey("{$kid}-idx"); $blindIndexHash = BlindIndexer::generate($rawNik, $blindIndexKey, 'digits'); // 3. Save to Database $db = \Config\Database::connect(); $db->table('citizens')->insert([ 'nik' => $encryptedJson, 'nik_bidx' => $blindIndexHash, ]);
Step 4: Query and Read Decrypted Records
To retrieve records, search using the blind index hash and decrypt the returned envelope:
use Panda\Crypto\Envelope; $searchQuery = '3201012501990001'; // 1. Hash the search value $keyProvider = new LocalKeyProvider(); $blindIndexKey = $keyProvider->getKey("opd-dukcapil-key-1-idx"); $searchHash = BlindIndexer::generate($searchQuery, $blindIndexKey, 'digits'); // 2. Perform query lookup $db = \Config\Database::connect(); $row = $db->table('citizens') ->where('nik_bidx', $searchHash) ->get() ->getRow(); if ($row) { $envelope = Envelope::fromJson($row->nik); // A. Masked value echo $cipher->decrypt($envelope); // Output: "32************01" // B. Raw unmasked value echo $cipher->decryptRaw($envelope); // Output: "3201012501990001" }
5. Database Refactoring & Zero-Downtime Migration Guide
If you have an existing application with a column containing plaintext data (e.g. nik VARCHAR(16) containing '3201012501990001'), follow this step-by-step workflow to migrate to encrypted and searchable storage without any system downtime.
Step-by-Step Refactor Flow
graph TD
A[Step 1: Upgrade DB Column & Add Index Column] --> B[Step 2: Initialize PANDA Master Key]
B --> C[Step 3: Run Chunked CLI Migration Command]
C --> D[Step 4: Update Application Models/Logic]
D --> E[Step 5: Run Backfill Script for Blind Indexes]
Loading
Step 1: Alter Database Column & Add Index Column
The existing plain-text column must be altered to TEXT (to fit the JSON envelope) and a separate index column (e.g. _bidx) must be added.
- Laravel Migration:
Schema::table('citizens', function (Blueprint $table) { $table->text('nik')->change(); $table->string('nik_bidx', 64)->nullable()->index(); });
- CodeIgniter 4 / Vanilla SQL:
ALTER TABLE citizens MODIFY COLUMN nik TEXT; ALTER TABLE citizens ADD COLUMN nik_bidx VARCHAR(64) NULL; CREATE INDEX idx_citizens_nik_bidx ON citizens(nik_bidx);
Step 2: Initialize PANDA Key
Initialize your local master key (as detailed in Section 1) and make sure your server is restarted.
Step 3: Run Chunked Migration Command
To convert all existing plaintext records in the database to encrypted envelopes, run the migrate CLI command:
# php vendor/bin/panda migrate <table> <column> <kid>
php vendor/bin/panda migrate citizens nik opd-dukcapil-key-1
This reads your DB in chunks (default: 100 rows), encrypts plaintext entries, updates them, and checkpoints its state in a panda_migration_state table so it can safely resume if interrupted.
Step 4: Update Application Code
- Laravel: Update the model with
HasEncryptedFieldsandEncryptedCast(as shown in Section 3). - CodeIgniter / Vanilla: Update CRUD operations to encrypt on write, compute blind indexes, and query using
whereon the_bidxcolumn (as shown in Section 4).
Step 5: Backfill Blind Indexes for Existing Records
Since the CLI migration command only converts plaintext to encrypted envelopes, your database now has encrypted values but empty _bidx columns. You must backfill the blind indexes.
- Laravel script:
// Loop through rows missing the blind index and trigger a save (which automatically computes the bidx) foreach (Citizen::whereNull('nik_bidx')->cursor() as $citizen) { $citizen->save(); }
- CodeIgniter 4 / Vanilla PHP script:
$db = \Config\Database::connect(); $rows = $db->table('citizens')->where('nik_bidx', null)->get()->getResult(); foreach ($rows as $row) { $envelope = Envelope::fromJson($row->nik); // Decrypt to get raw value $rawVal = $cipher->decryptRaw($envelope); // Calculate blind index $blindIndexHash = BlindIndexer::generate($rawVal, $blindIndexKey, 'digits'); // Update $db->table('citizens') ->where('id', $row->id) ->update(['nik_bidx' => $blindIndexHash]); }
Once backfilled, your zero-downtime database migration is complete!
Security
See SECURITY.md for responsible disclosure.
License
MIT