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

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.
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.
| Platform | Penyimpanan | Akses |
|---|---|---|
| Windows | Credential Locker | Terikat user, transparan saat login |
| macOS | Keychain | Dikunci user; prompt saat unlock |
| Linux | libsecret / Secret Service | Terikat user session (gnome-keyring, dll) |
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())
}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.
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:
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.
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()))
}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.
Ciphertext seaman kuncinya. Sumber kunci enkripsi CatatanKu, dari yang terkuat:
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) 1234 tetap bisa dipecah; edukasi pengguna + minimum length.Inti yang harus dibawa pulang:
Di episode 20 selanjutnya kita memastikan CatatanKu menghormati penggunanya: privacy & compliance — pengumpulan data, telemetry, dan kepatuhan GDPR. Sampai jumpa di episode 20!