Dokumen ini adalah baseline perilaku current (source-traced) untuk fitur Data Masking: setup key kriptografi server, penerbitan dan rotasi Data Access Key user, konfigurasi dynamic sensitive field, proses masking response, dan unmasking filter search — beserta known gaps dan target perbaikan (Flow 4).
Data Masking tidak menggunakan pasangan public key/private key. Implementasi current menggunakan satu symmetric AES-256 key dengan alias tetap aes-key-01, diakses melalui KeyService, dengan algoritma AES/GCM/NoPadding. Key ini dipakai untuk:
Key server ini berbeda dari Data Access Key milik user.
[!WARNING] Jaga kesinambungan key. Ciphertext yang sudah tersimpan atau sudah diberikan ke client hanya dapat didekripsi dengan AES key yang sama. Jangan mengganti/menghapus key tanpa strategi re-enkripsi atau retensi key lama. Jangan mengirimkan AES key ke frontend/client.
Pilih satu mekanisme berikut sebelum aplikasi dijalankan:
| Mode | Setup | Perilaku current |
|---|---|---|
| Direkomendasikan: environment secret | Set environment variable SHIFRAUD_AES_MASTER_KEY ke key AES-256 yang telah di-Base64-encode (hasil decode harus 32 byte). |
LocalKeyService memakai nilai ini untuk alias aes-key-01; key tidak dibuat sebagai file. |
| Local file fallback | Set shifraud.security.key.path ke direktori aman yang persisten dan hanya dapat dibaca account service. Biarkan SHIFRAUD_AES_MASTER_KEY kosong. |
Saat startup, sistem membaca <path>/aes-key-01.key; jika belum ada, sistem otomatis membuat AES-256 key baru dan menyimpannya dalam Base64. |
HSM (profile hsm) |
Aktifkan Spring profile hsm, konfigurasi hsm.library, hsm.slot, hsm.pin, dan opsional hsm.name; lalu provision secret key AES dengan alias aes-key-01 di HSM. |
HSMKeyService mengambil key tersebut melalui PKCS#11. Aplikasi gagal mengenkripsi/dekripsi bila alias tidak tersedia. |
Contoh pembuatan secret AES-256 untuk mode environment (jalankan di mesin/secret-management workflow yang aman, lalu simpan output-nya sebagai secret SHIFRAUD_AES_MASTER_KEY):
openssl rand -base64 32
[!IMPORTANT]
SHIFRAUD_AES_MASTER_KEYadalah secret server-side, bukan Data Access Key milik user.- Mode environment: rotasi dilakukan dengan menyiapkan key baru → re-enkripsi data relevan atau mempertahankan key lama untuk data historis → memperbarui secret → restart aplikasi.
- Mode local file:
rotateKeymembuat backup file sebelum membuat key baru; ciphertext lama tetap memerlukan key lama.DataAccessKeyServicememiliki fallback AES statis bilaKeyServicetidak tersedia/gagal, khusus untuk enkripsi Data Access Key diT_USER. Ini bukan setup yang aman untuk production, dan tidak menyelamatkanEncryptionService— masking/unmasking field tetap mengambilaes-key-01langsung dariKeyService.
aes-key-01 tersedia sebelum backend start.ApplicationReadyEvent, sistem membuat Data Access Key baru untuk user yang belum memilikinya/dummy key, lalu menyimpannya terenkripsi (ENC:<base64>) pada T_USER.key.GET /user/find/{username}; response menyediakan raw token pada atribut key. Frontend/client menyimpannya secara aman dan mengirimkannya sebagai header X-Data-Access-Key pada endpoint masking. Token juga dapat diperbarui lewat event SSE saat rotasi harian.POST /sensitive-fields/add. Lihat kandidat field lewat GET /sensitive-fields/available; gunakan format addtData.<subKey> untuk field dalam map addtData.[PROTECTED] untuk nilai non-String/error). Request dengan key valid menerima plaintext.| Area | Status | Batas verifikasi |
|---|---|---|
| Service flow dan branching | SOURCE-TRACED | Ditelusuri dari TransactionMaskService dan dependensinya. |
| Endpoint dan header | SOURCE-TRACED | Ditelusuri dari penggunaan anotasi header X-Data-Access-Key di controllers. |
| Database behavior | SOURCE-TRACED | Berdasarkan token user dan config entity behavior. |
| Runtime behavior | NOT RUNTIME-VERIFIED | Tidak menjalankan aplikasi atau integration test. |
| Production behavior | NOT PRODUCTION-VERIFIED | Tidak memeriksa cron setting dan keys environment production. |
| Concern | Komponen current | Tanggung jawab |
|---|---|---|
| Masking orchestration | TransactionMaskService |
Menangkap map payload, menguji token, meloop active sensitive config, dan mengganti nilai menjadi cipher atau fallback. |
| Access token lifecycle | DataAccessKeyService |
Menginisialisasi dummy key, memvalidasi input user vs database, menyimpan cache validasi, dan melakukan cron harian rotasi token. |
| Encryption engine | EncryptionService |
Melakukan operasi kriptografi AES/GCM/NoPadding menggunakan active KeyService. Menelan error decrypt pada unmasking filter. |
| Sensitive fields registry | SensitiveFieldConfigService |
Menyimpan field mana saja yang dianggap sensitif. Memvalidasi field baru terhadap domain class yang didukung. |
Token (Data Access Key) setiap user disimpan dan dirotasi di dalam database secara otomatis. Mekanisme validasi memiliki fallback terhadap format key legacy.
Ketika aplikasi backend pertama kali berjalan (terpicu oleh ApplicationReadyEvent), DataAccessKeyService menjalankan initializeUserKeysOnStartup():
T_USER).key user bernilai null, kosong (blank), atau masih menggunakan dummy seed key bawaan ($2a$12$1u8fN3JgS5s9kE1234567890abcdefghijklmnopqrstuvwxyz123), sistem men-generate raw token baru (gabungan 2 UUID murni tanpa -).key user dengan prefix ENC:.GET /user/find/{username} untuk mendapatkan Data Access Key murni (plaintext). Endpoint ini karenanya harus dilindungi secara tepat oleh konfigurasi otorisasi deployment.UserController, setelah User ditemukan, sistem memanggil dataAccessKeyService.getRawKeyForUser(user), yang mendekripsi cipher ENC:... dari database menggunakan EncryptionService (secret key aes-key-01) menjadi plaintext."key".GET /api/security/access-key/stream, event ACCESS_KEY_ROTATED dengan payload newKey) saat rotasi harian berjalan. Endpoint SSE menerima parameter userId opsional; tanpa parameter, user diambil dari authentication.Token divalidasi terhadap user terautentikasi (userId), bukan sekadar tokennya saja.
Format T_USER.key yang didukung: ENC: (current), BCrypt legacy ($2a$), atau plaintext legacy.
[!DANGER] Cache invalidation delay.
validationCachemenggunakanConcurrentHashMapdengan TTL 5 menit (CACHE_TTL_MS), namun current behavior tidak membersihkan cache spesifik user jika role atau token mereka dicabut (misalnya deactivate user) di pertengahan jalan, kecuali ketika global scheduled rotation berjalan. Token yang sudah ter-cache akan terus di-honor hingga 5 menit berlalu.
Proses rotasi token berjalan otomatis di belakang layar menggunakan Spring Scheduler:
shifraud.security.access-key.rotation-cron).DataAccessKeyService.scheduledDailyRotation() menarik semua user dari database. Untuk setiap user, sistem men-generate raw token baru, mengenkripsinya, dan melakukan update.validationCache di-clear secara global, sehingga request berikutnya memaksa validasi ulang ke database.KeyRotationSseService.notifyUserKeyRotated(userId, rawToken), sistem men-trigger event ke client/frontend agar bisa meng-update key tanpa relogin.SensitiveFieldConfigService mengatur field apa saja yang disembunyikan.
| Operasi | Endpoint | Catatan |
|---|---|---|
| Lihat field aktif | GET /sensitive-fields/ |
Membaca konfigurasi dari T_SENSITIVE_FIELD_CONFIG. |
| Lihat kandidat static field | GET /sensitive-fields/available |
Kandidat berasal dari field ExistingCurrTrans yang belum didaftarkan. |
| Tambah field | POST /sensitive-fields/add |
Body: { "fieldName": "...", "createdBy": "..." }; createdBy default system. |
| Hapus field | DELETE /sensitive-fields/delete/{fieldName} |
Menghapus konfigurasi aktif. |
Nama field static harus berasal dari ExistingCurrTrans. Field dinamis hanya diterima dalam bentuk addtData.<subKey>, misalnya addtData.customerName.
[!DANGER] Tightly coupled static validation. Validasi field bergantung pada variabel statis
VALID_FIELDSyang hanya me-load attributes dariExistingCurrTrans.class.getDeclaredFields(). Jika app memiliki entity lain yang digunakan dalam response (misalnyaAlertCaseatauAcquirerAlert) dan entity tersebut memiliki nama field unik yang perlu di-mask, current behavior menolak registrasinya. Hanya field dengan nama sama dengan milikExistingCurrTransatau field dalam objekaddtDatayang bisa ditambahkan.
Jika token invalid atau absent, proses mutasi map data berjalan (replace original value). Controller yang telah terintegrasi meneruskan X-Data-Access-Key dan userId dari authentication ke TransactionMaskService, mencakup: existing-transaction (pencarian & detail), alert-acquirer & alert-issuer (pencarian & detail), dan case (pencarian).
Alur ringkas per request:
Map.isValid = true): data dikembalikan tanpa perubahan (plaintext).[!DANGER] Type mismatch di JSON payload. Jika field yang dikonfigurasi sensitif aslinya berjenis Number atau Boolean (contoh:
amount,isFraud), current behavior memaksakan field tersebut menjadi String"[PROTECTED]". Ini langsung mengubah skema response API (dari integer ke string) dan berpotensi memunculkan error unmarshal/deserialization pada UI/client.
[!INFO] Grouped pagination. Pada endpoint pagination seperti
/case/search,applyMaskingToGroupedPaginationmembongkar property bernamaalertsdi dalam tiap baris (apabila tipenya List) dan mem-mask setiap item list secara terpisah lewat iterasi map manual. Behavior ini spesifik ada di MaskService agar nested object array tidak terlewat dari screening field aktif.
| Skenario | Masking payload | Search filter — current | Search filter — target (Flow 4) | State lain | Next step |
|---|---|---|---|---|---|
Request tanpa X-Data-Access-Key |
Masking berjalan aktif | Filter tetap di-decrypt (tanpa validasi key) | Ditolak/di-skip | Data disamarkan | UI menampilkan cipher. |
| Token valid tapi cache miss | Masking di-skip (plaintext) | Filter di-decrypt / lewat | Sama dengan current | Cache terisi timestamp baru | UI menampilkan plaintext. |
| Token valid dan cache hit | Masking di-skip (plaintext) | Filter di-decrypt / lewat | Sama dengan current | DB tidak disentuh untuk user key | UI menampilkan plaintext. |
| Token kadaluwarsa/diubah via cron | Masking berjalan (key ditolak) | Filter tetap di-decrypt (tanpa validasi key) | Ditolak/di-skip | Data disamarkan | Client harus request key baru (SSE/GET). |
| Client mengirim plaintext di filter | Masking bergantung token | Fallback raw value | Belum ditentukan di Flow 4 | Query DB memakai plaintext tersebut | Lolos logika pencarian normal (current). |
| Enkripsi/dekripsi AES gagal (misal invalid key) | Response diset [PROTECTED] |
Error decrypt ditelan, nilai asli dipakai | Ditolak (hard-reject) | Nilai riil dihapus dari payload response | UI menampilkan [PROTECTED]. |
| Field Integer dikonfigurasi sensitif | Response menjadi String [PROTECTED] |
N/A | N/A | Type JSON berubah | Kemungkinan error di parser frontend. |
| State | Authoritative current | Projection/consumer |
|---|---|---|
| Dynamic fields target | SensitiveFieldConfigRepository (T_SENSITIVE_FIELD_CONFIG) |
Seluruh controller yang memanggil MaskService. |
| Access key user | T_USER.key |
DataAccessKeyService dan token evaluator. |
| Temporary valid key | Memory: validationCache (Map) |
Mempercepat pengecekan sebelum DB hit. |
| Cryptographic secret key | KeyService (alias aes-key-01) |
EncryptionService. |
| Gap current | Dampak |
|---|---|
Validasi static field sangat tight ke ExistingCurrTrans |
Tidak bisa mem-mask field khusus dari entity Alert/Case yang tidak di-share dengan Transaksi (kecuali diselipkan di addtData). |
| Data Access Key cache per user tidak terhapus saat session diterminasi (hanya clear saat cron) | TTL 5 menit menyebabkan zombie validity window bagi token yang direvoke. |
Literal string "[PROTECTED]" pada field non-String |
Mutasi skema JSON mendadak dan berisiko merusak type-safety client frontend. |
Masking iteratif memakai ObjectMapper.convertValue dari Object ke Map pada pagination besar |
Potensi overhead serialization/deserialization ganda pada /search yang me-list ratusan object. |
unmaskSearchFilters tidak memvalidasi key dan menelan error decrypt |
Filter dapat di-decrypt tanpa key valid, dan cipher rusak dieksekusi sebagai teks biasa (diatasi Flow 4). |
| Surface | Endpoint family | Capability |
|---|---|---|
| Sensitive fields management | /sensitive-fields |
Menambahkan (/add), menghapus (/delete/{fieldName}), melihat field aktif (/), dan melihat field ExistingCurrTrans yang available diregistrasi (/available). |
| Consumer (Transaction) | /existing-transaction/* |
Memakai header X-Data-Access-Key untuk unmask filter search dan mask response. |
| Consumer (Alert Acquirer) | /alert-acquirer/* |
Memakai header mask di endpoint search dan findById. |
| Consumer (Alert Issuer) | /alert-issuer/* |
Memakai header mask di endpoint search dan findById. |
| Consumer (Case) | /case/* |
Memakai header mask di endpoint search. |
| User key | GET /user/find/{username} |
Mengembalikan Data Access Key plaintext pada atribut key. |
| Key rotation stream | GET /api/security/access-key/stream |
SSE event ACCESS_KEY_ROTATED dengan newKey; parameter userId opsional. |
| Flow/Area | Primary source |
|---|---|
| Orchestration dan nested addtData | SecurityConfiguration/Security/Masking/TransactionMaskService.java |
| Token generator dan rotation cron | SecurityConfiguration/Security/Key/DataAccessKeyService.java |
| Field restriction logic | SecurityConfiguration/Security/Masking/SensitiveFieldConfigService.java |
| Fallback dan encryption logic | SecurityConfiguration/Security/Encryption/EncryptionService.java |
| Sensitive field controller | SecurityConfiguration/Security/Masking/SensitiveFieldConfigController.java |
| User key fetching | UserManagement/User/UserController.java (endpoint /user/find/{username}) |
| Controller injection dan unmask invoke | AlertManagement/Case/CaseServiceImpl.java; GeneralComponent/Logic/ExistingCurrTrans/ExistingCurrTransController.java; dll. |
unmaskSearchFilters."[PROTECTED]" pada client.