Lompat ke konten
dokumentasi.id
Commit 0da916f
v1
Star0

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

4

Vault dan Model Kriptografi

VaultManager adalah satu-satunya tempat key turunan disimpan di backend. Struct ini hanya punya satu field, derived_key: RwLock<Option<[u8; 32]>>, sehingga key ada di memori proses dan tidak pernah ditulis ke tabel mana pun selain salt dan verifier (src-tauri/src/vault/mod.rs:12-21). AppState memegangnya sebagai Arc<VaultManager> dan membuat satu instance saat aplikasi dijalankan (src-tauri/src/lib.rs:24-30).

Primitif kriptografi

LapisanImplementasiNilai di kode
Key derivationArgon2::default() lewat hash_password_intooutput [u8; 32]
Saltgenerate_salt() dengan OsRng.fill_bytes16 byte
CipherAes256Gcm (aes-gcm)key 32 byte
NonceOsRng.fill_bytes per operasi12 byte
VerifierVERIFY_STRING = b"TERMIMUS_VAULT_OK"18 byte plaintext
Memory hygienezeroize pada lockkey.zeroize()

derive_key memanggil Argon2::default().hash_password_into dengan password dan salt, lalu mengembalikan array 32 byte; salt 16 byte dibuat oleh generate_salt memakai OsRng (src-tauri/src/vault/mod.rs:87-102). README menamai algoritma ini Argon2id dengan salt acak 16 byte (README.md:217).

encrypt_with_key membuat Aes256Gcm dari key 32 byte, mengisi nonce_bytes 12 byte dari OsRng untuk setiap panggilan, lalu mengembalikan pasangan (ciphertext, nonce); decrypt_with_key menolak nonce yang panjangnya bukan 12 dengan pesan Invalid nonce length (src-tauri/src/vault/mod.rs:124-155). Karena nonce dibuat per operasi, setiap item mendapat nonce acak sendiri. README menegaskan cipher terautentikasi dengan nonce 12 byte unik per item dari OsRng (README.md:218).

Siklus hidup key

sequenceDiagram
  autonumber
  participant UI as useVaultStore
  participant CMD as commands (tauri)
  participant VM as VaultManager
  participant DB as vault_meta

  Note over UI,DB: Setup
  UI->>CMD: vault_setup(password)
  CMD->>VM: generate_salt()
  CMD->>VM: derive_key(password, salt)
  CMD->>DB: set_vault_meta("salt")
  CMD->>VM: set_key(key)
  CMD->>VM: encrypt(VERIFY_STRING)
  CMD->>DB: set_vault_meta("verifier_ciphertext", "verifier_nonce")
  VM-->>CMD: (ciphertext, nonce)

  Note over UI,DB: Unlock
  UI->>CMD: vault_unlock(password)
  CMD->>DB: get_vault_meta("salt", "verifier_ciphertext", "verifier_nonce")
  CMD->>VM: derive_key(password, salt)
  CMD->>VM: set_key(key)
  CMD->>VM: decrypt(verifier_ct, verifier_nonce)
  alt payload == VERIFY_STRING
    VM-->>UI: Ok(true)
  else salah password
    CMD->>VM: lock() (zeroize)
    VM-->>UI: Err("Incorrect master password")
  end

  Note over UI,DB: Simpan secret
  UI->>CMD: host_save(secret)
  CMD->>VM: is_unlocked()
  CMD->>VM: encrypt(secret.as_bytes())
  VM-->>CMD: (ciphertext, nonce)
  CMD->>DB: save_credential(ciphertext, nonce)

  Note over UI,DB: Auto-lock
  UI->>UI: useAutoLock timer / blur
  UI->>CMD: vault_lock()
  CMD->>VM: lock()
  VM->>VM: key.zeroize()
  VM->>VM: derived_key = None

%% fig: Siklus hidup key vault dari setup sampai auto-lock %% desc: Key diturunkan dan diverifikasi lewat payload VERIFY_STRING, dipakai untuk enkripsi per item, lalu di-zeroize saat lock.

