D
P
0
← Semua artikel Read in English

JavaScript, DOM & Animasi Browser

Token dari Keychain Ditolak 401? `security add-generic-password -w` Memotong Nilainya di 128 Karakter Tanpa Pesan

· · 5 menit baca
Token dari Keychain Ditolak 401? `security add-generic-password -w` Memotong Nilainya di 128 Karakter Tanpa Pesan

Opsi -w di security add-generic-password bisa dipakai dengan dua cara. Nilai rahasianya bisa ditaruh langsung sesudah -w, dan dengan begitu ikut menjadi argumen perintah yang terlihat oleh proses lain. Bisa juga -w ditaruh di ujung perintah tanpa nilai, lalu security memintanya lewat prompt. Halaman manualnya menandai cara kedua sebagai yang dianjurkan: "Put at end of command to be prompted (recommended)".

Cara kedua itu yang saya pakai pada 10 September 2026, saat sebuah alat lokal perlu menyimpan token akses sebuah layanan pihak ketiga ke Keychain. Alasannya supaya nilai token tidak muncul di argumen perintah dan tidak terbaca proses lain lewat ps. Token itu berbentuk JWT dan panjangnya 842 karakter. Yang bisa dibaca kembali dari Keychain cuma 128 karakter, dan tidak ada satu pun pesan yang memberi tahu.

Prompt yang meminta nilainya dua kali

Nilainya dialirkan ke prompt itu lewat printf dan pipa. Begini perintah simpan di catatan saya, dengan nama akun dan layanan yang di sana memang sudah ditulis sebagai placeholder akun dan layanan:

printf '%s\n%s\n' "$NILAI" "$NILAI" | security add-generic-password -a akun -s layanan -w

Nilainya muncul dua kali karena prompt-nya meminta konfirmasi. Kalau dikirim sekali saja, perintahnya gagal dengan pesan "passwords don't match". Jadi prompt ini cukup cermat untuk menolak dua isian yang tidak sama. Kecermatan yang sama tidak berlaku untuk panjangnya.

Status 0, baca balik sukses, isinya lain

Perintah simpan itu keluar dengan status 0. Pembacaan baliknya lewat security find-generic-password -w juga berhasil. Dua tanda yang biasanya dipakai untuk menyatakan sebuah penyimpanan beres sama-sama hijau, dan nilai yang kembali tetap bukan nilai yang disimpan.

Catatan saya menyimpan hasil pengiriman nilai dengan empat panjang berbeda:

kirim 100 -> kembali 100  utuh
kirim 128 -> kembali 128  utuh
kirim 129 -> kembali 128  TERPOTONG
kirim 200 -> kembali 128  TERPOTONG

Batasnya persis 128 karakter. Nilai sepanjang 128 masih kembali utuh, sedangkan nilai 129 karakter sudah kembali sebagai 128. Pemotongannya senyap total. Untuk token 842 karakter, artinya 842 dikurangi 128, yaitu 714 karakter hilang, dan yang tersisa kira-kira 15 persen tokennya, karena 128 dibagi 842 sekitar 0,15.

Deskripsi opsi -w di halaman manual tidak menyebut batas panjang apa pun. Catatan saya juga hanya merekam perilakunya, bukan dari mana angka 128 itu datang, jadi sebabnya tidak akan saya tebak di sini.

Yang akhirnya mengeluh adalah server

Gejalanya tidak muncul di tempat kejadian. Token JWT yang terpotong tetap terlihat seperti token. JWT adalah tiga bagian base64url yang dipisah titik, dan potongan dari deretan seperti itu tetap tersusun dari karakter yang sama jenisnya. Dugaan saya, kalau potongannya kebetulan jatuh di tempat yang pas, token yang terpotong bahkan masih bisa punya tiga bagian yang dipisah titik.

Token itu baru ditolak di ujung, oleh server, sebagai 401 Unauthorized. Jawaban seperti itu menunjuk ke permintaan yang dikirim, bukan ke tempat token itu disimpan, jadi seluruh perhatian tertuju ke bentuk header, nama skema, dan kelengkapan Origin. Padahal yang rusak penyimpanannya.

Cetak panjangnya, bukan isinya

