2323import android .os .Looper ;
2424import android .os .PowerManager ;
2525import android .provider .Settings ;
26- import android .security .keystore .KeyGenParameterSpec ;
27- import android .security .keystore .KeyProperties ;
2826import android .util .DisplayMetrics ;
2927import android .util .Log ;
3028import android .view .Display ;
3836import org .json .JSONArray ;
3937import org .json .JSONObject ;
4038
39+ import java .io .ByteArrayInputStream ;
4140import java .io .InputStream ;
4241import java .io .OutputStream ;
4342import java .io .BufferedReader ;
4443import java .io .InputStreamReader ;
45- import java .net .HttpURLConnection ;
4644import java .net .InetAddress ;
4745import java .net .NetworkInterface ;
4846import java .net .ServerSocket ;
4947import java .net .Socket ;
5048import java .net .URL ;
51- import java .math .BigInteger ;
5249import java .nio .charset .StandardCharsets ;
5350import java .security .KeyFactory ;
5451import java .security .KeyPairGenerator ;
5855import java .security .SecureRandom ;
5956import java .security .Signature ;
6057import java .security .spec .PKCS8EncodedKeySpec ;
61- import java .util .Date ;
6258import java .util .LinkedHashSet ;
6359import java .util .Locale ;
6460import java .util .UUID ;
6561import javax .net .ssl .HostnameVerifier ;
6662import javax .net .ssl .HttpsURLConnection ;
6763import javax .net .ssl .KeyManagerFactory ;
6864import javax .net .ssl .SSLContext ;
65+ import javax .net .ssl .SSLServerSocket ;
6966import javax .net .ssl .SSLServerSocketFactory ;
7067import javax .net .ssl .SSLSession ;
68+ import javax .net .ssl .SSLSocket ;
7169import javax .net .ssl .TrustManager ;
7270import javax .net .ssl .X509TrustManager ;
73- import javax .security .auth .x500 .X500Principal ;
7471import java .security .cert .X509Certificate ;
7572
7673public 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