Setup

vault_setup menolak password lebih pendek dari 4 karakter dengan pesan Password must be at least 4 characters long, membuat salt baru, menurunkan key, menyimpan salt ke vault_meta, memasang key di memori lewat set_key, lalu mengenkripsi VERIFY_STRING dan menyimpan verifier_ciphertext serta verifier_nonce (src-tauri/src/commands/mod.rs:45-70). Salt dan pasangan verifier itu ditulis ke tabel vault_meta; skema tabelnya dibahas di Skema Database Lokal.

Unlock

vault_unlock membaca salt, verifier_ciphertext, dan verifier_nonce dari vault_meta; bila salah satunya hilang ia mengembalikan Vault has not been initialized yet, Corrupted vault state: verifier missing, atau Corrupted vault state: nonce missing (src-tauri/src/commands/mod.rs:73-90). Key hasil derive_key dipasang lebih dulu, lalu decrypt dipanggil pada verifier: hasil yang sama dengan VERIFY_STRING berarti sukses, selain itu vault.lock() dijalankan dan command mengembalikan Incorrect master password (src-tauri/src/commands/mod.rs:92-102).

Lock dan zeroize

lock mengambil write lock, memanggil key.zeroize() pada byte key, lalu mengisi derived_key dengan None (src-tauri/src/vault/mod.rs:27-34). Setelah itu is_unlocked() bernilai false dan setiap encrypt/decrypt gagal dengan Vault is locked. Unlock vault first. (src-tauri/src/vault/mod.rs:157-175). export_key_hex juga menolak saat terkunci dengan Vault is locked (src-tauri/src/vault/mod.rs:110-122).

Penyimpanan secret

Saat menyimpan host dengan password baru, host_save memeriksa state.vault.is_unlocked() dan menolak dengan Vault must be unlocked to save credentials, lalu mengenkripsi secret dan (bila ada) passphrase secara terpisah sehingga masing-masing memperoleh ciphertext dan nonce sendiri sebelum save_credential dipanggil (src-tauri/src/commands/mod.rs:371-417). Jalur baca ada di resolve_host_auth, yang memanggil state.vault.decrypt untuk password maupun PEM private key saat koneksi SSH dibuka (src-tauri/src/commands/mod.rs:500-549).

Command yang menampilkan secret ke UI memakai penjagaan yang sama: keychain_get_private_key menolak dengan Vault must be unlocked to view key, host_get_password dengan Vault must be unlocked to view password, dan credential_get_secret dengan Vault must be unlocked to view credentials (src-tauri/src/commands/mod.rs:891-958).

Auto-lock dan layar kunci

stateDiagram-v2
  [*] --> Terkunci
  Terkunci --> Terbuka: vault_unlock / vault_biometric_unlock
  Terbuka --> Terkunci: vault_lock (manual)
  Terbuka --> AppLocked: lockApp (padlock)
  Terbuka --> Terkunci: autoLock (timer, blur, beforeunload)
  AppLocked --> Terbuka: unlock(password) / unlockWithBiometric
  Terkunci --> Terbuka: vault_keyring_unlock (senyap)
  AppLocked --> AppLocked: keyring unlock sengaja dilewati
  style Terbuka fill:#2d7d46,color:#fff
  style Terkunci fill:#f59e0b,color:#000
  style AppLocked fill:#dc2626,color:#fff

%% fig: Tiga keadaan vault di UI %% desc: Lock manual dan auto-lock memakai jalur yang sama, sedangkan AppLocked memaksa autentikasi sadar tanpa keyring.

