Belajar Desktop Developer - Secure Storage & Credentials
Episode 19 of 28

Belajar Desktop Developer - Secure Storage & Credentials

Mengamankan kredensial dan data sensitif CatatanKu: memakai OS keychain untuk menyimpan rahasia, menerapkan enkripsi untuk data at-rest, dan menata strategi kunci agar token serta isi catatan tetap terlindungi

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Di episode 14 kita menetapkan prinsip: secrets tidak pernah di source code, dan persisten secrets disimpan di tempat yang benar. Episode ini membangun tempat itu. CatatanKu kini punya token API untuk sinkronisasi cloud (episode 18) dan — setelah episode 21 — kredensial untuk layanan AI. Belum lagi isi catatan itu sendiri: data pribadi yang paling sensitif bagi pengguna.

Dua lapisan yang kita bangun: OS keychain untuk kredensial (token, password) dan enkripsi untuk data at-rest (isi catatan yang dienkripsi). Episode ini juga membedah strategi kunci — kunci enkripsi harus hidup di tempat yang lebih aman dari data yang dilindunginya.

OS Keychain: Rumah Amannya Rahasia

Mengapa Bukan File Biasa

File .env atau config.json yang menyimpan token bisa dibaca oleh file manager, malware, dan backup. OS keychain (Credential Manager di Windows, Keychain di macOS, Secret Service/libsecret di Linux) menyimpan rahasia dalam penyimpanan yang terenkripsi, terikat user, dan dilindungi OS. Ini tempat yang benar untuk kredensial persisten.

PlatformPenyimpananAkses
WindowsCredential LockerTerikat user, transparan saat login
macOSKeychainDikunci user; prompt saat unlock
Linuxlibsecret / Secret ServiceTerikat user session (gnome-keyring, dll)

Praktik di Tiga Framework

Tauri: plugin keyring
use tauri_plugin_keyring::{Keyring, KeyringExt};
 
#[tauri::command]
async fn save_token(app: tauri::AppHandle, token: String) -> Result<(), String> {
    app.keyring()
        .set_password("catatanku", "cloud_token", &token)
        .map_err(|e| e.to_string())
}
 
#[tauri::command]
async fn get_token(app: tauri::AppHandle) -> Result<Option<String>, String> {
    app.keyring()
        .get_password("catatanku", "cloud_token")
        .map_err(|e| e.to_string())
}
JSElectron: safeStorage + keytar
const { safeStorage } = require("electron");
const keytar = require("keytar");
 
async function saveToken(token) {
  const encrypted = safeStorage.encryptString(token);
  await keytar.setPassword("catatanku", "cloud_token", encrypted.toString("base64"));
}
 
async function getToken() {
  const raw = await keytar.getPassword("catatanku", "cloud_token");
  if (!raw) return null;
  return safeStorage.decryptString(Buffer.from(raw, "base64"));
}

Pola yang sama di Flutter lewat plugin seperti flutter_keychain/flutter_secure_storage (Linux: libsecret; Windows: Credential Manager; macOS: Keychain). Prinsip yang konsisten: kredensial dibaca melalui API OS, bukan file manual.

Note

Di Linux, keyring bergantung pada lingkungan desktop (gnome-keyring, kwallet). Di lingkungan tanpa keyring (server, minimal WM), fallback perlu dipertimbangkan — tetapi selalu dengan enkripsi dan peringatan eksplisit, bukan sekadar file plaintext.

Enkripsi untuk Data At-Rest

Melindungi Isi Catatan

Keychain melindungi kredensial. Tapi isi catatan — yang jauh lebih bernilai — ada di database SQLite (episode 8). Jika laptop dicuri dan disk dibongkar, database plaintext bisa dibaca. Solusinya: enkripsi data at-rest.

Kita menambah content terenkripsi ke skema CatatanKu:

Skema catatan terenkripsi
CREATE TABLE notes (
  id         TEXT PRIMARY KEY,
  title      TEXT,
  content    BLOB,          -- isi terenkripsi
  enc_nonce  BLOB,          -- nonce/IV per catatan
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

Setiap catatan dienkripsi dengan nonce unik — kunci yang sama, nonce berbeda, ciphertext berbeda. Ini mencegah pola statistik dan serangan replay.

Tauri: enkripsi per catatan (AES-256-GCM)
use aes_gcm::{Aes256Gcm, KeyInit, aead::{Aead, KeyInit as _}};
use aes_gcm::aead::generic_array::GenericArray;
use rand::rngs::OsRng;
 
fn encrypt_content(key: &[u8; 32], content: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
    let cipher = Aes256Gcm::new_from_slice(key).unwrap();
    let nonce = rand::random::<[u8; 12]>();
    let ct = cipher
        .encrypt(GenericArray::from_slice(&nonce), content)
        .map_err(|e| e.to_string())?;
    Ok((ct, nonce.to_vec()))
}
JSElectron: enkripsi per catatan
const crypto = require("node:crypto");
 
function encryptContent(key, content) {
  const nonce = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
  const encrypted = Buffer.concat([cipher.update(content, "utf8"), cipher.final()]);
  return { encrypted, nonce, tag: cipher.getAuthTag() };
}

Kunci desain: jangan pernah menyimpan kunci di database yang sama dengan ciphertext. Kunci hidup di tempat yang lebih aman — dibedah di bawah.

Strategi Kunci: Kunci Harus Lebih Aman dari Data

Hierarki Kunci

Ciphertext seaman kuncinya. Sumber kunci enkripsi CatatanKu, dari yang terkuat:

100%
  • Data key dienkripsi oleh master key; master key diturunkan dari passphrase pengguna dengan key derivation function (Argon2id) yang lambat — menahan serangan brute-force.
  • Passphrase tidak pernah disimpan; hanya hasil derivation.
  • Jika pengguna lupa passphrase → data tidak bisa dibuka. Ini fitur (anti pencurian), bukan bug — komunikasikan dengan jelas di UI.
Derivasi kunci dari passphrase (Argon2id)
use argon2::{Argon2, password_hash::{SaltString, PasswordHasher}};
 
let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
let password_hash = argon2.hash_password(passphrase.as_bytes(), &salt)?.to_string();
// password_hash dipakai sebagai master key (via HKDF) 

Common Pitfalls

  • Keychain sebagai penyimpanan semua data — keychain untuk kredensial kecil; data besar tetap database terenkripsi.
  • Kunci dan data di tempat yang sama — kunci di file config sebelah DB = sandi di atas kunci pintu.
  • Enkripsi hanya isi, metadata bocor — judul catatan juga pribadi; pertimbangkan mengenkripsi judul juga (trade-off dengan pencarian FTS).
  • Nonce dipakai ulang — AES-GCM dengan nonce yang sama untuk dua pesan membocorkan kunci; selalu nonce acak per operasi.
  • Passphrase pendek — Argon2 dengan passphrase 1234 tetap bisa dipecah; edukasi pengguna + minimum length.

Penutup

Inti yang harus dibawa pulang:

  • OS keychain adalah tempat benar untuk kredensial persisten — terenkripsi dan terikat user.
  • Data at-rest (isi catatan) dienkripsi dengan AES-256-GCM, nonce unik per catatan.
  • Strategi kunci: master key dari passphrase (Argon2id) melindungi data key; kunci tidak pernah di database yang sama.
  • Pitfalls: nonce reuse, kunci di dekat data, dan keychain yang disalahgunakan untuk data besar.

Di episode 20 selanjutnya kita memastikan CatatanKu menghormati penggunanya: privacy & compliance — pengumpulan data, telemetry, dan kepatuhan GDPR. Sampai jumpa di episode 20!

Belajar Desktop Developer - Secure Storage & Credentials | Belajar Desktop Developer