Membangun ZeroPaste: Rust + WASM di Cloudflare
Kami membutuhkan cara cepat untuk berbagi satu rahasia — sebuah password, kunci API, token — lewat satu link sekali pakai, tanpa harus memercayai server yang menyimpannya. Hasilnya ZeroPaste: pastebin terenkripsi end-to-endⓘ yang burn-on-read, ditulis penuh dengan Rust → WebAssembly di atas satu Cloudflare Worker.
Artikel ini menelusuri keputusan-keputusan teknis yang membentuknya: kenapa kami pindah dari Go ke Rust, kenapa satu Cloudflare Worker dengan KV dan Durable Object ternyata pas, lalu langkah deploy lengkapnya — Cloudflare dan GitHub — beserta izin yang dibutuhkan.

Enkripsi end-to-end: kuncinya ada di link, bukan di server
Inti produknya satu kalimat: server tidak pernah bisa membaca paste Anda.
Saat Anda membuat paste, browser membuat kunci AES-256 acak dan meng-enkripsi teks dengan
AES-256-GCMⓘ lewat Web Crypto APIⓘ
bawaan browser — sebelum apa pun dikirim. Kunci dekripsi diletakkan di link setelah tanda #:
https://zeropaste.pangaea.id/#[link-id]:[kunci]
Trik-nya ada pada #. Bagian setelah # (URL fragment) tidak pernah dikirim browser ke
server dalam request HTTP. Jadi server hanya menerima link-id (untuk mencari blob) dan
ciphertext buram — tidak pernah teks asli, tidak pernah kuncinya. Ini yang membuatnya
zero-knowledgeⓘ: kalaupun database kami disita atau di-subpoena,
yang ada hanya data acak yang tak bisa kami buka.
Satu konsekuensi penting: karena kunci ada di link, siapa pun yang memegang link utuh bisa membaca paste itu sekali. Ciphertext yang tersimpan tak berguna tanpa potongan kuncinya.
# link, tak pernah ke server.Keputusan: dari Go ke Rust
Rencana awal pakai Go (TinyGo → WASM). Kami berpindah ke Rust setelah menimbang tiga hal — dan ketiganya condong ke Rust untuk produk yang berpusat pada kripto seperti ini.
crypto/aesdancrypto/ciphertidak lolos test suite TinyGo sendiri — mempertaruhkan AES buatan sendiri di implementasi yang belum solid bukan pilihan bijak untuk alat keamanan.- SDK Worker untuk Go adalah proyek komunitas pihak ketiga.
- Durable Object harus ditulis dalam JavaScript — class-nya tidak bisa Go.
- Crate
aes-gcmjalan benar di WASM — tapi kami tetap memanggilcrypto.subtlenative (teraudit, cepat), jadi tak ada kompromi. workers-rsadalah SDK first-party Cloudflare.- Durable Object bisa ditulis dalam Rust (
#[durable_object]) — tak ada JavaScript yang masuk.
Poin ketiga yang menentukan: kami butuh Durable Object untuk satu hal (lihat bagian berikutnya), dan
di Go itu memaksa sepotong JavaScript masuk ke proyek yang seharusnya "Rust saja". Rust membuat
seluruh tumpukan satu bahasa — Rust di backend (worker), Rust di frontend (UI + pemanggil
crypto.subtle), keduanya dikompilasi ke WebAssemblyⓘ.
Kenapa Worker + KV + satu Durable Object
Penyimpanannya tampak sepele: simpan blob, kembalikan sekali, hapus. Tapi ada satu kebutuhan yang memaksa pilihan arsitektur — penghitung paste yang akurat ("N secrets sealed" di halaman).
Workers KVⓘ kami pakai untuk ciphertext: ia adalah key-value store global, cocok untuk menyimpan blob buram lalu mengembalikannya. Tapi KV buruk untuk penghitung:
- KV tidak punya increment atomik. Satu-satunya cara adalah baca-ubah-tulis (
baca N,tulis N+1). Dua paste bersamaan sama-sama membacaN, sama-sama menulisN+1— satu increment hilang. - KV bersifat eventually consistent dan dipandu ~1 tulis/detik per key panas. Satu key penghitung global adalah kasus terburuk untuk KV.
Durable Objectⓘ (DO) menyelesaikannya. Satu DO instance
diserialkan oleh runtime-nya — hanya satu request berjalan pada satu waktu di dalamnya — sehingga
tokens += 1 lalu storage.put bersifat atomik tanpa lock, dan durable (selamat dari
restart, tidak seperti cache di memori). Itu penghitung pasti yang KV tak bisa beri.
Penting: DO berbasis SQLite tersedia di paket gratis Cloudflare — jadi penghitung atomik dan rate limiter tidak menambah biaya.
Burn-on-read, click-to-reveal, dan jebakan prefetch
Sebuah paste lenyap pada yang lebih dulu terjadi: dibaca pertama kali (burn-on-read), atau kedaluwarsa pada backstop TTL 7 hari jika tak pernah dibuka. KV menghapus key-nya di sisi server — tak perlu cron pembersih, dan tak ada paste yatim yang menumpuk.
Tapi "dibaca pertama kali" punya jebakan dunia nyata. Aplikasi pesan (Slack, WhatsApp, iMessage) dan pemindai email mem-prefetch URL untuk membuat pratinjau. Kalau halaman mengambil paste otomatis saat dimuat, bot pratinjau akan membakar paste itu sebelum manusia sempat membacanya.
Perbaikannya: click-to-reveal. Membuka link #id:kunci menampilkan gerbang "Reveal & burn";
request GET /api/:id (yang membakar paste) baru jalan saat pengguna mengklik. Bot prefetch yang
tak mengeklik tak akan membakarnya. Kami verifikasi di produksi: memuat halaman menghasilkan 0
panggilan /api; klik menghasilkan tepat satu.
Konfigurasi: semua lewat env
Setiap perilaku yang mungkin ingin Anda ubah ada di wrangler.toml [vars] — ubah, lalu deploy
ulang:
[vars]
PASTE_TTL = "604800" # backstop burn-on-read, detik (7 hari)
COUNT_CACHE_TTL = "60" # cache-baca penghitung per-isolate, detik
RL_BURST = "10" # kapasitas token bucket rate-limit per IP
RL_REFILL_SEC = "6" # detik untuk meregenerasi 1 token (~10/menit)
MIN_CHARS = "3" # panjang minimum (FE + validasi server)
CANARY_SINCE = "30 June 2026" # tanggal warrant canary
CONTACT_EMAIL = "[email protected]"
Endpoint GET /config mengembalikan JSON {min_chars, canary, contact}; frontend menyuntikkan
tanggal canary dan email kontak dari situ, dan security.txt di-generate worker dari CONTACT_EMAIL.
Satu sumber kebenaran.
Men-deploy-nya: Cloudflare + GitHub Actions
Build-nya tidak sepele: ada dua artifact WASM — worker (lewat worker-build, dijalankan otomatis
oleh wrangler deploy) dan frontend (lewat wasm-pack). make deploy merantai keduanya. Kami pilih
GitHub Actions + token (bukan integrasi Git bawaan Cloudflare) karena Actions memberi kendali
penuh atas toolchain Rust+WASM.
Penyiapan satu kali di Cloudflare:
- KV namespace —
wrangler kv namespace create PASTES, lalu tempel id-nya kewrangler.toml. - Durable Object — tak perlu dibuat manual;
[[migrations]](new_sqlite_classes) diterapkan otomatis di deploy pertama. - Domain kustom — blok
[[routes]]dengancustom_domain = trueuntukzeropaste.pangaea.id; Cloudflare mengurus DNS + sertifikat TLS.
Izin token API yang dibutuhkan. Ini alat Workers, bukan Pages — salah produk, deploy-nya
gagal. Cara tercepat: template token API "Edit Cloudflare Workers" bawaan Cloudflare (My Profile
→ API Tokens → Create Token). Kalau Anda membuat token kustom, ia butuh semua ini — kurang satu
saja, wrangler deploy gagal di tengah jalan:
Account · Workers Scripts ............ Edit deploy worker + class Durable Object-nya
Account · Workers KV Storage ......... Edit baca/tulis namespace PASTES
Account · Account Settings ........... Read wrangler menentukan akun mana yang dipakai
Zone · Workers Routes ............. Edit mengikat worker ke zeropaste.pangaea.id
Zone · DNS ........................ Edit membuat record domain kustom
Zone · SSL and Certificates ....... Edit menerbitkan sertifikat untuk domain kustom
User · User Details ............... Read verifikasi token / wrangler whoami
Account Resources: Include · <akun yang memiliki zeropaste>
Zone Resources: Include · pangaea.id
Dua hal yang sering terlewat: Durable Object tak butuh izin terpisah — Workers Scripts · Edit
sudah mencakup deploy class-nya dan penerapan migrasi; dan tiga baris Zone (Routes, DNS, SSL) ada
hanya untuk domain kustom — lepas semuanya kalau Anda rilis ke subdomain gratis *.workers.dev.
Do
- Mulai dari template "Edit Cloudflare Workers", lalu tambahkan DNS · Edit dan SSL and
Certificates · Edit pada zona
pangaea.iduntuk domain kustom. - Cakup Account Resources dan Zone Resources ke satu akun / satu zona yang memilikinya.
Don't
- Memakai token Cloudflare Pages — itu tidak bisa men-deploy sebuah Worker (beda produk, beda cakupan).
- Mengandalkan Global API Key — cakupannya seluruh akun; token sempit membatasi dampak bila bocor.
Penyiapan satu kali di GitHub:
- Repository secret — tambahkan
CLOUDFLARE_API_TOKENdanCLOUDFLARE_ACCOUNT_IDdi Settings → Secrets and variables → Actions → New repository secret. - Izinkan Actions membuka PR canary — Settings → Actions → General → Workflow permissions → centang "Allow GitHub Actions to create and approve pull requests." Tanpa ini, workflow warrant-canary mingguan punya izin untuk commit tapi tak bisa membuka PR-nya, dan job-nya gagal.
Setelah itu, push ke main akan men-deploy sendiri.
Intinya
ZeroPaste kecil, tapi setiap potongannya adalah satu keputusan: Rust agar kripto-nya andal dan Durable Object-nya tetap satu bahasa; Worker + KV untuk ciphertext buram; satu Durable Object untuk penghitung yang KV tak bisa buat atomik; click-to-reveal agar bot pratinjau tak membakar rahasia Anda; dan GitHub Actions agar sekali push merilis seluruhnya. Yang membuatnya bisa dipercaya bukan janji — melainkan fakta yang bisa Anda saksikan di tab Network: kunci tak pernah keluar dari browser Anda.
Kenapa tidak pakai integrasi Git bawaan Cloudflare, bukan GitHub Actions?
Build-nya butuh dua build Rust→WASM terpisah (worker via worker-build, frontend via wasm-pack).
GitHub Actions memberi kendali penuh untuk memasang toolchain itu; runner build bawaan Cloudflare
bisa, tapi lebih rumit untuk toolchain Rust+WASM. Maka Actions lebih pas di sini.
Kalau kunci ada di URL, bukankah itu bocor di history browser?
Fragment # tersimpan di history browser Anda — tapi tak pernah dikirim ke server, tak masuk log
server, dan tak muncul di header Referer lintas-situs. Risikonya lokal (perangkat Anda), bukan di
sisi host. Dan paste-nya burn-on-read: setelah dibaca, link-nya tak berguna lagi.
Apakah penghitung benar-benar atomik di paket gratis?
Ya. Durable Object berbasis SQLite tersedia di paket gratis Cloudflare, dan runtime DO menserialkan request ke satu instance — jadi increment-nya atomik tanpa lock. Itu sebabnya kami pakai DO untuk penghitung, bukan KV.
Sources