Store UI membedakan tiga aksi lock. lock memanggil api.lockVault() lalu menutup prompt; lockApp melakukan hal sama tetapi menyalakan isAppLocked sehingga UI tertutup AppLockScreen; autoLock membuka kembali prompt dan menandainya isUnlockPromptDismissable (src/stores/useVaultStore.ts:149-175). Komentar di store menyatakan lockApp sengaja tidak menyentuh sesi SSH/SFTP/tunnel maupun buffer terminal, hanya key vault yang di-zeroize (src/stores/useVaultStore.ts:156-163). AppLockScreen mengembalikan null bila !isAppLocked || isUnlocked, tidak punya tombol dismiss, dan menyediakan jalur password serta biometrik (src/components/vault/AppLockScreen.tsx:44-75).

refresh mencoba unlock senyap lewat OS keyring hanya bila vault terinisialisasi, terkunci, dan isAppLocked bernilai false; saat layar kunci aplikasi aktif, jalur itu dilewati agar kembali ke aplikasi selalu butuh autentikasi sadar (src/stores/useVaultStore.ts:51-75).

Kebijakan auto-lock

AutoLockPolicy berisi lima nilai: never, on_close, 15min, 1hour, dan on_focus_loss, dengan default on_close (src/stores/useSettingsStore.ts:4-33). useAutoLock memetakan 15min ke 15 * 60 * 1000 ms dan 1hour ke 60 * 60 * 1000 ms, lalu mereset timer pada mousemove, mousedown, keydown, touchstart, dan wheel (src/hooks/useAutoLock.ts:5-44). Untuk on_focus_loss ia memanggil autoLock() pada event blur jendela (src/hooks/useAutoLock.ts:46-56), dan untuk beforeunload ia memanggil useVaultStore.getState().autoLock() kecuali kebijakannya never (src/hooks/useAutoLock.ts:58-72).

⚠️ Kebijakan never tetap tidak melindungi key saat proses berakhir: key hanya di-zeroize bila lock dipanggil, dan lock dijalankan oleh event beforeunload serta oleh vault_lock. Menutup proses secara paksa melewati keduanya.

Keyring dan biometrik

Key turunan bisa didelegasikan ke OS keyring sebagai hex 64 karakter dengan service termimus dan user vault-derived-key; vault_keyring_save menolak bila vault terkunci memakai pesan Vault is locked — unlock it first before enabling OS Keyring. (src-tauri/src/commands/mod.rs:110-131). unlock_from_keyring_internal menolak entri yang panjangnya bukan 64 dengan Keyring entry is corrupted (unexpected length)., lalu memverifikasi key hasil decode terhadap verifier; bila verifier tidak cocok, key di-zeroize, entri keyring dihapus, dan fungsi mengembalikan Ok(false) (src-tauri/src/commands/mod.rs:133-177). README menyebut delegasi ini untuk KWallet, GNOME Secret Service, macOS Keychain, dan Windows Credential Manager (README.md:221).

Biometrik dibungkus modul biometric: is_supported() dan authenticate(reason) diekspor dari macos pada macOS dan dari unsupported pada platform lain (src-tauri/src/biometric/mod.rs:1-15). Implementasi macOS memakai LAContext dengan LAPolicyDeviceOwnerAuthenticationWithBiometrics = 1, dan authenticate mengembalikan Err("Touch ID is not available or not configured on this device") bila policy tidak bisa dievaluasi (src-tauri/src/biometric/macos.rs:12-63). vault_biometric_unlock menuntut is_supported(), menuntut key tersimpan di keyring (No saved vault key found in OS Keyring. Please unlock with master password first to enable biometric unlock.), menjalankan authenticate("Unlock Termimus Vault") di spawn_blocking, lalu memakai unlock_from_keyring_internal (src-tauri/src/commands/mod.rs:201-230). Artinya biometrik tidak menurunkan key baru; ia hanya memberi gerbang pada key yang sudah ada di keyring.

Rotasi master password

