Last Updated: 2026-05-02 (full re-verification pass against the codebase — every previously-listed audit item was diff'd against current code; entries that have actually shipped are now struck through with the verifying file:line citations)
Version: 0.0.9 (pinned via release.txt — DO NOT MODIFY without coordinated bump in app/build.gradle + F-Droid metadata)
Treat
CLAUDE.mdandFEATURES_AUDIT.mdas the authoritative state-of-the-app docs. This file tracks open issues + planned work that hasn't been implemented yet.
- Commits:
482c2a04("25 fixes") and the follow-on 6-feature batch. - 25-fix triage covered connect/reconnect lifecycle, notification system, cold-start ANR, per-tab same-host channels (Issue #163), on-screen keyboard ergonomics (
#161toggle removed,#162vim/tmux reorder), always-on keepalive (#166), repo cleanup (#160). - 6-feature follow-up: centralised error dialogs with Copy (
#167), edge-swipe tab switching (#168), tmux/screen auto-launch + postConnectScript wired (#170), modifier-aware hardware-keyboard arrows (#171), recordable macros (#173, DB v25 → v26), ViewStub-defer Active Sessions strip (#175). - DB now v26. New
macrostable for byte-exact recordable sequences (distinct from snippets, which carry typed text +{?var}substitutions).
- Commit:
ea4f687f572f("Added QR code support") - Spec:
AI.md§18 - Mobile QR-import flow lets the desktop client send connection profiles to the phone via a one-shot encrypted QR + 6-digit code.
- 5 new Kotlin files (~1,016 LOC) at
app/src/main/java/io/github/tabssh/pairing/:PairingPayload.kt(data classes),QrPayloadCodec.kt(hand-written CBOR codec, no library),PairingDecryptor.kt(Argon2id + AES-256-GCM),PairingImporter.kt(DB inserts with name-based dedupe), andImportFromQrActivity.kt(state-machine UI). - ZXing for the QR scanner (zero Google Play Services dependency, matching TabSSH's de-Googled-ROM stance).
tabssh-android-{arch}.apkbuilds verified compile-clean.
- Commit:
bed586fe45ec("Issue #36: ANR-on-update defenses") - Moved
initializeCoreComponents()+wireGlobalHostKeyCallbacks()into a backgroundapplicationScope.launch{}so lazy components (SecurePasswordManager,themeManager,keyStorage,sshSessionManager) initialise off the main thread instead of blocking it. - Tightened log file rotation: 10 MiB single-backup → 1 MiB × 5-file rotation. Total on-disk logs bounded at ~10 MiB across both debug + app logs; rotation is rename-only (microseconds vs the previous 10 MiB copy).
- Strict improvement — no regression if a main thread races and beats the scope.
- The actual ANR trace from a reproducing device is still useful future data, but the structural fixes here address the four most-plausible causes regardless.
- Commit:
05b7dac11642("Issue #37: SSH config RemoteCommand + SendEnv end-to-end") - DB v23 → v24 — new
connections.remote_commandcolumn. - Parser: explicit
RemoteCommand+SendEnvcases inSSHConfigParser.kt. SendEnv-derived names merge into the existingenvVarsfield asNAME=placeholders. - Connection layer:
SSHConnection.openShellChannel()now branches onprofile.remoteCommand. When set, opensChannelExecwithsetCommand(remoteCmd)+setPty(true); otherwise the existingChannelShellpath. Field type widened toChannel?(the JSch parentChannelSessionis package-private, so we dispatch on the concrete subclass for PTY-only methods). - Exporter: round-trips both directives. SendEnv vs SetEnv split — bare names → SendEnv, NAME=value → SetEnv.
- UX: new Spinner + conditional Custom EditText in
ConnectionEditActivity. 7 presets (Default — login shell /create(SourceForge) /sftp/internal-sftp/ tmux / screen / Custom…). On profile load, snaps to a matching preset verbatim or surfaces "Custom…" with the value pre-filled. - Fixes silent breakage for SourceForge
shell.sourceforge.net, forced-command="..."jails, gateway/menu hosts, SFTP-only accounts.
The audit findings below are historical; this section tracks status.
| Item | Status | Commit |
|---|---|---|
| P0 #1 backup encryption real | ✅ shipped | 2e4d9648 |
| P0 #2 hypervisor TLS — silent-bypass closed | ✅ shipped | 5a4b26f5 |
| P0 #2 hypervisor TLS — TOFU + change-detect cert pinning | ✅ shipped | DB v28 + crypto/tls/HypervisorTrustManagerFactory.kt + HypervisorCertPromptDialog.kt, wired into all 5 clients |
| Hypervisor reusable accounts (settled the "identity for hypervisors?" question) | ✅ shipped | DB v27 + HypervisorAccount entity / DAO / Activity, drawer entry |
| P1 Tasker IPC permission gate | ✅ shipped | 2e4d9648 |
| P1 HostKeyVerifier timeout/destroyed-activity | ✅ shipped | 5ac8f999 (now DIALOG_TIMEOUT_SECONDS=30 at HostKeyVerifier.kt:565) |
| P1 hypervisor passwords → Keystore | ✅ shipped | ae2c613a (HypervisorPasswordStore.resolveCredentials/store/clear/persistCapturedPinIfAny) |
| P1 WebSocket.send return ignored | ✅ shipped | ae2c613a (single-flight attemptSend + sendFailureFired) |
P1 profile.identityId!! NPE |
✅ shipped | 2e4d9648 (read once into local val at SSHConnection.kt:148) |
| MAC-failure root-cause (the actual disconnect bug) | ✅ shipped | bbf15665 (writeLock: Mutex at TermuxBridge.kt:86,141) |
| RECONNECT race that destroyed the activity | ✅ shipped | 1f25c29d (isReconnecting flag at TabTerminalActivity.kt:84,1796,1803) |
| Tasker preferences fragment | ✅ shipped | d714a7b4 (fragment at SettingsActivity.kt:605-697, IntentService consumes all 4 prefs) |
| advancedSettings JSON apply at connect | ✅ shipped | d714a7b4 (SSHConnection.applyAdvancedSettings for Local/Remote/Dynamic forwards) |
| X11 fixes batch | ✅ shipped | (this batch) Deleted orphan/broken X11ForwardingManager (437 LOC of stub in-app X server), refactored duplicated SSHConnection X11/agent setup into applyForwardingFlags, surfaced setup failures via onError listener, copied forwardX11/forwardAgent/compression/connectTimeout from imported ~/.ssh/config to entity columns (were previously dropped at parse time). |
Audit batch — widget tap crash (ConnectionWidgetProvider.kt:144 — connection.id.toInt() on UUID String) |
✅ shipped | this batch — threaded widgetId into getConnectIntent to use AppWidget's per-widget id as the PendingIntent request code (matches QuickConnectWidgetProvider). Crash fired on every widget tap. |
Audit batch — TabTerminalActivity TabManagerListener leak |
✅ shipped | this batch — listener moved from anonymous-inline to a tabManagerListener field, removed in onDestroy() before tabManager.cleanup(). Anonymous listener held implicit this@TabTerminalActivity; was preventing GC across reconnect cycles. |
Audit batch — TabSSHDatabase.exportDatabase() reading SQLite as text |
✅ shipped | this batch — deleted (was dead code, no callers, plus dbFile.readText() on a binary file would have produced corrupt output if anyone ever called it). |
Two read-only Explore-agent passes — feature-completeness vs. README + project tracker docs, and bug/security. Cited file:line locations are direct from the audit and verified for the P0 entries.
- Backup encryption is fake (Base64 only).
app/src/main/java/io/github/tabssh/backup/BackupManager.kt:268-285—encryptData()is justBase64.encodeToString(...),decryptData()isBase64.decode(...). The exported ZIP claims to be password-protected; it isn't. SSH keys, host-key fingerprints, and identity passwords all readable to anyone with the file. Fix: real AES-256-GCM with PBKDF2 (≥100k iterations) keyed off the user's backup password. ~3h. - Hypervisor TLS verification globally disabled by default.
verifySsl: Boolean = falseinhypervisor/proxmox/ProxmoxApiClient.kt:21,hypervisor/console/ConsoleWebSocketClient.kt:27, plus the matching XCP-ng / Xen Orchestra / VMware clients. The trust-allX509TrustManageraccepts any cert — including attacker-issued — for hypervisor REST + serial-console traffic. No per-host pin or CA store. Fix: per-host opt-in with cert pinning, or per-host CA bundle. ~6h (DB schema + UI).
- TaskerIntentService is
exported="true"with no permission gate.app/src/main/AndroidManifest.xml:278-289+automation/TaskerIntentService.kt. Any installed app can sendCONNECT/SEND_COMMAND/SEND_KEYSintents and drive arbitrary commands on the user's SSH targets. Fix: either setexported="false"(Tasker still works on most ROMs through alternate IPC) or require a customio.github.tabssh.permission.TASKERsignature-level permission. ~1h. - 8×
runBlocking(Dispatchers.IO)on the main thread insideHostKeyVerifier.check().ssh/HostKeyVerifier.kt:64,101,133,225,249,285,309,330. The CountDownLatch waits at lines 470, 546 have no timeout — an Activity destroyed mid-prompt → permanent worker-thread hang. Already triggers ANR risk on slow devices. Fix: convert to fully-async via callback; latch wait with 30s timeout default-rejecting on expiry. ~4h. - Hypervisor passwords stored as plaintext columns in
storage/database/entities/HypervisorProfile.kt:32-33. SSH passwords go throughSecurePasswordManager(Keystore-backed); hypervisor creds bypass it. Device backup or root → cleartext. Fix: route through SecurePasswordManager withhypervisor_${id}alias. ~2h. WebSocket.send()return value ignored in five places:hypervisor/console/ConsoleWebSocketClient.kt:149,254,309,335,344. Send-buffer-full or already-closed socket → user keystrokes silently dropped. Likely contributor to the VM-console disconnect symptom we already saw. Fix: check Boolean, surface failure to the UI / trigger reconnect. ~2h.profile.identityId!!atssh/connection/SSHConnection.kt:143. Identity row deleted between the null-guard at 141 and the bang at 143 → NPE. Fix:profile.identityId?.let { ... } ?: fallthrough. ~10min.
- Session passwords held in
mutableMapOf<String, String>for app lifetime —crypto/storage/SecurePasswordManager.kt:64. Cleared on explicitclearAllPasswords()(lines 409, 436, 448) but NOT on lifecycle events (pause/destroy/biometric-lock). Still open. Host-key dialog— VERIFIED FIXED.latch.await()no timeoutHostKeyVerifier.kt:520now useslatch.await(DIALOG_TIMEOUT_SECONDS, SECONDS)withDIALOG_TIMEOUT_SECONDS=30and a default-REJECT path on expiry (line 524).- DB query on
Dispatchers.Mainin widget update —widget/ConnectionWidgetProvider.kt:66. Cosmetic-only: Room's suspend DAO funcs (getConnectionById) dispatch their own IO regardless of the launching scope, so this isn't an actual main-thread DB hit. Worth tidying for clarity but not a correctness bug. Cosmetic / low priority. - Jump-host port-forward bind —
setPortForwardingL(0, profile.host, profile.port)atssh/connection/SSHConnection.kt:713. The 3-arg JSch overload defaults to127.0.0.1(not0.0.0.0), so this is safe in practice — but worth an explicitsetPortForwardingL("127.0.0.1", 0, host, port)for self-documentation and version pinning. Open (cosmetic). cachedPassword/cachedPassphraseheld asStringfor connection lifetime, never zeroed —ssh/connection/SSHConnection.kt:101,104. Same defense-in-depth shape as the SecurePasswordManager map. Still open.Host-key dialogs walk the context chain with no Activity guard— VERIFIED FIXED in commit5ac8f999.HostKeyVerifiernow resolves the activity viaTabSSHApplication.getCurrentActivity()and skips whenisFinishing || isDestroyed.- Logger key-bytes audit not yet performed — defensive grep across
Logger.[diwve]calls touchingbytes/key/pass/secretto confirm none print raw key material. Open (low-priority hygiene pass). - Translation drift:
values/strings.xmlhas 167 keys, each ofvalues-{es,fr,de}/strings.xmlhas 157. The 10 missing keys (cluster_progress,widget_*_description,sync_password_*,navigation_drawer_open/close,select_connection) silently fall back to base English at runtime — Android's standard locale-resolution behaviour. Accepted-known: needs a native-speaker translation pass before adding faux-translated stubs. - Hypervisor REST clients use
getJSONObjectrather thanoptJSONObject(ProxmoxApiClient.kt:84and similar across the four clients). Outer try/catch swallows the resultingJSONExceptionso it doesn't crash, but the user-facing error loses the actual API response shape. Accepted-known: defense-in-depth across ~20 call sites for marginal benefit; revisit if a real Proxmox/XO/etc. schema change actually fires opaque errors in practice.
Audit re-check (2026-05-02): several of the original claims here were stale by the time the audit ran. Verified-wired items are
struck throughbelow; only real gaps remain unmarked.
— VERIFIED WIRED as of 2026-05-02.encryptBackupUI promiseBackupManager.encryptDataat lines 273-285 routes throughSyncEncryptor(real AES-256-GCM + PBKDF2 100k iterations);decryptDatais forward-compatible and tolerates legacy Base64-only blobs for restoring pre-fix backups.Hypervisor TLS— VERIFIED WIRED as of 2026-05-02. DB v28 carriespinned_cert_sha256;crypto/tls/HypervisorTrustManagerFactory.installTrust(...)runs in all 5 clients (Proxmox/XCP-ng/XO/VMware/ConsoleWebSocketClient) implementing TOFU + change-detect viaHypervisorCertPromptDialog.HypervisorEditActivityshows the pinned fingerprint with a Forget button. TheverifySsl=falseswitch is now a deliberate per-host bypass, not the only feature.AWS / GCP / Azure cloud import — clients fully built— VERIFIED WIRED as of 2026-05-02.CloudAccountsActivityhas a drawer entry (drawer_menu.xml:44 nav_cloud_accounts) andMainActivitydispatches it. Audit was outdated.X11 toggle hidden— VERIFIED WIRED as of 2026-05-02. The switch is atactivity_connection_edit.xml:447with NOvisibility="gone", andConnectionEditActivityalready binds it (load at line 494, save at lines 685/766/797). Audit was outdated.SSH user-certificate auth— VERIFIED WIRED as of 2026-05-02.StoredKey.certificate(DB v19) is consumed atSSHConnection.kt:752-767viajsch.addIdentity(name, prvkey, pubkey=cert, passphrase).KeyManagementActivity.kt:424-433exposes paste/file pickers for attach/remove with-cert-v01@openssh.comvalidation. Audit was outdated.Snippet— VERIFIED WIRED as of 2026-05-02.{?var:default|hint}substitution UITabTerminalActivity.insertSnippetcallsshowVariablesDialog(line 2780) which builds an EditText pergetVariableSpecs()entry, with last-used recall insnippet_var_recallSharedPreferences. Audit was outdated.Recordable macros — zero UI— VERIFIED WIRED as of 2026-05-02. Record/replay flow exists inTabTerminalActivity(insertMacro at line 2259, getAllMacrosList + incrementUsageCount at 2284/2303). No dedicated CRUD activity yet, but the in-terminal flow is functional.- FIDO2 SSH signing —
crypto/fido/Fido2SshIdentity.kt:35-40throwsJSchException("FIDO2 SSH signing is alpha and not yet implemented"). JSch upstream doesn't supportsk-*key types; needs a JSch fork or alternate library. ~80h. Likely defer indefinitely. - Mosh full protocol —
protocols/mosh/MoshHandoff.kt:11-35only bootstraps the SSP exchange and returns a CLI string the user must paste into a real Mosh client. True transparent UDP/AES-128-OCB Mosh would be ~60h. Likely keep as handoff only — document accordingly. Tasker preferences XML orphaned— VERIFIED WIRED as of 2026-05-02.TaskerSettingsFragment(SettingsActivity.kt:605-697) inflates the XML and is reachable frompreferences_main.xml:46-50.TaskerIntentServicehonourstasker_enabled,tasker_require_unlock(KeyguardManager check),tasker_allowed_connections(whitelist),tasker_log_events, andtasker_command_timeout(default fallback when intent extra omitted).— WIRED as of 2026-05-02.advancedSettingsJSON apply at connectSSHConnection.applyAdvancedSettings(session)runs immediately after a successfulsession.connect()and applieslocalForwards,remoteForwards, anddynamicForwardsparsed from~/.ssh/config. Other directives (proxyJump/proxyCommand) already had their own paths; the extant gap was port forwards from imported configs being silently dropped.Xen Orchestra REST— MISLEADING AS WRITTEN (re-verified 2026-05-02). The TODO atTODO: Implement JSON parsingXenOrchestraApiClient.kt:300is on a genericparseJsonResponse<T>helper that is defined and never called (single grep hit at line 296). Concrete parsers ARE implemented for the methods that ship —listVMs(lines 365-393),getVM(lines 427-441), tags / OS-version helpers (parseJsonArray/parseJsonObjectat 643/655). If a future caller wants type-generic parsing, the helper has to be filled in then; the existing call sites all parse JSON concretely. Effective status: orphan dead code, can be deleted whenever someone passes through the file.— DELETED in commit cleanup batch 2026-05-02.activity_main_old.xmlis an orphan layout
- Status: 🔧 In progress (other instance — see
../desktop/.git/COMMIT_MESSPhase F line items) - Priority: MEDIUM
- Spec:
AI.md§18
The mobile decoder is in place and waiting for the desktop encoder + interop test vectors. The spec doc has the wire format, encryption parameters, payload schema, and CBOR field names that both sides must agree on.
- Status: Local/Remote/Dynamic forwards now apply at connect (
d714a7b4). Other directives still parsed → stored → ignored. - Priority: LOW (cosmetic — most users hit forwards first)
Re-verified 2026-05-02 against SSHConnection.applyAdvancedSettings:
| Directive | Parser | Stored | Applied at connect |
|---|---|---|---|
LocalForward / RemoteForward / DynamicForward |
✅ | JSON | ✅ as of d714a7b4 |
ProxyJump / ProxyCommand |
✅ | JSON | ❌ — ProxyJump should populate the existing proxy_host/proxy_port/proxy_username columns at parse time instead of living in JSON. ProxyCommand has no JSch equivalent and would require a custom Proxy impl. |
ServerAliveInterval / StrictHostKeyChecking |
✅ | JSON | ❌ — ServerAliveInterval is overridden by the mobile-default 60s keepalive (intentional). StrictHostKeyChecking is hardwired to "ask" because we own the dialog flow (intentional). Both can stay ignored. |
ForwardAgent / ForwardX11 |
✅ | JSON + columns | ✅ as of the X11 fixes batch — convertToConnectionProfile now copies host.forwardAgent/host.forwardX11 straight into agentForwarding/x11Forwarding columns. Same fix wired compression and connectTimeout while it was open. |
RequestTTY |
✅ | JSON | 🟡 — partly: when remoteCommand is set we always allocate a PTY (exec.setPty(true)), matching RequestTTY=yes. The force/no/auto distinctions aren't honored. |
Fix sketch (remaining): for ProxyJump, parse user@host:port and populate the existing proxy_host/proxy_port/proxy_username columns directly. The forward-agent/X11/compression/connect-timeout copy is already done.
Estimate: ~1 hour for the ProxyJump piece.
CLAUDE.md— project tracker, current state, recent wavesFEATURES_AUDIT.md— have/want/drop matrix vs JuiceSSH and Termiusfdroid-submission/SPEC.md— technical specification (architecture, schema, build)AI.md§18 — design spec for desktop→mobile QR pairing (folded in from the standaloneQR_PAIRING.md; the desktop project carries its own copy attabssh/desktop/QR_PAIRING.mdfor cross-repo reference)release.txt— single-line version pin, source of truth forversionName(currently0.0.9)