8
Manajemen Host dan Keychain
Satu host di Termimus disimpan sebagai baris tabel hosts, bukan sebagai kumpulan rahasia. Baris itu memuat address, port, username, auth_method, serta credential_id yang menunjuk ke satu baris credentials. Private key atau password tidak pernah ikut ke hosts; ia hidup sebagai ciphertext dan nonce di credentials sehingga satu key dapat dipakai banyak host sekaligus. Bentuk kedua struct itu didefinisikan di src-tauri/src/db/models.rs:39-631 dan src-tauri/src/db/models.rs:86-1112.
Tiga tabel yang membentuk satu host
Host.folder_id dan Host.credential_id adalah foreign key ke folders dan credentials. Keduanya memakai ON DELETE SET NULL, jadi menghapus folder atau credential tidak ikut menghapus host — host hanya kehilangan rujukannya. auth_method disimpan sebagai teks, sedangkan enum AuthMethod di backend menyediakan tiga nilai baku: password, private_key, dan agent (src-tauri/src/db/mod.rs:38-773, src-tauri/src/db/models.rs:13-374).
erDiagram
folders ||--o{ hosts : "folder_id"
credentials ||--o{ hosts : "credential_id"
hosts ||--o{ hosts : "jump_host_id"
folders {
TEXT id PK
TEXT name
TEXT parent_id
}
credentials {
TEXT id PK
TEXT kind
BLOB ciphertext
BLOB nonce
BLOB passphrase_ciphertext
TEXT name
TEXT key_type
TEXT public_key
TEXT fingerprint
TEXT username
}
hosts {
TEXT id PK
TEXT folder_id FK
TEXT label
TEXT address
INTEGER port
TEXT username
TEXT auth_method
TEXT credential_id FK
TEXT tags
TEXT os_icon
TEXT jump_host_id FK
TEXT last_connected_at
}
%% fig: Relasi hosts, folders, dan credentials
%% desc: Satu folder menampung banyak host, satu credential Keychain dapat dirujuk banyak host, dan jump_host_id menunjuk host lain sebagai bastion.tags disimpan di hosts sebagai satu string JSON, bukan tabel terpisah: save_host memanggil serde_json::to_string(&host.tags) sebelum menulis baris (src-tauri/src/db/mod.rs:269-3075). Karena itu penyaringan tag di frontend bekerja pada array di memori, bukan lewat query SQL.
Dari private key menjadi credential terenkripsi
Saat sebuah key disimpan lewat keychain_save_key, backend tidak mempercayai algorithm, public_key, maupun fingerprint yang dikirim frontend. Nilai-nilai itu diturunkan ulang di sisi Rust dari PEM dengan inspect_key; hanya ciphertext hasil vault.encrypt yang disimpan bersama hasil turunannya. Komentar fungsi itu menyatakan tujuannya secara eksplisit: "The algorithm badge, OpenSSH public key and fingerprint are re-derived server-side from the PEM so the UI never has to trust client-supplied values." (src-tauri/src/commands/mod.rs:734-8186).
graph LR
pem["private_key_pem dari form"] --> inspect["inspect_key"]
inspect --> algo["algorithm: ED25519 / RSA / ECDSA"]
inspect --> pub["public_key OpenSSH"]
inspect --> fp["fingerprint SHA256"]
pem --> enc["vault.encrypt"]
enc --> blob["ciphertext + nonce"]
algo --> cred[("credentials")]
pub --> cred
fp --> cred
blob --> cred
style inspect fill:#f59e0b,color:#000
style enc fill:#2d7d46,color:#fff
style pub fill:#8b5cf6,color:#fff
style fp fill:#8b5cf6,color:#fff
style cred fill:#336791,color:#fff
%% fig: Penurunan public key dan fingerprint saat menyimpan key
%% desc: PEM dienkripsi untuk kolom ciphertext, sementara algorithm, public_key, dan fingerprint dihitung ulang oleh inspect_key sebelum masuk tabel credentials.inspect_key menerima dua bentuk masukan. Teks yang dimulai ssh-, ecdsa-, atau sk- dianggap public key dan dikembalikan dengan is_public_key_only: true; selain itu teks diperlakukan sebagai private key, didekripsi dengan passphrase bila ada, lalu public key-nya diekstrak. Fingerprint selalu dihitung dengan HashAlg::Sha256 sehingga berawalan SHA256:, dan label algoritma dinormalkan menjadi ED25519, RSA, atau ECDSA (src-tauri/src/sshkey/mod.rs:101-1467).
Untuk membuat key baru, generate_keypair memilih algoritma lewat parse_algorithm: ed25519, rsa-3072, rsa-4096, rsa, ecdsa-p256, ecdsa-p384, atau ecdsa-p521. Nama lain ditolak dengan pesan Unsupported key algorithm: {other}. Semua varian RSA menghasilkan key 4096 bit; alias rsa-3072 diterima karena pustaka hanya mengekspos satu ukuran RSA saat pembuatan key (src-tauri/src/sshkey/mod.rs:17-278, src-tauri/src/sshkey/mod.rs:39-619).
Perintah Tauri yang membuka operasi ini ke frontend:
| Command | Masukan | Keluaran | Sumber |
|---|---|---|---|
key_generate | algorithm, comment | private_key_pem dan public_key_openssh | src-tauri/src/commands/mod.rs:698-70510 |
key_derive_public | pem, passphrase | satu baris public key OpenSSH | src-tauri/src/commands/mod.rs:711-71411 |
keychain_list | — | daftar KeychainItem tanpa secret | src-tauri/src/commands/mod.rs:723-72912 |
keychain_save_key | KeychainKeyInput, item_id opsional | KeychainItem | src-tauri/src/commands/mod.rs:734-8186 |
keychain_save_identity | KeychainIdentityInput, item_id opsional | KeychainItem | src-tauri/src/commands/mod.rs:820-86113 |
keychain_get_public_key | id | public_key tersimpan | src-tauri/src/commands/mod.rs:870-88114 |
keychain_get_private_key | id | private_key_pem dan passphrase terdekripsi | src-tauri/src/commands/mod.rs:889-91615 |
keychain_save_key menolak tiga keadaan sebelum menulis apa pun: vault terkunci (Vault must be unlocked to save Keychain items), nama kosong (Key name is required), dan PEM kosong tanpa item lama (Private key is required). Bila item_id diberikan dan private_key_pem kosong, ciphertext serta metadata lama dipakai ulang, sehingga mengganti nama key tidak memaksa impor ulang PEM (src-tauri/src/commands/mod.rs:740-74216, src-tauri/src/commands/mod.rs:743-74517, src-tauri/src/commands/mod.rs:766-76818).
Memilih credential saat menyimpan host
HostModal membagi isi Keychain menjadi dua daftar sebelum ditampilkan: availableKeys berisi item dengan kind === "private_key", availableIdentities berisi item dengan kind === "password". Item public_key sengaja tidak masuk daftar key karena tidak punya materi privat dan akan gagal saat koneksi (src/components/hosts/HostModal.tsx:51-5519).
Pemilihan identity ikut mengisi username: handleSelectCredential menyalin item.username bila kolom username masih kosong atau masih bernilai root (src/components/hosts/HostModal.tsx:139-14820). Kolom password bermasker secara default; klik ikon mata memicu handleTogglePassword, yang mengambil secret lewat api.getCredentialSecret untuk identity terpilih atau api.getHostPassword untuk password host itu sendiri, lalu baru menampilkan nilainya (src/components/hosts/HostModal.tsx:154-17821).
Saat key dipilih, panel pratinjau menampilkan public_key dari item tersebut beserta tombol Copy Public Key yang menyalinnya ke clipboard — inilah jalur menempelkan key ke ~/.ssh/authorized_keys di server tujuan tanpa membuka private key sama sekali (src/components/hosts/HostModal.tsx:547-60122).
Validasi di form terjadi sebelum saveHost dipanggil:
| Kondisi | Pesan error | Sumber |
|---|---|---|
address kosong | Address (IP or hostname) is required | src/components/hosts/HostModal.tsx:209-22123 |
username kosong | Username is required | src/components/hosts/HostModal.tsx:209-22123 |
auth private_key tanpa key terpilih | Please select an SSH key from Keychain | src/components/hosts/HostModal.tsx:209-22123 |
credential_id yang dikirim bergantung pada metode auth: pada private_key nilainya wajib selectedCredentialId, pada password nilainya selectedCredentialId || null. Field secret hanya diisi bila pengguna memilih password sekali pakai, bukan identity dari Keychain (src/components/hosts/HostModal.tsx:205-26424).
Di backend, host_save memakai input.credential_id bila ada. Bila tidak ada dan secret terisi, ia membuat Credential baru dengan name kosong — "Anonymous one-off credential, not shown in the Keychain list". Bila host yang sedang diedit sudah punya credential anonim, id itu dipakai ulang; credential bernama selalu mendapat id baru agar entri Keychain bersama tidak tertimpa (src-tauri/src/commands/mod.rs:361-45725).
Dua jenis isi Keychain
kind | Dibuat oleh | key_type / public_key / fingerprint | Muncul di daftar Keychain | Dapat dipakai host |
|---|---|---|---|---|
private_key | keychain_save_key dengan PEM privat | hasil inspect_key | ya | ya, untuk auth_method = private_key |
public_key | keychain_save_key dengan teks public key | hasil inspect_key, is_public_key_only: true | ya | tidak |
password | keychain_save_identity | string kosong | ya | ya, untuk auth_method = password |
anonim (name kosong) | host_save dengan secret inline | string kosong | tidak | hanya oleh host pemiliknya |
keychain_save_identity selalu menulis kind: "password" dengan key_type, public_key, dan fingerprint kosong; password-nya dienkripsi lebih dulu lewat vault.encrypt (src-tauri/src/commands/mod.rs:820-86113). list_keychain_items hanya mengembalikan baris dengan name != '', itulah sebabnya credential anonim tidak pernah tampil di UI (src-tauri/src/db/mod.rs:487-51426).
KeychainItem adalah tampilan bebas secret: ia membawa id, kind, name, key_type, public_key, fingerprint, dan username, tetapi tidak pernah membawa ciphertext maupun nonce. Konversinya dari Credential dilakukan oleh impl From<&Credential> for KeychainItem (src-tauri/src/db/models.rs:113-12627).
KeychainView memakai pembagian yang sama seperti HostModal, tetapi memasukkan public_key ke kelompok Keys dan password ke kelompok Identities. Pencarian mencocokkan name, key_type, dan username. Kartu key menampilkan badge PUB untuk item public_key dan menonaktifkan tombol salin bila public_key kosong (src/components/keychain/KeychainView.tsx:79-9528, src/components/keychain/KeychainView.tsx:415-44829). Menu klik kanan menyediakan Copy Public Key, Copy Fingerprint, Copy Username, Edit Credential, dan Delete Credential (src/components/keychain/KeychainView.tsx:275-28730).
Penyegaran data setelah mutasi ditangani store, bukan komponen: saveKey, saveIdentity, dan deleteItem memanggil API terkait lalu refresh(), yang mengambil ulang seluruh daftar lewat api.listKeychain. isLoading hanya dinyalakan bila daftar masih kosong, sehingga penyimpanan berikutnya tidak membuat daftar berkedip (src/stores/useKeychainStore.ts:46-7531).
Daftar host: folder, tag, dan pencarian
HostList menghitung tag unik dari seluruh host, lalu menyaring berdasarkan tag terpilih dan kata kunci yang dicocokkan ke label, address, username, serta setiap tag. Urutan tampilan memakai last_connected_at, dengan created_at dan updated_at sebagai cadangan bila host belum pernah terkoneksi. Jumlah host per folder dihitung dari folder_id sehingga kartu folder dapat menampilkan isinya (src/components/hosts/HostList.tsx:195-23132). Preferensi tampilan grid atau list disimpan di localStorage dengan kunci termimus_hosts_view_mode (src/components/hosts/HostList.tsx:85-9833).
Menghapus host dan menghapus folder sama-sama melewati dialog konfirmasi. Pesan untuk folder menyatakan bahwa host di dalamnya tidak ikut dihapus dan berpindah ke ungrouped, sesuai ON DELETE SET NULL pada hosts.folder_id (src/components/hosts/HostList.tsx:290-32034, src-tauri/src/db/mod.rs:38-773).
Duplikat host dan pemakaian ulang credential
Mode duplikat memakai templateHost sebagai sumber semua field dan memberi label [Copy] <label asli>. Rujukan Keychain dipertahankan hanya bila credential_id sumber benar-benar ada di daftar item Keychain; credential anonim dibuang sehingga pengguna harus mengisi rahasia baru (src/components/hosts/HostModal.tsx:101-12035).
Menghapus credential
⚠️ Menghapus item Keychain tidak menolak permintaan hanya karena item itu sedang dipakai.
delete_credentialmenjalankanDELETE FROM credentialslalu menulis tombstoneentity_type = 'credential', sementarahosts.credential_iddisetNULLoleh foreign key. Host lama tetap ada, tetapi kehilangan rujukan kredensialnya dan tidak bisa lagi terhubung sampai credential baru dipasang. Dialog konfirmasi di UI sudah menyatakan hal ini: "Any hosts using this credential will need a new one assigned. This action cannot be undone." (src-tauri/src/db/mod.rs:475-48536, src/components/keychain/KeychainView.tsx:108-12137).
Menghapus host juga membersihkan rujukan bastion: delete_host lebih dulu menjalankan UPDATE hosts SET jump_host_id = NULL WHERE jump_host_id = ?1, baru menghapus baris host dan menulis tombstone entity_type = 'host'. Credential anonim milik host itu tidak ikut terhapus (src-tauri/src/db/mod.rs:319-33038).
Saat koneksi berjalan, password host diambil ulang dengan mendekripsi credential yang dirujuk: host_get_password mengembalikan string kosong bila credential_id bernilai None, dan credential_get_secret menolak permintaan dengan Vault must be unlocked to view credentials ketika vault masih terkunci (src-tauri/src/commands/mod.rs:918-94239, src-tauri/src/commands/mod.rs:946-95640). Rincian enkripsi dan kunci vault ada di Vault dan Model Kriptografi, sedangkan pemakaian credential saat sesi dibuka dibahas di Siklus Hidup Koneksi SSH. Struktur kolom lengkap seluruh tabel ada di Skema Database Lokal.