From 066e21a3f286d1a5c7a2517f1a103c1dd51a8b96 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Mon, 15 Jun 2026 13:34:19 +0200 Subject: [PATCH] some rssh stuff --- app/build.gradle | 3 ++ app/src/main/assets/licenses/combined.html | 3 +- .../remoteaccess/ReverseSshTunnelBridge.java | 2 +- .../remoteaccess/RsshLocalSshServer.java | 23 +++++----- .../remoteaccess/RsshSecuritySetup.java | 45 +++++++++++++++++++ .../backend/public/assets/js/remote_access.js | 30 ++++++++++++- 6 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/foxx/androidcast/remoteaccess/RsshSecuritySetup.java diff --git a/app/build.gradle b/app/build.gradle index 8b390e4..b9ccf1d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,7 @@ android { excludes += '/META-INF/LICENSE.txt' excludes += '/META-INF/NOTICE' excludes += '/META-INF/NOTICE.txt' + excludes += '/META-INF/versions/**' } } @@ -146,6 +147,8 @@ dependencies { implementation 'com.github.mwiede:jsch:0.2.21' implementation 'org.apache.sshd:sshd-core:2.14.0' + implementation 'org.apache.sshd:sshd-sftp:2.14.0' + implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'org.slf4j:slf4j-android:1.7.36' implementation project(':tunnel') diff --git a/app/src/main/assets/licenses/combined.html b/app/src/main/assets/licenses/combined.html index 785c741..a09de93 100644 --- a/app/src/main/assets/licenses/combined.html +++ b/app/src/main/assets/licenses/combined.html @@ -19,7 +19,8 @@
  • Google Play In-App Review — Google Play SDK terms
  • Eclipse Paho MQTT client — Eclipse Public License 1.0 and Eclipse Distribution License 1.0 (dual-licensed)
  • JSch (mwiede fork) — BSD 3-Clause License (reverse SSH remote access)
  • -
  • Apache MINA SSHD — Apache License 2.0 (embedded local SSH on device for RSSH alpha)
  • +
  • Apache MINA SSHD — Apache License 2.0 (embedded local SSH/SFTP for RSSH alpha)
  • +
  • Bouncy Castle — MIT-style license (SSHD crypto on Android)
  • JUnit 4

    diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java index 48572b2..c4bb929 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java @@ -42,7 +42,7 @@ public final class ReverseSshTunnelBridge { Log.w(TAG, "incomplete RSSH credentials"); return false; } - if (!RsshLocalSshServer.start(user, pass, localPort)) { + if (!RsshLocalSshServer.start(context, user, pass, localPort)) { Log.w(TAG, "local SSH on :" + localPort + " failed"); return false; } diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshLocalSshServer.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshLocalSshServer.java index 3cc3ce5..e3dd32a 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshLocalSshServer.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshLocalSshServer.java @@ -1,18 +1,19 @@ package com.foxx.androidcast.remoteaccess; +import android.content.Context; import android.util.Log; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.shell.ProcessShellFactory; +import org.apache.sshd.sftp.server.SftpSubsystemFactory; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; /** - * Embedded SSH on {@code 127.0.0.1:8022} for RSSH reverse forward target. + * Embedded SSH/SFTP on {@code 127.0.0.1:8022} for RSSH reverse forward target. * Operator reaches this via bastion forwarded port (same username/password). */ public final class RsshLocalSshServer { @@ -24,9 +25,10 @@ public final class RsshLocalSshServer { private RsshLocalSshServer() {} - public static boolean start(String username, String password, int port) { + public static boolean start(Context context, String username, String password, int port) { stop(); - if (username == null || username.isEmpty() || password == null || password.isEmpty()) { + if (context == null || username == null || username.isEmpty() + || password == null || password.isEmpty()) { return false; } if (port <= 0 || port > 65535) { @@ -36,8 +38,8 @@ public final class RsshLocalSshServer { final String pass = password; final int listenPort = port; try { - Path hostKey = Files.createTempFile("rssh_host", ".key"); - hostKey.toFile().deleteOnExit(); + RsshSecuritySetup.ensureReady(context); + java.nio.file.Path hostKey = context.getCacheDir().toPath().resolve("rssh_host.key"); SshServer sshd = SshServer.setUpDefaultServer(); sshd.setHost("127.0.0.1"); sshd.setPort(listenPort); @@ -45,14 +47,15 @@ public final class RsshLocalSshServer { sshd.setPasswordAuthenticator((PasswordAuthenticator) (u, p, session) -> user.equals(u) && pass.equals(p)); sshd.setShellFactory(new ProcessShellFactory("/system/bin/sh", "-")); + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); sshd.start(); server = sshd; activeUser = user; running.set(true); - Log.i(TAG, "local SSH listening 127.0.0.1:" + listenPort + " user=" + user); + Log.i(TAG, "local SSH/SFTP listening 127.0.0.1:" + listenPort + " user=" + user); return true; - } catch (Exception e) { - Log.w(TAG, "local SSH start failed: " + e.getMessage()); + } catch (Throwable e) { + Log.e(TAG, "local SSH start failed: " + e.getMessage(), e); stop(); return false; } diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshSecuritySetup.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshSecuritySetup.java new file mode 100644 index 0000000..c0b9ea7 --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RsshSecuritySetup.java @@ -0,0 +1,45 @@ +package com.foxx.androidcast.remoteaccess; + +import android.content.Context; +import android.util.Log; + +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.io.PathUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.nio.file.Path; +import java.security.Security; +import java.util.concurrent.atomic.AtomicBoolean; + +/** One-time MINA SSHD + BouncyCastle setup for Android (see mina-sshd docs/android.md). */ +final class RsshSecuritySetup { + private static final String TAG = "RsshSecurity"; + private static final AtomicBoolean READY = new AtomicBoolean(false); + + private RsshSecuritySetup() {} + + static void ensureReady(Context context) { + if (!READY.compareAndSet(false, true)) { + return; + } + Context app = context.getApplicationContext(); + try { + try { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } catch (Exception ignored) { + } + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + Path home = app.getFilesDir().toPath(); + System.setProperty("user.home", home.toString()); + PathUtils.setUserHomeFolderResolver(() -> home); + OsUtils.setCurrentUser(app.getPackageName()); + Log.i(TAG, "SSHD security providers ready; user.home=" + home); + } catch (Exception e) { + READY.set(false); + Log.e(TAG, "SSHD security setup failed: " + e.getMessage(), e); + throw new IllegalStateException("RSSH security setup failed", e); + } + } +} diff --git a/examples/crash_reporter/backend/public/assets/js/remote_access.js b/examples/crash_reporter/backend/public/assets/js/remote_access.js index b042d8c..b4aa098 100644 --- a/examples/crash_reporter/backend/public/assets/js/remote_access.js +++ b/examples/crash_reporter/backend/public/assets/js/remote_access.js @@ -518,8 +518,16 @@ const rowKey = 'ra-' + i; const briefId = 'ra-brief-' + rowKey; const wl = Number(d.whitelisted) === 1; - const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh'); - const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"'; + const optIn = d.opt_in_mode || 'none'; + const openReady = wl && (optIn === 'wireguard' || optIn === 'rssh'); + const openHint = !canOperate() + ? 'Need remote_access_operate permission' + : !wl + ? 'Whitelist device first' + : openReady + ? 'Open session — device connects on next poll (≤7 min)' + : 'Phone must poll with RSSH/WG enabled (dev settings on device; wait ≤7 min)'; + const openDisabled = canOperate() ? '' : ' disabled'; const wlDisabled = canAdmin() ? '' : ' disabled'; const isOpen = lastExpanded.has(String(d.device_id || '')); const rowClass = @@ -545,6 +553,8 @@ openDisabled + ' data-open-session="' + esc(d.device_id) + + '" title="' + + esc(openHint) + '">Open session ' + '