diff --git a/moonlight-common/.classpath b/moonlight-common/.classpath index db1e0181..09191526 100644 --- a/moonlight-common/.classpath +++ b/moonlight-common/.classpath @@ -4,5 +4,6 @@ + diff --git a/moonlight-common/libs/commons-codec-1.9.jar b/moonlight-common/libs/commons-codec-1.9.jar new file mode 100644 index 00000000..ef35f1c5 Binary files /dev/null and b/moonlight-common/libs/commons-codec-1.9.jar differ diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/com/limelight/nvstream/NvConnection.java index db00627b..c44d92b6 100644 --- a/moonlight-common/src/com/limelight/nvstream/NvConnection.java +++ b/moonlight-common/src/com/limelight/nvstream/NvConnection.java @@ -5,11 +5,15 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; +import java.security.NoSuchAlgorithmException; import java.util.Enumeration; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + import org.xmlpull.v1.XmlPullParserException; import com.limelight.LimeLog; @@ -19,8 +23,10 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoStream; import com.limelight.nvstream.control.ControlStream; import com.limelight.nvstream.http.GfeHttpResponseException; +import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; +import com.limelight.nvstream.http.PairingManager; import com.limelight.nvstream.input.NvController; import com.limelight.nvstream.rtsp.RtspConnection; @@ -28,6 +34,7 @@ public class NvConnection { private String host; private NvConnectionListener listener; private StreamConfiguration config; + private LimelightCryptoProvider cryptoProvider; private InetAddress hostAddr; private ControlStream controlStream; @@ -41,19 +48,38 @@ public class NvConnection { private VideoDecoderRenderer videoDecoderRenderer; private AudioRenderer audioRenderer; private String localDeviceName; + private SecretKey riKey; private ThreadPoolExecutor threadPool; - public NvConnection(String host, NvConnectionListener listener, StreamConfiguration config) + public NvConnection(String host, NvConnectionListener listener, StreamConfiguration config, LimelightCryptoProvider cryptoProvider) { this.host = host; this.listener = listener; this.config = config; + this.cryptoProvider = cryptoProvider; + + try { + // This is unique per connection + this.riKey = generateRiAesKey(); + } catch (NoSuchAlgorithmException e) { + // Should never happen + e.printStackTrace(); + } this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue(), new ThreadPoolExecutor.DiscardPolicy()); } + private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + + // RI keys are 128 bits + keyGen.init(128); + + return keyGen.generateKey(); + } + public static String getMacAddressString() throws SocketException { Enumeration ifaceList; NetworkInterface selectedIface = null; @@ -136,24 +162,18 @@ public class NvConnection { private boolean startSteamBigPicture() throws XmlPullParserException, IOException { - NvHTTP h = new NvHTTP(hostAddr, getMacAddressString(), localDeviceName); + NvHTTP h = new NvHTTP(hostAddr, getMacAddressString(), localDeviceName, cryptoProvider); - if (h.getAppVersion().startsWith("1.")) { + if (h.getServerVersion().startsWith("1.")) { listener.displayMessage("Limelight now requires GeForce Experience 2.0.1 or later. Please upgrade GFE on your PC and try again."); return false; } - if (!h.getPairState()) { + if (h.getPairState() != PairingManager.PairState.PAIRED) { listener.displayMessage("Device not paired with computer"); return false; } - int sessionId = h.getSessionId(); - if (sessionId == 0) { - listener.displayMessage("Invalid session ID"); - return false; - } - NvApp app = h.getSteamApp(); if (app == null) { listener.displayMessage("Steam not found in GFE app list"); @@ -163,7 +183,7 @@ public class NvConnection { // If there's a game running, resume it if (h.getCurrentGame() != 0) { try { - if (!h.resumeApp()) { + if (!h.resumeApp(riKey)) { listener.displayMessage("Failed to resume existing session"); return false; } @@ -185,7 +205,7 @@ public class NvConnection { else { // Launch the app since it's not running int gameSessionId = h.launchApp(app.getAppId(), config.getWidth(), - config.getHeight(), config.getRefreshRate()); + config.getHeight(), config.getRefreshRate(), riKey); if (gameSessionId == 0) { listener.displayMessage("Failed to launch application"); return false; @@ -231,7 +251,7 @@ public class NvConnection { // it to the instance variable once the object is properly initialized. // This avoids the race where inputStream != null but inputStream.initialize() // has not returned yet. - NvController tempController = new NvController(hostAddr); + NvController tempController = new NvController(hostAddr, riKey); tempController.initialize(); inputStream = tempController; return true; diff --git a/moonlight-common/src/com/limelight/nvstream/http/LimelightCryptoProvider.java b/moonlight-common/src/com/limelight/nvstream/http/LimelightCryptoProvider.java new file mode 100644 index 00000000..e36a44b0 --- /dev/null +++ b/moonlight-common/src/com/limelight/nvstream/http/LimelightCryptoProvider.java @@ -0,0 +1,9 @@ +package com.limelight.nvstream.http; + +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; + +public interface LimelightCryptoProvider { + public X509Certificate getClientCertificate(); + public RSAPrivateKey getClientPrivateKey(); +} diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index 701ad31d..01c20af8 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -3,13 +3,20 @@ package com.limelight.nvstream.http; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.LinkedList; +import java.util.Scanner; import java.util.Stack; +import javax.crypto.SecretKey; + +import org.apache.commons.codec.binary.Base64; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -17,13 +24,14 @@ import org.xmlpull.v1.XmlPullParserFactory; public class NvHTTP { private String uniqueId; + private PairingManager pm; - public static final int PORT = 47989; + public static final int PORT = 47984; public static final int CONNECTION_TIMEOUT = 5000; public String baseUrl; - - public NvHTTP(InetAddress host, String uniqueId, String deviceName) { + + public NvHTTP(InetAddress host, String uniqueId, String deviceName, LimelightCryptoProvider cryptoProvider) { this.uniqueId = uniqueId; String safeAddress; @@ -35,16 +43,16 @@ public class NvHTTP { safeAddress = host.getHostAddress(); } - this.baseUrl = "http://" + safeAddress + ":" + PORT; + this.baseUrl = "https://" + safeAddress + ":" + PORT; + this.pm = new PairingManager(this, cryptoProvider); } - - private String getXmlString(InputStream in, String tagname) - throws XmlPullParserException, IOException { + + static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); XmlPullParser xpp = factory.newPullParser(); - xpp.setInput(new InputStreamReader(in)); + xpp.setInput(r); int eventType = xpp.getEventType(); Stack currentTag = new Stack(); @@ -70,8 +78,16 @@ public class NvHTTP { return null; } + + static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException { + return getXmlString(new StringReader(str), tagname); + } - private void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException { + static String getXmlString(InputStream in, String tagname) throws XmlPullParserException, IOException { + return getXmlString(new InputStreamReader(in), tagname); + } + + private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException { int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code")); if (statusCode != 200) { throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message")); @@ -80,31 +96,37 @@ public class NvHTTP { private InputStream openHttpConnection(String url) throws IOException { URLConnection conn = new URL(url).openConnection(); + System.out.println(conn.getURL()); conn.setConnectTimeout(CONNECTION_TIMEOUT); - conn.setDefaultUseCaches(false); + conn.setUseCaches(false); conn.connect(); return conn.getInputStream(); } + + String openHttpConnectionToString(String url) throws MalformedURLException, IOException { + Scanner s = new Scanner(openHttpConnection(url)); + + String str = ""; + while (s.hasNext()) { + str += s.next() + " "; + } + + s.close(); + System.out.println(str); + return str; + } - public String getAppVersion() throws XmlPullParserException, IOException { - InputStream in = openHttpConnection(baseUrl + "/appversion"); + public String getServerVersion() throws XmlPullParserException, IOException { + InputStream in = openHttpConnection(baseUrl + "/serverinfo?uniqueid=" + uniqueId); return getXmlString(in, "appversion"); } - public boolean getPairState() throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/pairstate?uniqueid=" + uniqueId); - String paired = getXmlString(in, "paired"); - return Integer.valueOf(paired) != 0; - } - - public int getSessionId() throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/pair?uniqueid=" + uniqueId); - String sessionId = getXmlString(in, "sessionid"); - return Integer.parseInt(sessionId); + public PairingManager.PairState getPairState() throws IOException, XmlPullParserException { + return pm.getPairState(uniqueId); } public int getCurrentGame() throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/serverinfo"); + InputStream in = openHttpConnection(baseUrl + "/serverinfo?uniqueid=" + uniqueId); String game = getXmlString(in, "currentgame"); return Integer.parseInt(game); } @@ -120,6 +142,10 @@ public class NvHTTP { return null; } + public PairingManager.PairState pair(String pin) throws Exception { + return pm.pair(uniqueId, pin); + } + public LinkedList getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException { InputStream in = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); @@ -161,18 +187,19 @@ public class NvHTTP { return appList; } - // Returns gameSession XML attribute - public int launchApp(int appId, int width, int height, int refreshRate) throws IOException, XmlPullParserException { + public int launchApp(int appId, int width, int height, int refreshRate, SecretKey inputKey) throws IOException, XmlPullParserException { InputStream in = openHttpConnection(baseUrl + "/launch?uniqueid=" + uniqueId + "&appid=" + appId + - "&mode=" + width + "x" + height + "x" + refreshRate); + "&mode=" + width + "x" + height + "x" + refreshRate + + "&additionalStates=1&sops=1&rikey="+Base64.encodeBase64String(inputKey.getEncoded())); String gameSession = getXmlString(in, "gamesession"); return Integer.parseInt(gameSession); } - public boolean resumeApp() throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId); + public boolean resumeApp(SecretKey inputKey) throws IOException, XmlPullParserException { + InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId + + "&rikey="+Base64.encodeBase64String(inputKey.getEncoded())); String resume = getXmlString(in, "resume"); return Integer.parseInt(resume) != 0; } diff --git a/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java new file mode 100644 index 00000000..b0a71dec --- /dev/null +++ b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java @@ -0,0 +1,325 @@ +package com.limelight.nvstream.http; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.*; + +import org.xmlpull.v1.XmlPullParserException; + +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.io.*; +import java.net.MalformedURLException; +import java.net.Socket; +import java.security.*; +import java.security.cert.*; +import java.util.Arrays; + +public class PairingManager { + + private NvHTTP http; + + private PrivateKey pk; + private X509Certificate cert; + private SecretKey aesKey; + + byte[] privKeyBytes; + byte[] pubKeyBytes; + + public enum PairState { + NOT_PAIRED, + PAIRED, + PIN_WRONG, + FAILED + } + + public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) { + this.http = http; + this.cert = cryptoProvider.getClientCertificate(); + this.pk = cryptoProvider.getClientPrivateKey(); + + // Update the trust manager and key manager to use our certificate and PK + installSslKeysAndTrust(); + } + + public void installSslKeysAndTrust() { + // Create a trust manager that does not validate certificate chains + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + }}; + + KeyManager[] ourKeyman = new KeyManager[] { + new X509KeyManager() { + public String chooseClientAlias(String[] keyTypes, + Principal[] issuers, Socket socket) { + return "Limelight-RSA"; + } + + public String chooseServerAlias(String keyType, Principal[] issuers, + Socket socket) { + return null; + } + + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] {cert}; + } + + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + public PrivateKey getPrivateKey(String alias) { + return pk; + } + + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + } + }; + + // Ignore differences between given hostname and certificate hostname + HostnameVerifier hv = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { return true; } + }; + + // Install the all-trusting trust manager + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(ourKeyman, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(hv); + } catch (Exception e) {} + } + + final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + private static byte[] hexToBytes(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException, CertificateException + { + String certText = NvHTTP.getXmlString(text, "plaincert"); + byte[] certBytes = hexToBytes(certText); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes)); + } + + private byte[] generateRandomBytes(int length) + { + byte[] rand = new byte[length]; + new SecureRandom().nextBytes(rand); + return rand; + } + + private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException { + byte[] saltedPin = new byte[salt.length + pin.length()]; + System.arraycopy(salt, 0, saltedPin, 0, salt.length); + System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length()); + return saltedPin; + } + + private static byte[] toSHA1Bytes(byte[] convertme) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-1"); + } + catch(NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return md.digest(convertme); + } + + private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(cert.getPublicKey()); + sig.update(data); + return sig.verify(signature); + } + + private static byte[] signData(byte[] data, PrivateKey key) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign(key); + sig.update(data); + byte[] signature = new byte[256]; + sig.sign(signature, 0, signature.length); + return signature; + } + + private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) throws NoSuchAlgorithmException, SignatureException, + InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException { + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + + int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16; + byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize); + byte[] fullDecrypted = new byte[blockRoundedSize]; + + cipher.init(Cipher.DECRYPT_MODE, secretKey); + cipher.doFinal(blockRoundedEncrypted, 0, + blockRoundedSize, fullDecrypted); + return fullDecrypted; + } + + private static byte[] encryptAes(byte[] data, SecretKey secretKey) throws NoSuchAlgorithmException, SignatureException, + InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException { + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + + int blockRoundedSize = ((data.length + 15) / 16) * 16; + byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize); + + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return cipher.doFinal(blockRoundedData); + } + + private static SecretKey generateAesKey(byte[] keyData) { + byte[] aesTruncated = Arrays.copyOf(toSHA1Bytes(keyData), 16); + System.out.println("AES key data: "+bytesToHex(aesTruncated)); + return new SecretKeySpec(aesTruncated, "AES"); + } + + private static byte[] concatBytes(byte[] a, byte[] b) { + byte[] c = new byte[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + public PairState getPairState(String uniqueId) throws MalformedURLException, IOException, XmlPullParserException { + String serverInfo = http.openHttpConnectionToString(http.baseUrl + "/serverinfo?uniqueid="+uniqueId); + if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) { + return PairState.NOT_PAIRED; + } + + String pairChallenge = http.openHttpConnectionToString(http.baseUrl + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=pairchallenge"); + if (NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) { + return PairState.PAIRED; + } + else { + return PairState.NOT_PAIRED; + } + } + + public PairState pair(String uniqueId, String pin) throws MalformedURLException, IOException, XmlPullParserException, CertificateException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException { + // Generate a salt for hashing the PIN + byte[] salt = generateRandomBytes(16); + System.out.println("Using salt: "+bytesToHex(salt)); + + // Combine the salt and pin, then create an AES key from them + byte[] saltAndPin = saltPin(salt, pin); + aesKey = generateAesKey(saltAndPin); + + // Send the salt and get the server cert + String getCert = http.openHttpConnectionToString(http.baseUrl + + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=getservercert&salt="+bytesToHex(salt)+"&clientcert="+bytesToHex(pubKeyBytes)); + if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) { + return PairState.FAILED; + } + X509Certificate serverCert = extractPlainCert(getCert); + System.out.println(serverCert); + + // Generate a random challenge and encrypt it with our AES key + byte[] randomChallenge = generateRandomBytes(16); + System.out.println("Unencrypted challenge: "+bytesToHex(randomChallenge)); + byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey); + System.out.println("Encrypted challenge: "+bytesToHex(encryptedChallenge)); + + // Send the encrypted challenge to the server + String challengeResp = http.openHttpConnectionToString(http.baseUrl + + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge)); + if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) { + return PairState.FAILED; + } + + // Decode the server's response and subsequent challenge + byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse")); + System.out.println("Encrypted challenge response: "+bytesToHex(encServerChallengeResponse)); + byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey); + System.out.println("Decrypted challenge response: "+bytesToHex(decServerChallengeResponse)); + + byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, 20); + byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, 20, 36); + System.out.println("Server response: "+bytesToHex(serverResponse)); + System.out.println("Server challenge: "+bytesToHex(serverChallenge)); + + // Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge + byte[] clientSecret = generateRandomBytes(16); + byte[] challengeRespHash = toSHA1Bytes(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret)); + System.out.println("Client challenge response hash: "+bytesToHex(challengeRespHash)); + byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey); + System.out.println("Client challenge response encrypted: "+bytesToHex(challengeRespEncrypted)); + String secretResp = http.openHttpConnectionToString(http.baseUrl + + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted)); + if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) { + return PairState.FAILED; + } + + // Get the server's signed secret + byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret")); + byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16); + byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272); + + // Ensure the authenticity of the data + if (!verifySignature(serverSecret, serverSignature, serverCert)) { + // Cancel the pairing process + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + + // Looks like a MITM + return PairState.FAILED; + } + + // Ensure the server challenge matched what we expected (aka the PIN was correct) + byte[] serverChallengeRespHash = toSHA1Bytes(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret)); + System.out.println("Re-calculated server challenge response hash: "+bytesToHex(serverChallengeRespHash)); + System.out.println("Original challenge response: "+bytesToHex(serverResponse)); + if (!Arrays.equals(serverChallengeRespHash, serverResponse)) { + // Cancel the pairing process + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + + // Probably got the wrong PIN + return PairState.PIN_WRONG; + } + + // Send the server our signed secret + byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk)); + System.out.println("Client pairing secret: "+bytesToHex(clientPairingSecret)); + String clientSecretResp = http.openHttpConnectionToString(http.baseUrl + + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret)); + if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) { + return PairState.FAILED; + } + + // Do the initial challenge (seems neccessary for us to show as paired) + String pairChallenge = http.openHttpConnectionToString(http.baseUrl + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=pairchallenge"); + if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) { + return PairState.FAILED; + } + + return PairState.PAIRED; + } +} diff --git a/moonlight-common/src/com/limelight/nvstream/input/NvController.java b/moonlight-common/src/com/limelight/nvstream/input/NvController.java index 28655a6e..2848ce04 100644 --- a/moonlight-common/src/com/limelight/nvstream/input/NvController.java +++ b/moonlight-common/src/com/limelight/nvstream/input/NvController.java @@ -5,6 +5,15 @@ import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; public class NvController { @@ -15,10 +24,27 @@ public class NvController { private InetAddress host; private Socket s; private OutputStream out; + private Cipher riCipher; + - public NvController(InetAddress host) + private final static byte[] ENCRYPTED_HEADER = new byte[] {0x00, 0x00, 0x00, 0x20}; + + public NvController(InetAddress host, SecretKey riKey) { this.host = host; + try { + // This cipher is guaranteed to be supported + this.riCipher = Cipher.getInstance("AES/CBC/NoPadding"); + this.riCipher.init(Cipher.ENCRYPT_MODE, riKey, new IvParameterSpec(new byte[16])); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (NoSuchPaddingException e) { + e.printStackTrace(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (InvalidAlgorithmParameterException e) { + e.printStackTrace(); + } } public void initialize() throws IOException @@ -36,36 +62,51 @@ public class NvController { } catch (IOException e) {} } + private byte[] encryptAesInputData(byte[] data) throws Exception { + // Input data is rounded to units of 32 bytes + byte[] blockRoundedData = Arrays.copyOf(data, 32); + return riCipher.update(blockRoundedData); + } + + private void sendPacket(InputPacket packet) throws IOException { + out.write(ENCRYPTED_HEADER); + byte[] encryptedInput; + try { + encryptedInput = encryptAesInputData(packet.toWire()); + } catch (Exception e) { + // Should never happen + e.printStackTrace(); + return; + } + out.write(encryptedInput); + out.flush(); + } + public void sendControllerInput(short buttonFlags, byte leftTrigger, byte rightTrigger, short leftStickX, short leftStickY, short rightStickX, short rightStickY) throws IOException { - out.write(new ControllerPacket(buttonFlags, leftTrigger, + sendPacket(new ControllerPacket(buttonFlags, leftTrigger, rightTrigger, leftStickX, leftStickY, - rightStickX, rightStickY).toWire()); - out.flush(); + rightStickX, rightStickY)); } public void sendMouseButtonDown(byte mouseButton) throws IOException { - out.write(new MouseButtonPacket(true, mouseButton).toWire()); - out.flush(); + sendPacket(new MouseButtonPacket(true, mouseButton)); } public void sendMouseButtonUp(byte mouseButton) throws IOException { - out.write(new MouseButtonPacket(false, mouseButton).toWire()); - out.flush(); + sendPacket(new MouseButtonPacket(false, mouseButton)); } public void sendMouseMove(short deltaX, short deltaY) throws IOException { - out.write(new MouseMovePacket(deltaX, deltaY).toWire()); - out.flush(); + sendPacket(new MouseMovePacket(deltaX, deltaY)); } public void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier) throws IOException { - out.write(new KeyboardPacket(keyMap, keyDirection, modifier).toWire()); - out.flush(); + sendPacket(new KeyboardPacket(keyMap, keyDirection, modifier)); } }