Teobiefy
daravel_teobiefy berbicara dengan protokol payload terenkripsi teobiefy
(v1) seperti yang diimplementasikan sebuah API Laravel, sehingga client web
dan mobile dari API tersebut bisa berkomunikasi dengan backend Daravel
tanpa perubahan satu baris pun, dan backend Laravel serta Daravel bisa
melayani client yang sama selama migrasi.
Protokolnya, seperti yang diimplementasikan aplikasi Laravel:
- Handshake. Client mengirim public key X25519-nya ke
POST /api/v1/session-key; server menjawab dengan public key-nya sendiri dan sebuah session id. Keduanya menurunkan sepasang kunci sesi (crypto_kxdari libsodium): server mendekripsi denganrxapa yang dienkripsi client dengantx-nya, dan sebaliknya. - Request. Body
{"data_enc": ..., "nonce": ...}(ChaCha20-Poly1305 IETF, base64) dengan headerX-Session-Iddidekripsi, lalu objek JSON-nya digabungkan ke input request. - Response. Field
datadari jawaban JSON di-encode, dibungkus frame zstd, disegel dengan kuncitxsesi, lalu diganti dengandata_encdannonce.
dart pub add daravel_teobiefy
// bootstrap/app.dart
import 'package:daravel_teobiefy/daravel_teobiefy.dart';
Application createApp() => Application.configure(basePath: basePath)
.withTeobiefy()
// ...
.create();
// routes/api.dart
router.post('/v1/session-key', Teobiefy.handshake)
.middleware(const ['throttle:30,1'])
.name('session-key.store');
router.group(middleware: const ['jwt', 'teobiefy'], (r) {
r.get('/profile', ProfileController.show);
r.post('/evaluation-class-students/{id}', EvaluationController.update);
});
LIBSODIUM_SESSION_TTL=1800 # seconds a session lives after its last use
LIBSODIUM_PAYLOAD_V2_ENABLED=false # true: seal the zstd bytes, enc_v: 2
LIBSODIUM_LEGACY_KEY_ENABLED=false # the old static key; keep it false
LIBSODIUM_LEGACY_KEY=
LIBSODIUM_STRICT_PAYLOAD=false # true: only the decrypted payload reaches the handler
LIBSODIUM_STRICT_PAYLOAD=true membuang key polos yang dikirim di samping
data_enc (aplikasi Laravel menggabungkannya, payload yang menang) dan
menjawab 406 untuk body polos dari client yang memegang sesi. Aktifkan
setelah semua client mengenkripsi semuanya; saat mati, perilakunya sama
dengan Laravel.
Nama-nama ini sama dengan yang dibaca aplikasi Laravel, sehingga satu .env
melayani keduanya.
Sesi#
Teobiefy.handshake memvalidasi client_public_key (base64 dari 32 byte,
selain itu 422), menurunkan kuncinya, lalu menyimpannya di cache dengan
session id 64-hex acak selama LIBSODIUM_SESSION_TTL detik setelah setiap
pemakaian. Jawabannya berupa envelope obiefy/api-response, sama seperti
Laravel:
{
"status": 200,
"message": "Sesi enkripsi berhasil dibuat.",
"data": {"session_id": "…64 hex…", "server_public_key": "…", "expires_in": 1800},
"data_count": 3
}
Session id bukan kredensial: session id hanya menunjuk pasangan kunci dan tidak memberi hak apa pun. Autentikasi tetap dipegang guard di route. Handshake memang terbuka (endpoint publik pun mengenkripsi), karena itu ada rate limit.
Sesi disimpan di store cache default, atau di store process bila store
default menyimpan salinan per isolate: dengan beberapa worker, handshake
yang dijawab satu worker harus berlaku juga di worker lain. Pemeriksaan
replay menyimpan satu entri kecil per request terenkripsi di sana selama
TTL sesi, jadi ukurlah store untuk sesi ditambah request selama satu TTL:
store in-memory menampung 10.000 entri dan membuang yang paling lama tidak
dipakai di luar itu, yaitu sesi yang menganggur lebih lama dari sebanyak
itu request; Redis tidak punya batas seperti itu.
Middleware#
Pasang teobiefy di route yang payload-nya dienkripsi, sesuai daftar di
aplikasi Laravel.
- Request dengan
data_encdidekripsi dengan kuncirxsesi, lalu objek JSON-nya digabungkan ke input (request.merge);data_encdannoncedihapus. Body yang gagal didekripsi dijawab406 Invalid payload, begitu pula body yang dikirim ulang: setiap nonce diingat selama sesi hidup (protokolnya tidak punya timestamp, jadi inilah pemeriksaan replay; aplikasi Laravel tidak memilikinya). - Response JSON yang punya key
datadienkripsi dengan kuncitxsesi. Key lain (status,message,data_count) tetap terbuka. Response tanpadata, dan apa pun yang bukan JSON, diteruskan apa adanya. - Tanpa sesi dan tanpa legacy key, jawabannya
409dengantype: SESSION_KEY_REQUIRED, tidak pernah data polos; client melakukan handshake ulang lalu mengulang request. Body polos dari client seperti itu ditolak sebelum handler berjalan, dan session id yang tidak dikenal store (kedaluwarsa, server lain) dijawab409meski legacy key aktif.
LIBSODIUM_PAYLOAD_V2_ENABLED=true menyegel byte zstd secara langsung dan
menambahkan enc_v: 2; client mengenalinya dari magic number zstd setelah
dekripsi. Aktifkan hanya setelah semua client mendukungnya, sama seperti di
Laravel.
Response berupa frame zstd dengan blok tersimpan (stored): semua decoder
zstd bisa membacanya (sudah dicek dengan zstd referensi), dan tidak butuh
library native, tetapi ukurannya tidak lebih kecil dari JSON-nya.
Legacy key#
LIBSODIUM_LEGACY_KEY_ENABLED=true menerima kunci statis lama untuk client
tanpa sesi. Kunci itu pernah terkirim ke browser dan harus dianggap publik:
ia hanya jembatan untuk frontend lama yang masih ter-cache selama rollout,
bukan perlindungan. Saat boot muncul warning selama kunci ini aktif, dan
boot gagal bila nilainya bukan kunci base64 32 byte.
Envelope OpenSSL (teobiefy.aes)#
Sebagian API Laravel tidak memakai libsodium, melainkan override
obiefy/api-response yang mengenkripsi data dengan OpenSSL dan kunci statis
(App\Helpers\OpenSslHelper):
$key = base64_decode(config('openssl.openssl_key')); // 32 bytes
$iv = random_bytes(16);
$data = json_encode($json['data']);
$json['data_enc'] = base64_encode(openssl_encrypt($data, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv));
$json['iv'] = base64_encode($iv);
unset($json['data']);
withTeobiefyAesCbc() menambahkan envelope itu ke aplikasi Daravel:
AES-256-CBC dengan padding PKCS#7, kunci dari OPENSSL_KEY (base64 dari 32
byte, dengan atau tanpa base64:; boot gagal bila nilainya bukan kunci),
dan IV 16 byte acak per pesan. Pasang teobiefy.aes di route yang
dienkripsi aplikasi Laravel (aplikasi itu memutuskannya dari nama route,
request()->routeIs('admin.user.login')), dan DecryptInterceptor di
client membuka jawabannya seperti sebelumnya:
Application.configure(basePath: basePath).withTeobiefyAesCbc() // OPENSSL_KEY
router.post('/admin/v1/user/login', AuthController.login)
.middleware(const ['teobiefy.aes']);
{"status": 200, "message": "ok", "data_count": 1, "data_enc": "…base64…", "iv": "…base64…"}
- Tanpa parameter,
datadari response objek JSON diganti dengandata_encdaniv; key lain (status,message,data_count,errors) tetap terbuka, sebagaimanaOpenSslHelpermembiarkannya. Response JSON tanpadata, dan apa pun yang bukan JSON, diteruskan apa adanya; status code dan header dipertahankan. teobiefy.aes:bodymengenkripsi seluruh body JSON, apa pun isinya, menjadi{"data_enc": …, "iv": …}.teobiefy.aes:decryptjuga membuka body request yang dikirim sebagai{"data_enc": …, "iv": …}dan menggabungkan objek JSON-nya ke input (key polos di sampingnya tetap ada, payload yang menang;data_encdanivdihapus). Body yang tidak bisa dibuka dijawab406 Invalid payload. Body polos tetap lolos: mode ini tidak bisa membedakan client tanpa kunci dari client yang memilih tidak mengenkripsi. Parameter bisa digabung:teobiefy.aes:body,decrypt.teobiefy.aes:phpmenulis teks polos dan envelope sepertijson_encodePHP (slash di dalamdata_encdanivdi-escape, karakter non-ASCII menjadi escape), untuk client yang membandingkan atau menandatangani byte yang dikirim API Laravel.TeobiefyAesCbc.current.encrypt(value)dan.decrypt(envelope)melakukan hal yang sama secara manual (misalnya payload yang dibangun di job);TeobiefyAesCbc.fromKey(key)memberi cipher di luar container.
Implementasinya dicek terhadap NIST SP 800-38A (F.2.5, CBC-AES256) dan
terhadap openssl enc, library yang dipanggil sisi Laravel.
Mode ini tidak punya MAC. Kuncinya statis dan ikut terkirim bersama client, jadi tidak menyembunyikan apa pun dari siapa pun yang memegang client, dan tidak ada yang mencegah ciphertext yang diubah sampai ke handler: blok yang diubah terdekripsi menjadi sampah atau, bila penyerang beruntung, menjadi sesuatu yang bisa di-parse. Mode ini ada agar client dari API yang di-port tetap berjalan; ini bukan perlindungan. Pekerjaan baru, dan client tersebut begitu bisa diperbarui, sebaiknya memakai protokol X25519 + ChaCha20-Poly1305 di atas.
Bersama Laravel#
Test-nya mengecek pertukaran kunci dan cipher terhadap vector yang dibuat
sodium PHP (libsodium): kunci sesi crypto_kx dan ChaCha20-Poly1305 IETF ke
dua arah. Perbedaannya:
- Body yang gagal didekripsi dijawab
406. Middleware Laravel memanggilapi()->error(406, 'Invalid payload'), yang argumennya adalah message dan data, sehingga menjawab500denganmessage: 406. - Kunci sesi disimpan dengan key yang dipakai Laravel
(
libsodium:session-key:<id>, array berisirxdantxbase64). Di satu cache Redis denganREDIS_SERIALIZER=php(Redis) dan prefix cache yang sama, kedua aplikasi bisa membaca sesi satu sama lain (belum dicek end to end di sini). Storedatabasedi core membungkus nilai dalam JSON, sehingga lewat store itu tidak bisa: di belakang proxy yang mengirim client ke keduanya, client akan menerima409lalu handshake ulang. - Response mana yang dienkripsi ditentukan oleh letak middleware, bukan oleh daftar nama route dan keyword relasi sensitif di Laravel.
Protokol v2 di blueprint (kunci diturunkan dari bearer token, tanda tangan request) tidak diimplementasikan: kunci yang diturunkan dari token yang ikut dikirim di request yang sama tidak melindungi apa pun yang belum dilindungi koneksi TLS.
Profil payload#
teobiefy-laravel-api-response 0.3 mengganti session dengan profil per route.
withTeobiefyProfiles() menambahkan profil dan middleware
teobiefy.profile:
.withTeobiefyProfiles(
keys: {'v1': env.string('TEOBIEFY_ENCRYPTION_KEY_V1')},
responseProfiles: {'api/v1/exams/*': 'encrypted'},
requestProfiles: {'api/v1/exams/*': 'encrypted'},
)
Pola dicocokkan dengan nama atau path route, seperti route_profiles di
config/teobiefy.php. 'teobiefy.profile:encrypted,plain' pada route
menetapkan profil response dan request, seperti attribute ResponseProfile
dan RequestProfile. Profilnya adalah plain, compressed, encrypted,
compressed_encrypted, signed, dan compressed_signed.
TEOBIEFY_ENCRYPTION_DRIVER bernilai xchacha20-poly1305,
chacha20-poly1305-ietf, atau aes-256-gcm; TEOBIEFY_ENCRYPTION_KEY,
TEOBIEFY_ENCRYPTION_ACTIVE_KID, TEOBIEFY_SIGNING_KEY,
TEOBIEFY_SIGNING_ACTIVE_KID, dan TEOBIEFY_REPLAY_PROTECTION tetap memakai
nama yang sama.
Response berupa frame Zstandard dengan raw block: semua decoder bisa membacanya,
tetapi ukurannya tidak mengecil. Request boleh memakai frame apa pun yang ditulis
ext-zstd atau library mobile; dictionary ditolak. Decoder-nya murni Dart,
sehingga server tetap satu binary.