aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/Adb.java
diff options
context:
space:
mode:
authorLena <lena@omega>2026-01-01 00:00:00 +0000
committerLena <lena@omega>2026-06-24 22:14:50 +0300
commiteb0c8951196c637e44daf3c0617b131b997d5d2c (patch)
tree2f83b6a41cee745467e53fc401942b82cea286ea /app/src/main/java/invalid/lena/scrcpy/Adb.java
downloadscrcpy-android-0.1.tar.gz
scrcpy-android: mirror an Android device over wireless ADB0.1
Native Java app for Android 12+ that mirrors another Android device over wireless ADB, forwarding video, audio, touch input, and clipboard. Bundles a pinned scrcpy-server.jar and the vendored libadb-android stack. Supports h264/h265/av1 video and raw/opus audio with in-app codec selection. No NDK, no Kotlin. Includes JVM unit tests and a Docker-based emulator e2e rig.
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/Adb.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/Adb.java210
1 files changed, 210 insertions, 0 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/Adb.java b/app/src/main/java/invalid/lena/scrcpy/Adb.java
new file mode 100644
index 0000000..2909156
--- /dev/null
+++ b/app/src/main/java/invalid/lena/scrcpy/Adb.java
@@ -0,0 +1,210 @@
+package invalid.lena.scrcpy;
+
+import android.content.Context;
+
+import org.bouncycastle.asn1.ASN1EncodableVector;
+import org.bouncycastle.asn1.ASN1Encoding;
+import org.bouncycastle.asn1.ASN1Integer;
+import org.bouncycastle.asn1.DERBitString;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.asn1.x509.TBSCertificate;
+import org.bouncycastle.asn1.x509.Time;
+import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import io.github.muntashirakon.adb.AbsAdbConnectionManager;
+import io.github.muntashirakon.adb.AdbStream;
+import io.github.muntashirakon.adb.LocalServices;
+
+// Thin facade over libadb-android. One instance per process: pairs once,
+// connects per session, then exposes typed openers for the three streams
+// the rest of the app cares about (shell, sync, localabstract).
+//
+// The keypair lives in filesDir/{adbkey,adbcert}: PKCS#8 DER for the key,
+// X.509 DER for the cert. Software-backed RSA - AndroidKeyStore is unusable
+// here because libadb-android's auth path uses raw RSA/ECB/NoPadding which
+// hardware-backed keys refuse.
+public final class Adb extends AbsAdbConnectionManager {
+
+ private static final String DEVICE_NAME = "scrcpy-android";
+ private static final String KEY_FILE = "adbkey";
+ private static final String CERT_FILE = "adbcert";
+
+ // Per-operation timeout for adb protocol calls (pair/connect/openStream).
+ // libadb-android defaults to Long.MAX_VALUE; a hung target would block
+ // forever otherwise.
+ private static final long OP_TIMEOUT_MS = 15_000L;
+
+ // libadb-android's mApi is documented as "the target's API for protocol
+ // negotiation". We don't know the target's API at construction time;
+ // pinning to the current Android max means the protocol stays at its
+ // latest version, which is what wireless-debugging Android-11+ targets
+ // expect. Bump when newer protocol versions ship.
+ private static final int TARGET_API_HINT = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+
+ // Singleton: Main and Mirror both want an Adb; building two would
+ // race on the on-disk keypair and waste a RSA generation. Lazily
+ // initialised on the FIRST caller, which should be off the UI thread.
+ private static volatile Adb instance;
+
+ public static Adb getInstance(Context ctx) throws Exception {
+ Adb local = instance;
+ if (local != null) return local;
+ synchronized (Adb.class) {
+ if (instance == null) instance = new Adb(ctx.getApplicationContext());
+ return instance;
+ }
+ }
+
+ private final PrivateKey privateKey;
+ private final Certificate certificate;
+
+ private Adb(Context ctx) throws Exception {
+ setApi(TARGET_API_HINT);
+ setTimeout(OP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ File keyFile = new File(ctx.getFilesDir(), KEY_FILE);
+ File certFile = new File(ctx.getFilesDir(), CERT_FILE);
+ if (keyFile.exists() && certFile.exists()) {
+ privateKey = loadKey(keyFile);
+ certificate = loadCert(certFile);
+ Log.i("adb: loaded keypair from %s", keyFile);
+ } else {
+ KeyPair kp = generateRsa();
+ privateKey = kp.getPrivate();
+ certificate = selfSignedCert(kp);
+ saveKey(keyFile, privateKey);
+ saveCert(certFile, certificate);
+ Log.i("adb: generated new keypair at %s", keyFile);
+ }
+ }
+
+ @Override protected PrivateKey getPrivateKey() { return privateKey; }
+ @Override protected Certificate getCertificate() { return certificate; }
+ @Override protected String getDeviceName() { return DEVICE_NAME; }
+
+ // ---- typed openers ----
+
+ public AdbStream openAbstract(String name) throws IOException, InterruptedException {
+ return openStream(LocalServices.LOCAL_UNIX_SOCKET_ABSTRACT, name);
+ }
+
+ public AdbStream openShell(String cmd) throws IOException, InterruptedException {
+ // Bypass LocalServices.getDestination(SHELL, args) - it wraps any
+ // arg containing a space in literal double quotes, and adbd then
+ // tries to exec `"the whole quoted thing"` as a single filename.
+ // Build the destination ourselves so the cmd reaches sh -c
+ // unmolested.
+ return openStream("shell:" + cmd);
+ }
+
+ public AdbStream openSync() throws IOException, InterruptedException {
+ return openStream(LocalServices.SYNC);
+ }
+
+ // ---- key + cert I/O ----
+
+ private static KeyPair generateRsa() throws Exception {
+ KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
+ gen.initialize(2048, new SecureRandom());
+ return gen.generateKeyPair();
+ }
+
+ private static PrivateKey loadKey(File f) throws Exception {
+ byte[] data = Files.readAllBytes(f.toPath());
+ return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(data));
+ }
+
+ private static Certificate loadCert(File f) throws Exception {
+ try (InputStream in = new FileInputStream(f)) {
+ return CertificateFactory.getInstance("X.509").generateCertificate(in);
+ }
+ }
+
+ private static void saveKey(File f, PrivateKey k) throws IOException {
+ AtomicFiles.write(f, k.getEncoded());
+ }
+
+ private static void saveCert(File f, Certificate c) throws Exception {
+ AtomicFiles.write(f, c.getEncoded());
+ }
+
+ // Build a minimal self-signed X.509 certificate over the keypair.
+ //
+ // ASN.1 layout (RFC 5280 sec. 4.1):
+ //
+ // Certificate ::= SEQUENCE {
+ // tbsCertificate TBSCertificate,
+ // signatureAlgorithm AlgorithmIdentifier, -- sha256WithRSAEnc
+ // signature BIT STRING -- RSA over tbs
+ // }
+ //
+ // TBSCertificate ::= SEQUENCE {
+ // version [0] EXPLICIT v3,
+ // serialNumber INTEGER 1,
+ // signatureAlgorithm AlgorithmIdentifier,
+ // issuer = subject Name (CN=scrcpy-android),
+ // validity { notBefore, notAfter },
+ // subjectPublicKeyInfo SubjectPublicKeyInfo
+ // }
+ //
+ // ADB validates the peer by the raw RSA public-key fingerprint in the
+ // SPKI bits, not by the DN or signature, so the surrounding cert is
+ // cosmetic - but a valid X.509 wrapper is still required for the TLS
+ // handshake that wireless-debugging uses post-pairing.
+ private static Certificate selfSignedCert(KeyPair kp) throws Exception {
+ long now = System.currentTimeMillis();
+ Date notBefore = new Date(now - 60_000L);
+ Date notAfter = new Date(now + 50L * 365 * 24 * 3600 * 1000L);
+ X500Name dn = new X500Name("CN=" + DEVICE_NAME);
+ AlgorithmIdentifier sigAlg =
+ new AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption);
+
+ V3TBSCertificateGenerator g = new V3TBSCertificateGenerator();
+ g.setSerialNumber(new ASN1Integer(BigInteger.ONE));
+ g.setSignature(sigAlg);
+ g.setIssuer(dn);
+ g.setSubject(dn);
+ g.setStartDate(new Time(notBefore));
+ g.setEndDate(new Time(notAfter));
+ g.setSubjectPublicKeyInfo(
+ SubjectPublicKeyInfo.getInstance(kp.getPublic().getEncoded()));
+
+ TBSCertificate tbs = g.generateTBSCertificate();
+
+ Signature s = Signature.getInstance("SHA256withRSA");
+ s.initSign(kp.getPrivate());
+ s.update(tbs.getEncoded(ASN1Encoding.DER));
+ byte[] sig = s.sign();
+
+ ASN1EncodableVector v = new ASN1EncodableVector();
+ v.add(tbs);
+ v.add(sigAlg);
+ v.add(new DERBitString(sig));
+ byte[] certDer = new DERSequence(v).getEncoded(ASN1Encoding.DER);
+
+ return CertificateFactory.getInstance("X.509")
+ .generateCertificate(new ByteArrayInputStream(certDer));
+ }
+}