Skip to content

Commit 919216b

Browse files
author
Codex
committed
Fix backend-initiated companion pairing
1 parent e371412 commit 919216b

4 files changed

Lines changed: 624 additions & 218 deletions

File tree

jetkvm-companion/src/com/jetkvm/companion/CompanionService.java

Lines changed: 55 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import android.os.Looper;
2424
import android.os.PowerManager;
2525
import android.provider.Settings;
26-
import android.security.keystore.KeyGenParameterSpec;
27-
import android.security.keystore.KeyProperties;
2826
import android.util.DisplayMetrics;
2927
import android.util.Log;
3028
import android.view.Display;
@@ -38,17 +36,16 @@
3836
import org.json.JSONArray;
3937
import org.json.JSONObject;
4038

39+
import java.io.ByteArrayInputStream;
4140
import java.io.InputStream;
4241
import java.io.OutputStream;
4342
import java.io.BufferedReader;
4443
import java.io.InputStreamReader;
45-
import java.net.HttpURLConnection;
4644
import java.net.InetAddress;
4745
import java.net.NetworkInterface;
4846
import java.net.ServerSocket;
4947
import java.net.Socket;
5048
import java.net.URL;
51-
import java.math.BigInteger;
5249
import java.nio.charset.StandardCharsets;
5350
import java.security.KeyFactory;
5451
import java.security.KeyPairGenerator;
@@ -58,19 +55,19 @@
5855
import java.security.SecureRandom;
5956
import java.security.Signature;
6057
import java.security.spec.PKCS8EncodedKeySpec;
61-
import java.util.Date;
6258
import java.util.LinkedHashSet;
6359
import java.util.Locale;
6460
import java.util.UUID;
6561
import javax.net.ssl.HostnameVerifier;
6662
import javax.net.ssl.HttpsURLConnection;
6763
import javax.net.ssl.KeyManagerFactory;
6864
import javax.net.ssl.SSLContext;
65+
import javax.net.ssl.SSLServerSocket;
6966
import javax.net.ssl.SSLServerSocketFactory;
7067
import javax.net.ssl.SSLSession;
68+
import javax.net.ssl.SSLSocket;
7169
import javax.net.ssl.TrustManager;
7270
import javax.net.ssl.X509TrustManager;
73-
import javax.security.auth.x500.X500Principal;
7471
import java.security.cert.X509Certificate;
7572

