1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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));
}
}
|