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));
}
}