7673
public class CompanionService extends Service implements InputManager.InputDeviceListener {
@@ -81,16 +78,19 @@ public class CompanionService extends Service implements InputManager.InputDevic
8178
static final String KEY_JETKVM_URL = "jetkvm_url";
8279
static final String KEY_JETKVM_URLS = "jetkvm_urls";
8380
static final String KEY_JETKVM_PAIRINGS = "jetkvm_pairings";
81+
static final String KEY_PENDING_PAIR_URL = "pending_pair_url";
82+
static final String KEY_PENDING_PAIR_CREATED_AT = "pending_pair_created_at";
8483
static final String DEFAULT_JETKVM_URL = "https://jetkvm.local";
8584
static final String EXTRA_JETKVM_URL = "jetkvm_url";
86-
static final String EXTRA_PAIR_REQUEST_ID = "pair_request_id";
85+
static final String ACTION_PAIR_REQUEST_UPDATED = "com.jetkvm.companion.PAIR_REQUEST_UPDATED";
8786

8887
private static final String CHANNEL_ID = "jetkvm-companion";
8988
private static final int NOTIFICATION_ID = 1001;
90-
private static final int PAIRING_NOTIFICATION_ID = 1002;
9189
private static final int NOTIFICATION_RESPAWN_BASE_ID = 1100;
9290
private static final int PAIRING_LISTEN_PORT = 8787;
93-
private static final String PAIRING_TLS_KEY_ALIAS = "jetkvm-companion-pairing-listener";
91+
private static final char[] PAIRING_TLS_KEYSTORE_PASSWORD = "jetkvm-pairing".toCharArray();
92+
private static final String PAIRING_TLS_PKCS12_BASE64 =
93+
"MIIErQIBAzCCBGMGCSqGSIb3DQEHAaCCBFQEggRQMIIETDCCAsoGCSqGSIb3DQEHBqCCArswggK3AgEAMIICsAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBAwxVAx3n9iprRMGxOVTnRcAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ5v0SWOS0Y9RgSUC9zVIDmICCAkD4oroylTCG9IGFbvvNQo8oL+ZktHuHmsK0ympLsKaiba0kqnyUDVHPIQCPkbqpzLvbVxK2v3XvqOW6uIDnO6WuGQ4SgJIHwtjKEfYE2nwRXzJAxG3K3W4rJcMojAOaH693FOFIJzAHGhfvBJJA91vzcoPbg54/8JQw2p9fxTUbUC3Oear/9uQV5zXO0gkA76YWakuLStdXE1V/DkHCq900J3OTla1d4FlIIc/6T30j4JmnLFBfsC42miNMYH9si6YiaqPk7kR4AAyzSGHLdT7nEUqLQYbFTodDGrpRON3uQdqoF2DV7jo/m7uoLkh/cqKyzLp6gBDP5PUemdvVV4NARUkVN+6k4y5nJdjERPUByu9sXvsVDXvlwtRpchcLTPA4Lu7csdusCJheyuXtU6AjkhkQ1bZodjhLwmYhUK9TWZy1Fc+0/xptp54aS0BT1zMZlNlN0h7QU1f7qfCK56IO8ElUh69yfx5n2we41/uZyJEMCgZAWhbSFhLe1LjPYjHjOipw2xkJpm6Q4jN6gANxacQQ86MK1TqP1bzArbBTMKKu/X2uLvd9GTVnZvPnqafMlzw8iEcnaKnRG2J3EmTL6VgEPIPNYwajbjV5ClVJd7OtoXx5NU2CyWRbwcc3wehQuJdLEfaHunPfv06zmCJ20p2bhOZmTRMFDJpnEM6SpVXxmpFObjJQ40jziuxFGR7O1LrNIYr+NdDUL0wL3eJeCiw26oi3+H3f3GfOGEkbyN1Uz31CPPxJpHT5eCfe2xkwggF6BgkqhkiG9w0BBwGgggFrBIIBZzCCAWMwggFfBgsqhkiG9w0BDAoBAqCB9zCB9DBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQnjQKS2/2mS7/VtWfmlHqHgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEFyZrGeNQzflyuU01xMpV7gEgZCrgV4LouyEmAWy0YBYmPmyA+TqOXViN93lcBaJ/3VkWjhp6RIiaSx6PNCt7Gm2LXOjMBQ7U6atpRD7QBjSn+EfJv7CKxVXm6Q+kzk7yrx/cyUuP35vchCJM5x+2V1yUQ3kGJzUjNEwaUelHd4WTOwhrCwFU5nXvDyu28IniGD/rYuH5eTzcgrlX8hrke/seCcxVjAjBgkqhkiG9w0BCRUxFgQUnipi/wwZbTQzZhlNJ18M5wfnnz8wLwYJKoZIhvcNAQkUMSIeIABwAGEAaQByAGkAbgBnAC0AbABpAHMAdABlAG4AZQByMEEwMTANBglghkgBZQMEAgEFAAQgbIjNG8GU9ctDrstkzRq+YyFrBF/IZ3gjpVi75eIMoVgECKUudDl7lnsAAgIIAA==";
9494
private static final long SCREEN_ON_DISMISS_DELAY_MS = 600;
9595
private static final long TARGET_REPORT_INTERVAL_MS = 15000;
9696
private static final long TARGET_LEASE_MS = 120000;
@@ -437,13 +437,10 @@ private void saveJetKvmUrlFromIntent(Intent intent) {
437437
}
438438

439439
private void postTargetDeclaration(String baseUrl, boolean connected, int width, int height, JetKvmPeripheralSnapshot snapshot) {
440-
HttpURLConnection conn = null;
440+
HttpsURLConnection conn = null;
441441
try {
442-
String trimmedBaseUrl = baseUrl == null ? "" : baseUrl.trim();
442+
String trimmedBaseUrl = normalizeJetKvmUrl(baseUrl);
443443
if (trimmedBaseUrl.length() == 0) trimmedBaseUrl = DEFAULT_JETKVM_URL;
444-
while (trimmedBaseUrl.endsWith("/")) {
445-
trimmedBaseUrl = trimmedBaseUrl.substring(0, trimmedBaseUrl.length() - 1);
446-
}
447444

448445
URL url = new URL(trimmedBaseUrl + "/companion/target");
449446
SharedPreferences prefs = getCompanionPreferences(this);
@@ -530,7 +527,7 @@ public void run() {
530527
});
531528
}
532529

533-
private static String readResponseBody(HttpURLConnection conn) throws Exception {
530+
private static String readResponseBody(HttpsURLConnection conn) throws Exception {
534531
InputStream stream = conn.getInputStream();
535532
if (stream == null) return "";
536533
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
@@ -811,18 +808,18 @@ static String[] getVisibleLocalIPs() {
811808
return ips.toArray(new String[ips.size()]);
812809
}
813810

814-
static HttpURLConnection openTrustedConnection(URL url) throws Exception {
815-
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
816-
if (conn instanceof HttpsURLConnection) {
817-
HttpsURLConnection https = (HttpsURLConnection) conn;
818-
https.setSSLSocketFactory(trustAllSslContext().getSocketFactory());
819-
https.setHostnameVerifier(new HostnameVerifier() {
820-
@Override
821-
public boolean verify(String hostname, SSLSession session) {
822-
return true;
823-
}
824-
});
811+
static HttpsURLConnection openTrustedConnection(URL url) throws Exception {
812+
if (!"https".equalsIgnoreCase(url.getProtocol())) {
813+
throw new IllegalArgumentException("JetKVM communication requires HTTPS");
825814
}
815+
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
816+
conn.setSSLSocketFactory(trustAllSslContext().getSocketFactory());
817+
conn.setHostnameVerifier(new HostnameVerifier() {
818+
@Override
819+
public boolean verify(String hostname, SSLSession session) {
820+
return true;
821+
}
822+
});
826823
return conn;
827824
}
828825

@@ -871,7 +868,7 @@ private static String joinUrls(LinkedHashSet<String> urls) {
871868
return builder.toString();
872869
}
873870

874-
static void applyCompanionSignatureHeaders(HttpURLConnection conn, String method, String path, byte[] bodyBytes, CompanionPairing pairing) throws Exception {
871+
static void applyCompanionSignatureHeaders(HttpsURLConnection conn, String method, String path, byte[] bodyBytes, CompanionPairing pairing) throws Exception {
875872
String timestamp = java.time.Instant.now().toString();
876873
String nonce = UUID.randomUUID().toString() + "-" + Long.toHexString(new SecureRandom().nextLong());
877874
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(bodyBytes));
@@ -1191,7 +1188,7 @@ public void run() {
11911188
handlePairingRequestSocket(pairingServerSocket.accept());
11921189
}
11931190
} catch (Exception e) {
1194-
Log.w(TAG, "pairing request listener stopped: " + e.getClass().getSimpleName());
1191+
Log.w(TAG, "pairing request listener stopped", e);
11951192
}
11961193
}
11971194
}, "JetKVM-pair-listener");
@@ -1213,41 +1210,31 @@ private void stopPairingRequestServer() {
12131210
}
12141211

12151212
private ServerSocket createPairingTLSServerSocket() throws Exception {
1216-
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
1217-
keyStore.load(null);
1218-
if (!keyStore.containsAlias(PAIRING_TLS_KEY_ALIAS)) {
1219-
KeyPairGenerator generator = KeyPairGenerator.getInstance(
1220-
KeyProperties.KEY_ALGORITHM_RSA,
1221-
"AndroidKeyStore"
1222-
);
1223-
generator.initialize(new KeyGenParameterSpec.Builder(
1224-
PAIRING_TLS_KEY_ALIAS,
1225-
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_DECRYPT
1226-
)
1227-
.setKeySize(2048)
1228-
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
1229-
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
1230-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
1231-
.setCertificateSubject(new X500Principal("CN=JetKVM Companion"))
1232-
.setCertificateSerialNumber(BigInteger.ONE)
1233-
.setCertificateNotBefore(new Date(System.currentTimeMillis() - 86400000L))
1234-
.setCertificateNotAfter(new Date(System.currentTimeMillis() + 315360000000L))
1235-
.build());
1236-
generator.generateKeyPair();
1237-
}
1213+
KeyStore keyStore = KeyStore.getInstance("PKCS12");
1214+
byte[] keyStoreBytes = android.util.Base64.decode(PAIRING_TLS_PKCS12_BASE64, android.util.Base64.DEFAULT);
1215+
keyStore.load(new ByteArrayInputStream(keyStoreBytes), PAIRING_TLS_KEYSTORE_PASSWORD);
12381216

12391217
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
12401218
KeyManagerFactory.getDefaultAlgorithm()
12411219
);
1242-
keyManagerFactory.init(keyStore, null);
1220+
keyManagerFactory.init(keyStore, PAIRING_TLS_KEYSTORE_PASSWORD);
12431221
SSLContext context = SSLContext.getInstance("TLS");
12441222
context.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
12451223
SSLServerSocketFactory factory = context.getServerSocketFactory();
1246-
return factory.createServerSocket(PAIRING_LISTEN_PORT);
1224+
SSLServerSocket socket = (SSLServerSocket) factory.createServerSocket(PAIRING_LISTEN_PORT);
1225+
socket.setUseClientMode(false);
1226+
socket.setNeedClientAuth(false);
1227+
socket.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" });
1228+
return socket;
12471229
}
12481230

12491231
private void handlePairingRequestSocket(Socket socket) {
12501232
try {
1233+
if (socket instanceof SSLSocket) {
1234+
SSLSocket sslSocket = (SSLSocket) socket;
1235+
sslSocket.setUseClientMode(false);
1236+
sslSocket.startHandshake();
1237+
}
12511238
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
12521239
String requestLine = reader.readLine();
12531240
int contentLength = 0;
@@ -1268,12 +1255,13 @@ private void handlePairingRequestSocket(Socket socket) {
12681255

12691256
String body = new String(chars, 0, read);
12701257
String jetkvmUrl = extractJsonString(body, "jetkvm_url");
1258+
String normalizedJetKvmUrl = normalizeJetKvmUrl(jetkvmUrl);
12711259
String requestId = extractJsonString(body, "request_id");
12721260
if (requestLine != null
12731261
&& requestLine.startsWith("POST /pair/request ")
1274-
&& jetkvmUrl.length() > 0
1262+
&& normalizedJetKvmUrl.length() > 0
12751263
&& requestId.length() > 0) {
1276-
showPairingRequestNotification(jetkvmUrl, requestId);
1264+
savePendingPairRequest(normalizedJetKvmUrl);
12771265
writePairingServerResponse(socket, 202, "{\"status\":\"pending\"}");
12781266
} else if (requestLine != null && requestLine.startsWith("POST /pair/unpair ")) {
12791267
int removed = removePairingsFromAdminUnpair(body);
@@ -1282,7 +1270,7 @@ private void handlePairingRequestSocket(Socket socket) {
12821270
writePairingServerResponse(socket, 400, "{\"error\":\"invalid pairing request\"}");
12831271
}
12841272
} catch (Exception e) {
1285-
Log.w(TAG, "pairing request failed: " + e.getClass().getSimpleName());
1273+
Log.w(TAG, "pairing request failed", e);
12861274
} finally {
12871275
try {
12881276
socket.close();
@@ -1324,6 +1312,17 @@ public void run() {
13241312
return removed;
13251313
}
13261314

1315+
private void savePendingPairRequest(String jetkvmUrl) {
1316+
SharedPreferences prefs = getCompanionPreferences(this);
1317+
prefs.edit()
1318+
.putString(KEY_PENDING_PAIR_URL, jetkvmUrl)
1319+
.putLong(KEY_PENDING_PAIR_CREATED_AT, System.currentTimeMillis())
1320+
.apply();
1321+
Intent intent = new Intent(ACTION_PAIR_REQUEST_UPDATED);
1322+
intent.setPackage(getPackageName());
1323+
sendBroadcast(intent);
1324+
}
1325+
13271326
private void writePairingServerResponse(Socket socket, int status, String body) throws java.io.IOException {
13281327
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
13291328
String reason = status == 202 ? "Accepted" : status == 200 ? "OK" : "Bad Request";
@@ -1333,32 +1332,6 @@ private void writePairingServerResponse(Socket socket, int status, String body)
13331332
out.flush();
13341333
}
13351334

1336-
private void showPairingRequestNotification(String jetkvmUrl, String requestId) {
1337-
Intent intent = new Intent(this, MainActivity.class);
1338-
intent.putExtra(EXTRA_JETKVM_URL, jetkvmUrl);
1339-
intent.putExtra(EXTRA_PAIR_REQUEST_ID, requestId);
1340-
PendingIntent pendingIntent = PendingIntent.getActivity(
1341-
this,
1342-
PAIRING_NOTIFICATION_ID,
1343-
intent,
1344-
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
1345-
);
1346-
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= 26
1347-
? new Notification.Builder(this, CHANNEL_ID)
1348-
: new Notification.Builder(this);
1349-
Notification notification = builder
1350-
.setSmallIcon(getApplicationInfo().icon)
1351-
.setContentTitle("JetKVM pairing request")
1352-
.setContentText("Open companion to pair with " + jetkvmUrl)
1353-
.setContentIntent(pendingIntent)
1354-
.setAutoCancel(true)
1355-
.build();
1356-
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
1357-
if (manager != null) {
1358-
manager.notify(PAIRING_NOTIFICATION_ID, notification);
1359-
}
1360-
}
1361-
13621335
private static String extractJsonString(String json, String key) {
13631336
if (json == null || key == null) return "";
13641337
String needle = "\"" + key + "\"";
@@ -1535,4 +1508,5 @@ public String toString() {
15351508
+ " present=" + present;
15361509
}
15371510
}
1511+
15381512
}

0 commit comments

Comments
 (0)