Yang membongkar masalahnya satu baris: mencetak panjang nilai yang terbaca dari penyimpanan, lalu membandingkannya dengan panjang yang diharapkan. Angka 128 lawan 842 langsung menutup seluruh dugaan lain. Begitu jelas bahwa yang terbaca dari penyimpanan bukan token yang utuh, bentuk header dan Origin tidak perlu dipersoalkan lagi.

Contoh minimalnya untuk Keychain, bukan kutipan dari skrip saya, dengan placeholder yang sama:

security find-generic-password -a akun -s layanan -w | tr -d '\n' | wc -c

tr -d '\n' memastikan baris baru di ujung keluaran tidak ikut terhitung. Karena JWT hanya berisi karakter ASCII, jumlah byte yang dihitung wc -c sama dengan jumlah karakternya. Angka panjang juga aman ditampilkan di terminal, karena isi tokennya tidak ikut tercetak.

Balik arahnya: Keychain cukup memegang kunci

Dari sini ada dua jalan pintas yang sama-sama salah. Rahasia panjang jangan ditaruh di Keychain lewat prompt, karena di situlah dia terpotong. Memindahkannya ke -w <nilai> juga bukan jalan keluar, karena argumen perintah terlihat oleh proses lain, dan itu alasan prompt dipilih sejak awal.

Bentuk yang saya tulis sebagai jawabannya membalik arah. Keychain memegang kunci acak 32 byte. Dalam heksa, satu byte ditulis dengan dua karakter, jadi kunci itu cuma 32 kali 2, yaitu 64 karakter, separuh dari batas 128 dan aman di bawahnya. Ukuran itu juga pas untuk AES-256, yang memakai kunci 256 bit, atau 32 byte. Rahasianya sendiri dipegang berkas yang dienkripsi dengan AES-256-GCM, bermode 0600, dan ditulis atomik lewat rename.

Dua istilah terakhir layak dijelaskan. Mode 0600 berarti hanya pemilik berkas yang boleh membaca dan menulisnya. Menulis atomik lewat rename berarti isi baru ditulis ke berkas sementara dulu, lalu berkas itu diganti namanya menjadi berkas tujuan, sehingga tidak pernah ada saat ketika berkas tujuan berisi tulisan yang setengah jadi.

Bentuk ini juga lebih kuat daripada pola yang umum dipakai, yaitu menurunkan kunci dari nama mesin dan nama pengguna. Bahan seperti itu bukan rahasia, dan siapa pun yang sudah ada di mesin bisa menebaknya. Kunci acak tidak punya bahan yang bisa ditebak.

Gerbang baca balik

Resep itu punya satu bagian lagi yang wajib ikut: sesudah menulis, baca balik, bandingkan dengan yang disimpan, lalu gagalkan kalau berbeda. Alasannya saya tulis di catatan yang sama. Penyimpanan yang bisa gagal diam-diam harus dibuktikan tiap kali, bukan dipercaya.

Contoh minimal gerbang itu untuk Keychain, dengan placeholder netral, bukan kode alat saya:

balik=$(security find-generic-password -a akun -s layanan -w)
if [ "$balik" != "$NILAI" ]; then
  echo "baca balik beda: ${#balik} lawan ${#NILAI} karakter" >&2
  exit 1
fi

Pesan gagalnya mencetak panjang, bukan isi, dengan alasan yang sama seperti di atas. Seandainya gerbang seperti ini sudah terpasang di belakang perintah simpan tadi, token 842 karakter itu sudah gagal saat disimpan, karena 128 karakter tidak sama dengan 842 karakter, alih-alih baru muncul jauh di hilir sebagai 401.

Yang sudah berubah dan yang belum terbukti

Catatan proyek alat itu menulis penyimpanannya dalam bentuk kini: kunci acak 32 byte di Keychain, isinya di berkas AES-256-GCM bermode 0600, dan sebabnya dirujuk ke catatan tentang batas 128 ini.

Yang belum ada di catatan mana pun adalah buktinya. Tidak ada angka yang memperlihatkan token 842 karakter terbaca utuh dari penyimpanan yang baru, dan baris penyimpanan itu tidak menyebut gerbang baca balik maupun penulisan atomik lewat rename. Jadi yang bisa saya katakan dengan jujur hanya ini: bentuk penyimpanannya sudah berubah, sedangkan dua bagian resepnya belum tercatat terpasang. Menurut aturan saya sendiri di bagian sebelumnya, bentuk baru itu pun belum boleh dipercaya sampai baca baliknya terbukti sama dengan yang disimpan.