WHC Shell: keyboard-first Android terminal over SSH to local Termux

Vendored Termux terminal-emulator/renderer (Apache 2.0) with kitty keyboard
protocol, modifyOtherKeys and any-event mouse tracking; sshj client with
Ed25519 key login and TOFU host keys; Termux font/colors/cursor sync;
mouse selection, context menu, zoom and fullscreen shortcuts.
This commit is contained in:
WHC
2026-10-06 01:51:31 +08:00
commit a0023fd2ae
37 changed files with 7840 additions and 0 deletions
@@ -0,0 +1,69 @@
package xyz.whcself.whcshell;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters;
import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil;
import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.SecureRandom;
import java.util.Base64;
/** The client's Ed25519 identity, stored in OpenSSH format in a private directory. */
final class SshKeys {
final File privateKey;
final File publicKey;
private SshKeys(File dir) {
privateKey = new File(dir, "id_ed25519");
publicKey = new File(dir, "id_ed25519.pub");
}
/** Loads the key pair from {@code dir}, generating it first if missing. */
static SshKeys ensure(File dir, String comment) throws IOException {
SshKeys k = new SshKeys(dir);
if (k.privateKey.isFile() && k.publicKey.isFile()) return k;
if (!dir.isDirectory() && !dir.mkdirs()) throw new IOException("cannot create " + dir);
Ed25519KeyPairGenerator gen = new Ed25519KeyPairGenerator();
gen.init(new Ed25519KeyGenerationParameters(new SecureRandom()));
AsymmetricCipherKeyPair pair = gen.generateKeyPair();
Base64.Encoder b64 = Base64.getEncoder();
String priv = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
+ Base64.getMimeEncoder(70, new byte[]{'\n'}).encodeToString(
OpenSSHPrivateKeyUtil.encodePrivateKey(pair.getPrivate()))
+ "\n-----END OPENSSH PRIVATE KEY-----\n";
String pub = "ssh-ed25519 "
+ b64.encodeToString(OpenSSHPublicKeyUtil.encodePublicKey(pair.getPublic()))
+ " " + comment.replaceAll("[^A-Za-z0-9._@-]", "_") + "\n";
write(k.privateKey, priv);
write(k.publicKey, pub);
return k;
}
/** The single authorized_keys line, without a trailing newline. */
String publicLine() throws IOException {
return new String(Files.readAllBytes(publicKey.toPath()), StandardCharsets.UTF_8).trim();
}
private static void write(File f, String s) throws IOException {
File tmp = new File(f.getPath() + ".tmp");
try (FileOutputStream out = new FileOutputStream(tmp)) {
out.write(s.getBytes(StandardCharsets.UTF_8));
out.getFD().sync();
}
tmp.setReadable(false, false);
tmp.setReadable(true, true);
tmp.setWritable(false, false);
tmp.setWritable(true, true);
if (!tmp.renameTo(f)) throw new IOException("cannot rename " + tmp);
}
}