Add support for secure pairing and input encryption to fix GFE 2.1 compatibility.

TODO:
Needs a LimelightCryptoProvider implementation for each platform to work.
Untested (and probably broken) on Android.
Needs more testing in general, especially in corner cases.
This commit is contained in:
Cameron Gutman 2014-06-15 04:40:47 -07:00
parent 4a2ee91700
commit 07cf96c5ce
7 changed files with 476 additions and 53 deletions

View File

@ -4,5 +4,6 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="libs/xpp3-1.1.4c.jar"/> <classpathentry kind="lib" path="libs/xpp3-1.1.4c.jar"/>
<classpathentry kind="lib" path="libs/tinyrtsp.jar"/> <classpathentry kind="lib" path="libs/tinyrtsp.jar"/>
<classpathentry kind="lib" path="libs/commons-codec-1.9.jar"/>
<classpathentry kind="output" path="bin"/> <classpathentry kind="output" path="bin"/>
</classpath> </classpath>

Binary file not shown.

View File

@ -5,11 +5,15 @@ import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.SocketException; import java.net.SocketException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import com.limelight.LimeLog; 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.av.video.VideoStream;
import com.limelight.nvstream.control.ControlStream; import com.limelight.nvstream.control.ControlStream;
import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.input.NvController; import com.limelight.nvstream.input.NvController;
import com.limelight.nvstream.rtsp.RtspConnection; import com.limelight.nvstream.rtsp.RtspConnection;
@ -28,6 +34,7 @@ public class NvConnection {
private String host; private String host;
private NvConnectionListener listener; private NvConnectionListener listener;
private StreamConfiguration config; private StreamConfiguration config;
private LimelightCryptoProvider cryptoProvider;
private InetAddress hostAddr; private InetAddress hostAddr;
private ControlStream controlStream; private ControlStream controlStream;
@ -41,19 +48,38 @@ public class NvConnection {
private VideoDecoderRenderer videoDecoderRenderer; private VideoDecoderRenderer videoDecoderRenderer;
private AudioRenderer audioRenderer; private AudioRenderer audioRenderer;
private String localDeviceName; private String localDeviceName;
private SecretKey riKey;
private ThreadPoolExecutor threadPool; 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.host = host;
this.listener = listener; this.listener = listener;
this.config = config; 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, this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(), new ThreadPoolExecutor.DiscardPolicy()); new LinkedBlockingQueue<Runnable>(), 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 { public static String getMacAddressString() throws SocketException {
Enumeration<NetworkInterface> ifaceList; Enumeration<NetworkInterface> ifaceList;
NetworkInterface selectedIface = null; NetworkInterface selectedIface = null;
@ -136,24 +162,18 @@ public class NvConnection {
private boolean startSteamBigPicture() throws XmlPullParserException, IOException 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."); listener.displayMessage("Limelight now requires GeForce Experience 2.0.1 or later. Please upgrade GFE on your PC and try again.");
return false; return false;
} }
if (!h.getPairState()) { if (h.getPairState() != PairingManager.PairState.PAIRED) {
listener.displayMessage("Device not paired with computer"); listener.displayMessage("Device not paired with computer");
return false; return false;
} }
int sessionId = h.getSessionId();
if (sessionId == 0) {
listener.displayMessage("Invalid session ID");
return false;
}
NvApp app = h.getSteamApp(); NvApp app = h.getSteamApp();
if (app == null) { if (app == null) {
listener.displayMessage("Steam not found in GFE app list"); 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 there's a game running, resume it
if (h.getCurrentGame() != 0) { if (h.getCurrentGame() != 0) {
try { try {
if (!h.resumeApp()) { if (!h.resumeApp(riKey)) {
listener.displayMessage("Failed to resume existing session"); listener.displayMessage("Failed to resume existing session");
return false; return false;
} }
@ -185,7 +205,7 @@ public class NvConnection {
else { else {
// Launch the app since it's not running // Launch the app since it's not running
int gameSessionId = h.launchApp(app.getAppId(), config.getWidth(), int gameSessionId = h.launchApp(app.getAppId(), config.getWidth(),
config.getHeight(), config.getRefreshRate()); config.getHeight(), config.getRefreshRate(), riKey);
if (gameSessionId == 0) { if (gameSessionId == 0) {
listener.displayMessage("Failed to launch application"); listener.displayMessage("Failed to launch application");
return false; return false;
@ -231,7 +251,7 @@ public class NvConnection {
// it to the instance variable once the object is properly initialized. // it to the instance variable once the object is properly initialized.
// This avoids the race where inputStream != null but inputStream.initialize() // This avoids the race where inputStream != null but inputStream.initialize()
// has not returned yet. // has not returned yet.
NvController tempController = new NvController(hostAddr); NvController tempController = new NvController(hostAddr, riKey);
tempController.initialize(); tempController.initialize();
inputStream = tempController; inputStream = tempController;
return true; return true;

View File

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

View File

@ -3,13 +3,20 @@ package com.limelight.nvstream.http;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Scanner;
import java.util.Stack; import java.util.Stack;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base64;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
@ -17,13 +24,14 @@ import org.xmlpull.v1.XmlPullParserFactory;
public class NvHTTP { public class NvHTTP {
private String uniqueId; 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 static final int CONNECTION_TIMEOUT = 5000;
public String baseUrl; public String baseUrl;
public NvHTTP(InetAddress host, String uniqueId, String deviceName) { public NvHTTP(InetAddress host, String uniqueId, String deviceName, LimelightCryptoProvider cryptoProvider) {
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
String safeAddress; String safeAddress;
@ -35,16 +43,16 @@ public class NvHTTP {
safeAddress = host.getHostAddress(); 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) static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException {
throws XmlPullParserException, IOException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true); factory.setNamespaceAware(true);
XmlPullParser xpp = factory.newPullParser(); XmlPullParser xpp = factory.newPullParser();
xpp.setInput(new InputStreamReader(in)); xpp.setInput(r);
int eventType = xpp.getEventType(); int eventType = xpp.getEventType();
Stack<String> currentTag = new Stack<String>(); Stack<String> currentTag = new Stack<String>();
@ -71,7 +79,15 @@ public class NvHTTP {
return null; return null;
} }
private void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException { static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException {
return getXmlString(new StringReader(str), tagname);
}
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")); int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
if (statusCode != 200) { if (statusCode != 200) {
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message")); 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 { private InputStream openHttpConnection(String url) throws IOException {
URLConnection conn = new URL(url).openConnection(); URLConnection conn = new URL(url).openConnection();
System.out.println(conn.getURL());
conn.setConnectTimeout(CONNECTION_TIMEOUT); conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setDefaultUseCaches(false); conn.setUseCaches(false);
conn.connect(); conn.connect();
return conn.getInputStream(); return conn.getInputStream();
} }
public String getAppVersion() throws XmlPullParserException, IOException { String openHttpConnectionToString(String url) throws MalformedURLException, IOException {
InputStream in = openHttpConnection(baseUrl + "/appversion"); Scanner s = new Scanner(openHttpConnection(url));
String str = "";
while (s.hasNext()) {
str += s.next() + " ";
}
s.close();
System.out.println(str);
return str;
}
public String getServerVersion() throws XmlPullParserException, IOException {
InputStream in = openHttpConnection(baseUrl + "/serverinfo?uniqueid=" + uniqueId);
return getXmlString(in, "appversion"); return getXmlString(in, "appversion");
} }
public boolean getPairState() throws IOException, XmlPullParserException { public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
InputStream in = openHttpConnection(baseUrl + "/pairstate?uniqueid=" + uniqueId); return pm.getPairState(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 int getCurrentGame() throws IOException, XmlPullParserException { public int getCurrentGame() throws IOException, XmlPullParserException {
InputStream in = openHttpConnection(baseUrl + "/serverinfo"); InputStream in = openHttpConnection(baseUrl + "/serverinfo?uniqueid=" + uniqueId);
String game = getXmlString(in, "currentgame"); String game = getXmlString(in, "currentgame");
return Integer.parseInt(game); return Integer.parseInt(game);
} }
@ -120,6 +142,10 @@ public class NvHTTP {
return null; return null;
} }
public PairingManager.PairState pair(String pin) throws Exception {
return pm.pair(uniqueId, pin);
}
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException { public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
InputStream in = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId); InputStream in = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
@ -161,18 +187,19 @@ public class NvHTTP {
return appList; return appList;
} }
// Returns gameSession XML attribute public int launchApp(int appId, int width, int height, int refreshRate, SecretKey inputKey) throws IOException, XmlPullParserException {
public int launchApp(int appId, int width, int height, int refreshRate) throws IOException, XmlPullParserException {
InputStream in = openHttpConnection(baseUrl + InputStream in = openHttpConnection(baseUrl +
"/launch?uniqueid=" + uniqueId + "/launch?uniqueid=" + uniqueId +
"&appid=" + appId + "&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"); String gameSession = getXmlString(in, "gamesession");
return Integer.parseInt(gameSession); return Integer.parseInt(gameSession);
} }
public boolean resumeApp() throws IOException, XmlPullParserException { public boolean resumeApp(SecretKey inputKey) throws IOException, XmlPullParserException {
InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId); InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId +
"&rikey="+Base64.encodeBase64String(inputKey.getEncoded()));
String resume = getXmlString(in, "resume"); String resume = getXmlString(in, "resume");
return Integer.parseInt(resume) != 0; return Integer.parseInt(resume) != 0;
} }

View File

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

View File

@ -5,6 +5,15 @@ import java.io.OutputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; 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 { public class NvController {
@ -15,10 +24,27 @@ public class NvController {
private InetAddress host; private InetAddress host;
private Socket s; private Socket s;
private OutputStream out; 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; 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 public void initialize() throws IOException
@ -36,36 +62,51 @@ public class NvController {
} catch (IOException e) {} } 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, public void sendControllerInput(short buttonFlags, byte leftTrigger, byte rightTrigger,
short leftStickX, short leftStickY, short rightStickX, short rightStickY) throws IOException short leftStickX, short leftStickY, short rightStickX, short rightStickY) throws IOException
{ {
out.write(new ControllerPacket(buttonFlags, leftTrigger, sendPacket(new ControllerPacket(buttonFlags, leftTrigger,
rightTrigger, leftStickX, leftStickY, rightTrigger, leftStickX, leftStickY,
rightStickX, rightStickY).toWire()); rightStickX, rightStickY));
out.flush();
} }
public void sendMouseButtonDown(byte mouseButton) throws IOException public void sendMouseButtonDown(byte mouseButton) throws IOException
{ {
out.write(new MouseButtonPacket(true, mouseButton).toWire()); sendPacket(new MouseButtonPacket(true, mouseButton));
out.flush();
} }
public void sendMouseButtonUp(byte mouseButton) throws IOException public void sendMouseButtonUp(byte mouseButton) throws IOException
{ {
out.write(new MouseButtonPacket(false, mouseButton).toWire()); sendPacket(new MouseButtonPacket(false, mouseButton));
out.flush();
} }
public void sendMouseMove(short deltaX, short deltaY) throws IOException public void sendMouseMove(short deltaX, short deltaY) throws IOException
{ {
out.write(new MouseMovePacket(deltaX, deltaY).toWire()); sendPacket(new MouseMovePacket(deltaX, deltaY));
out.flush();
} }
public void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier) throws IOException public void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier) throws IOException
{ {
out.write(new KeyboardPacket(keyMap, keyDirection, modifier).toWire()); sendPacket(new KeyboardPacket(keyMap, keyDirection, modifier));
out.flush();
} }
} }