Catatan dari produksi
RustCloudflareWebAssembly

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.

Aplikasi web ZeroPaste di zeropaste.pangaea.id: layar tulis gelap dan minimal dengan area teks besar untuk rahasia, tombol untuk membuat link sekali-pakai, dan penghitung tenang berapa rahasia telah disegel.
Layar tulis ZeroPaste, live di zeropaste.pangaea.id — enkripsi di browser, dapatkan link sekali pakai.

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.

Cara kerja ZeroPaste, end to end: pembuat menulis rahasia; dienkripsi di browser dengan kunci AES-256-GCM; hanya ciphertext yang disimpan di zeropaste.pangaea.id di bawah sebuah link id; link (#id:kunci) dikirim ke pembaca; pembaca mengambil ciphertext berdasarkan id lalu mendekripsinya secara lokal dengan kunci untuk mengungkap rahasia.
Seluruh jalurnya, dianimasikan dari situs live: dienkripsi oleh pembuat, disimpan sebagai ciphertext, didekripsi oleh pembaca — kuncinya hanya bepergian di fragment # 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/aes dan crypto/cipher tidak 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-gcm jalan benar di WASM — tapi kami tetap memanggil crypto.subtle native (teraudit, cepat), jadi tak ada kompromi.
  • workers-rs adalah 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 membaca N, sama-sama menulis N+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.

git push origin mainGitHub Actions: pasang Rust + wasm32, wasm-pack, worker-buildcargo test (logika inti) → make build (frontend wasm)wrangler deploy: kompilasi worker, terapkan migrasi DO, unggah asetLive di zeropaste.pangaea.id
Sekali push ke main → rilis.

Penyiapan satu kali di Cloudflare:

  1. KV namespacewrangler kv namespace create PASTES, lalu tempel id-nya ke wrangler.toml.
  2. Durable Object — tak perlu dibuat manual; [[migrations]] (new_sqlite_classes) diterapkan otomatis di deploy pertama.
  3. Domain kustom — blok [[routes]] dengan custom_domain = true untuk zeropaste.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 terpisahWorkers 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.id untuk 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:

  1. Repository secret — tambahkan CLOUDFLARE_API_TOKEN dan CLOUDFLARE_ACCOUNT_ID di Settings → Secrets and variables → Actions → New repository secret.
  2. 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 krip­to-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

  1. workers-rs — Rust bindings for Cloudflare Workers
  2. Cloudflare — Durable Objects
  3. MDN — SubtleCrypto (Web Crypto)
  4. Cloudflare — Workers KV