mobile-ssh-manager/.zcode/plans/plan-sess_2e35df48-e85a-43a2-9d74-a11c445163f4.md

6 KiB

SSH Connection Manager (Android, Kotlin + Jetpack Compose) — Bauplan

Wichtige Vorbemerkung: 1Password auf Android

Echte native 1Password-SSH-Integration ist auf Android technisch nicht möglich — der 1Password SSH Agent und die SDKs sind Desktop-only (macOS/Windows/Linux), es gibt keine Kotlin/Java-API und keinen Android-Agent, nur einen offenen Feature-Request. Du hast dich für „Nur Datei-Import" entschieden: Private Keys werden per Storage-Access-Framework importiert, AES-verschlüsselt (Android Keystore) gespeichert. Der praktische 1P-Workflow ist dann: Key auf dem Desktop aus 1Password exportieren → aufs Handy übertragen → in der App importieren. Kein 1Password-Bezug im UI.

Stack & Abhängigkeiten (je current stable, 2026)

  • Kotlin 2.0+ (K2), AGP 8.7+, JDK 17, Compile/Target SDK 35, Min SDK 26
  • Jetpack Compose (BOM), Material 3, dunkles Theme, Single-Activity + Navigation-Compose
  • Hilt (DI, via KSP), Room 2.6+ (via KSP), Coroutines + Flow
  • sshj 0.39.x — SSH-Verbindung, Auth (Passwort + Public Key), Shell/PTY
  • Termux terminal-emulator via JitPack (v0.118.3) — VT100/xterm-Emulation (state machine, ANSI-Parsing, Buffer)
  • Bouncy Castle (bcprov-jdk18on) — registriert als Provider, damit sshj OpenSSH-/Ed25519-Keys liest
  • Android Keystore — AES-GCM-Schlüssel zur Feldverschlüsselung (Passwörter, private Keys)

Architektur (Clean Layering, Paket-Struktur)

core/crypto      KeystoreCrypto (AES-GCM Ver-/Entschlüsselung sensitiver Felder)
data             Room: AppDatabase, Entities (Host, SshKey, Group), DAOs, Repositories, DI
domain/model     Host, SshKey, Group, AuthMethod (sealed)
ssh              SshConnectionManager (sshj SSHClient, Auth, Shell), SshSession, KeyLoader, DI
terminal         SshTerminalBridge, TerminalShell, KeyEncoder, ui/TerminalScreen (Canvas), ViewModel
ui               theme, nav (AppNavGraph, Routes), hostlist, hosteditor, keylist, keyimport, components

Datenmodell (Room)

  • Host: id, name, hostName, port, userName, groupId?, authType (PASSWORD/KEY), passwordCipher?, keyId?, lastConnected?
  • SshKey: id, name, keyType, fingerprint, privateKeyCipher, hasPassphrase, createdAt
  • Group (optional): id, name, sortIndex — zum Ordnen von Hosts

Sicherheits-Workflow

  1. Key-Import via SAF (ACTION_OPEN_DOCUMENT, MIME */*), Datei als String lesen.
  2. Mit sshj parsen → Typ + Fingerprint bestimmen; ggf. Passphrase abfragen.
  3. Private Key mit Keystore-AES-GCM verschlüsseln, Ciphertext in Room speichern, Klartext sofort verwerfen.
  4. Beim Verbinden: entschlüsseln → in den Arbeitsspeicher → sshj laden → verbinden → verwerfen.
  5. Host-Passwörter analog verschlüsselt.

Terminal-Integration (Kernbaustein)

Bidirektionale Brücke zwischen sshj-Shell-Stream und Termux-TerminalEmulator:

  • sshj Shell → Output-Bytes in Reader-Thread → TerminalEmulator.process(bytes) → aktualisiert TerminalBuffer.
  • Tastatur-Eingabe → KeyEncoder (Pfeiltasten etc. → ESC-Sequenzen) → SshTerminalBridge.write() → sshj Shell-OutputStream.
  • TerminalEmulator direkt verwendet (nicht TerminalSession, das lokale Prozesse startet).
  • Renderer: eigene dünne Canvas-Composable, die TerminalBuffer rendert — orientiert an Termux' TerminalRenderer. Scope: monospaces Raster, 16/256-Farben-Palette, bold/italic/underline/inverse, Cursor, Scrollback, Alternate Screen. Deckt reale Server-Sessions (vim, htop, Shell) ab; exotisches (Sixel/Maus) bewusst out-of-scope.
  • IME-Anpassung (sichtbare Soft-Tastatur, Ctrl/Alt/Esc/Tastatur-Erweiterungsleiste) als Bestandteil.

UI-Screens (Compose)

  1. HostList — gruppierte Liste, Suche, Connect-Action, Swipe-to-delete; FAB „Host hinzufügen".
  2. HostEditor — Felder für Name/Host/Port/User, Auth-Auswahl (Passwort vs. Key-Auswahl), Group-Zuordnung, „Verbindung testen".
  3. KeyList — Keys mit Fingerprint/Typ, Import-Button, Löschen.
  4. KeyImport — Datei wählen (SAF), Name, Passphrase, Vorschau-Validierung.
  5. Terminal — Fullscreen Canvas-Terminal + Tastaturleiste; Connection-Status, Disconnect.

Build-Reihenfolge (Phasen)

  1. Scaffold: Gradle (Version Catalog libs.versions.toml), Manifest, Application (Hilt), MainActivity, Theme, Navigation-Gerüst.
  2. Daten & Crypto: Room-Entities/DAOs/DB, Repos, KeystoreCrypto, DI-Module.
  3. UI CRUD: HostList, HostEditor, KeyList, KeyImport (SAF + sshj-Parse + Verschlüsselung). Ab hier lauffähiger Manager.
  4. SSH-Layer: SshConnectionManager (sshj Connect + Auth Passwort/Key), „Verbindung testen".
  5. Terminal: Brücke + TerminalEmulator-Wiring + Canvas-Renderer + Tastatur. End-to-end SSH-Session im Terminal.
  6. Robustheit: Connection-Lifecycle im process-globalen Singleton (überlebt Konfigwechsel), Fehler-/Timeout-Handling, Später: Foreground-Service für echte Hintergrund-Persistenz (als Erweiterung markiert).

Ehrliche Hinweise / Risiken

  • Terminal-Renderer: komplette xterm-Fidelity ist kein Ziel; angestrebt ist ein robuster Renderer für typische Shell/vim/htop-Nutzung. Polarisierung (Unicode-Width, Truetype-Monospace-Messing) inkrementell.
  • Hintergrund-Persistenz echter Sessions braucht einen Foreground-Service (mit Notification) — im MVP wird die Session nur vor Config-Wechsel geschützt; der FG-Service ist klar als Folge-Task markiert.
  • Kein echtes 1Password: wird im UI ehrlich nicht suggeriert.

Was du am Ende hast

Ein lauffähiges Android-App-Projekt: Hosts/Keys verwalten (verschlüsselt gespeichert), über Passwort oder importierten Key verbinden, und ein interaktives Compose-Terminal für SSH-Sessions. Bauen mit ./gradlew assembleDebug.

Umsetzungshinweis

Ich lege das komplette Projekt als neues Gradle-Projekt an (es ist aktuell ein leeres Verzeichnis) und implementiere alle Phasen. Bei sehr umfangreichen einzelnen Klassen (Terminal-Renderer, SshConnectionManager) halte ich sie fokunktional, aber schlank und klar kommentiert.