graph LR
  OLD["old_password + salt"] --> DK1[derive_key] --> V{"decrypt verifier"}
  V -->|"bukan VERIFY_STRING"| FAIL["Current password is incorrect."]
  V -->|cocok| LOOP["for cred in list_credentials"]
  LOOP --> D["decrypt_with_key(old_key)"]
  D -->|"Err"| SKIP["eprintln warning, lewati"]
  D -->|"Ok"| E["encrypt_with_key(new_key)"]
  E --> TX["rekey_vault: satu transaksi SQLite"]
  SKIP --> TX
  NEW["new_password + new_salt"] --> DK2[derive_key] --> E
  DK2 --> NV["encrypt_with_key(VERIFY_STRING)"] --> TX
  TX --> SET["set_key(new_key)"]
  SET --> CLR["vault_keyring_clear()"]
  style FAIL fill:#dc2626,color:#fff
  style SKIP fill:#f59e0b,color:#000
  style TX fill:#336791,color:#fff
  style SET fill:#2d7d46,color:#fff
  style CLR fill:#8b5cf6,color:#fff

%% fig: Alur vault_change_password %% desc: Password lama diverifikasi ke verifier, kredensial dienkripsi ulang dengan key baru, lalu semuanya di-commit dalam satu transaksi.

vault_change_password menolak password baru di bawah 4 karakter dengan New password must be at least 4 characters., memverifikasi password lama terhadap verifier (Current password is incorrect.), membuat salt baru, lalu mengenkripsi ulang setiap credential beserta passphrase-nya dengan key baru (src-tauri/src/commands/mod.rs:247-317). Credential yang gagal didekripsi tidak menggagalkan proses: ia dilewati dan dicatat lewat eprintln! dengan prefiks [Vault] Warning: skipping undecryptable credential during re-keying (src-tauri/src/commands/mod.rs:313-316).

Hasil enkripsi ulang, salt baru, dan verifier baru di-commit oleh rekey_vault dalam satu transaksi SQLite sehingga database tidak pernah setengah ter-key (src-tauri/src/db/mod.rs:378-420). Setelah commit, key di memori diganti dengan key baru dan entri keyring lama dihapus supaya pengguna mengaktifkannya ulang (src-tauri/src/commands/mod.rs:334-340).

⚠️ vault_reset bersifat destruktif: ia memanggil state.vault.lock(), menghapus entri keyring, lalu reset_vault() yang menjalankan DELETE FROM credentials; DELETE FROM vault_meta; UPDATE hosts SET credential_id = NULL; sehingga semua password dan private key hilang permanen (src-tauri/src/commands/mod.rs:343-354, src-tauri/src/db/mod.rs:932-941).

Enkripsi dengan passphrase terpisah

Backup memakai jalur yang tidak menyentuh key vault: encrypt_with_passphrase membuat salt sendiri, menurunkan key dari passphrase, dan mengembalikan tiga nilai (ciphertext, salt, nonce), sedangkan decrypt_with_passphrase menurunkan ulang key dari salt itu dan memetakan kegagalan AEAD menjadi Decryption failed — wrong passphrase or corrupted backup (src-tauri/src/vault/mod.rs:36-85). Karena kuncinya berbeda, mengubah master password tidak membuat backup lama tidak terbaca. Alur backup dan sync dijelaskan di Backup, Sync Relay, dan API Server.

Batas kepercayaan

Yang disimpan di diskYang hanya di memori
salt 16 bytederived_key [u8; 32]
verifier_ciphertext + verifier_noncepassword master (hanya selama command berjalan)
ciphertext + nonce per credentialplaintext secret saat decrypt dipanggil
hex key di OS keyring (bila diaktifkan)—

Salt dan verifier disimpan sebagai blob di vault_meta lewat set_vault_meta, yang memakai INSERT ... ON CONFLICT(key) DO UPDATE (src-tauri/src/db/mod.rs:911-930). Karena yang tersimpan hanya salt, ciphertext, dan nonce, penyerang yang membaca berkas database tidak dapat menurunkan key tanpa password master. Verifikasi host key SSH memakai mekanisme terpisah dan dibahas di Verifikasi Host Key TOFU.