Remote input encryption changes for GFE 2.1.1

This commit is contained in:
Cameron Gutman 2014-07-31 00:22:17 -07:00
parent 8f53b6f233
commit ae8cb18f63
3 changed files with 73 additions and 23 deletions

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
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;
@ -47,6 +48,7 @@ public class NvConnection {
private AudioRenderer audioRenderer; private AudioRenderer audioRenderer;
private String localDeviceName; private String localDeviceName;
private SecretKey riKey; private SecretKey riKey;
private int riKeyId;
private ThreadPoolExecutor threadPool; private ThreadPoolExecutor threadPool;
@ -66,6 +68,8 @@ public class NvConnection {
e.printStackTrace(); e.printStackTrace();
} }
this.riKeyId = generateRiKeyId();
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());
} }
@ -79,6 +83,10 @@ public class NvConnection {
return keyGen.generateKey(); return keyGen.generateKey();
} }
private static int generateRiKeyId() {
return new SecureRandom().nextInt();
}
public void stop() public void stop()
{ {
threadPool.shutdownNow(); threadPool.shutdownNow();
@ -104,8 +112,8 @@ public class NvConnection {
{ {
NvHTTP h = new NvHTTP(hostAddr, uniqueId, localDeviceName, cryptoProvider); NvHTTP h = new NvHTTP(hostAddr, uniqueId, localDeviceName, cryptoProvider);
if (h.getServerVersion().startsWith("1.")) { if (!h.getServerVersion().startsWith("3.")) {
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.1.1 or later. Please upgrade GFE on your PC and try again.");
return false; return false;
} }
@ -123,7 +131,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.getCurrentGame() == app.getAppId() && !h.resumeApp(riKey)) { if (h.getCurrentGame() == app.getAppId() && !h.resumeApp(riKey, riKeyId)) {
listener.displayMessage("Failed to resume existing session"); listener.displayMessage("Failed to resume existing session");
return false; return false;
} else if (h.getCurrentGame() != app.getAppId()) { } else if (h.getCurrentGame() != app.getAppId()) {
@ -169,7 +177,8 @@ public class NvConnection {
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
// 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(), riKey, config.getSops()); config.getHeight(), config.getRefreshRate(), riKey, config.getSops(),
riKeyId);
if (gameSessionId == 0) { if (gameSessionId == 0) {
listener.displayMessage("Failed to launch application"); listener.displayMessage("Failed to launch application");
return false; return false;
@ -213,7 +222,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, riKey); NvController tempController = new NvController(hostAddr, riKey, riKeyId);
tempController.initialize(); tempController.initialize();
inputStream = tempController; inputStream = tempController;
return true; return true;

View File

@ -27,7 +27,6 @@ import com.limelight.nvstream.http.PairingManager.PairState;
public class NvHTTP { public class NvHTTP {
private String uniqueId; private String uniqueId;
private PairingManager pm; private PairingManager pm;
private LimelightCryptoProvider cryptoProvider;
private InetAddress address; private InetAddress address;
public static final int PORT = 47984; public static final int PORT = 47984;
@ -39,7 +38,6 @@ public class NvHTTP {
public NvHTTP(InetAddress host, String uniqueId, String deviceName, LimelightCryptoProvider cryptoProvider) { public NvHTTP(InetAddress host, String uniqueId, String deviceName, LimelightCryptoProvider cryptoProvider) {
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
this.cryptoProvider = cryptoProvider;
this.address = host; this.address = host;
String safeAddress; String safeAddress;
@ -252,20 +250,33 @@ public class NvHTTP {
openHttpConnection(baseUrl + "/unpair?uniqueid=" + uniqueId); openHttpConnection(baseUrl + "/unpair?uniqueid=" + uniqueId);
} }
public int launchApp(int appId, int width, int height, int refreshRate, SecretKey inputKey, boolean sops) throws IOException, XmlPullParserException { 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);
}
public int launchApp(int appId, int width, int height, int refreshRate, SecretKey inputKey, boolean sops, int riKeyId) 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=" + (sops ? 1 : 0) + "&additionalStates=1&sops=" + (sops ? 1 : 0) +
"&rikey="+cryptoProvider.encodeBase64String(inputKey.getEncoded())); "&rikey="+bytesToHex(inputKey.getEncoded()) +
"&rikeyid="+riKeyId);
String gameSession = getXmlString(in, "gamesession"); String gameSession = getXmlString(in, "gamesession");
return Integer.parseInt(gameSession); return Integer.parseInt(gameSession);
} }
public boolean resumeApp(SecretKey inputKey) throws IOException, XmlPullParserException { public boolean resumeApp(SecretKey inputKey, int riKeyId) throws IOException, XmlPullParserException {
InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId + InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId +
"&rikey="+cryptoProvider.encodeBase64String(inputKey.getEncoded())); "&rikey="+bytesToHex(inputKey.getEncoded()) +
"&rikeyid="+riKeyId);
String resume = getXmlString(in, "resume"); String resume = getXmlString(in, "resume");
return Integer.parseInt(resume) != 0; return Integer.parseInt(resume) != 0;
} }

View File

@ -5,6 +5,7 @@ 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.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -26,16 +27,17 @@ public class NvController {
private OutputStream out; private OutputStream out;
private Cipher riCipher; private Cipher riCipher;
public NvController(InetAddress host, SecretKey riKey, int riKeyId)
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 { try {
// This cipher is guaranteed to be supported // This cipher is guaranteed to be supported
this.riCipher = Cipher.getInstance("AES/CBC/NoPadding"); this.riCipher = Cipher.getInstance("AES/CBC/NoPadding");
this.riCipher.init(Cipher.ENCRYPT_MODE, riKey, new IvParameterSpec(new byte[16]));
ByteBuffer bb = ByteBuffer.allocate(16);
bb.putInt(riKeyId);
this.riCipher.init(Cipher.ENCRYPT_MODE, riKey, new IvParameterSpec(bb.array()));
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
e.printStackTrace(); e.printStackTrace();
} catch (NoSuchPaddingException e) { } catch (NoSuchPaddingException e) {
@ -62,23 +64,51 @@ public class NvController {
} catch (IOException e) {} } catch (IOException e) {}
} }
private static int getPaddedSize(int length) {
return ((length + 15) / 16) * 16;
}
private static byte[] padData(byte[] data) {
// This implements the PKCS7 padding algorithm
if ((data.length % 16) == 0) {
// Already a multiple of 16
return data;
}
byte[] padded = Arrays.copyOf(data, getPaddedSize(data.length));
byte paddingByte = (byte)(16 - (data.length % 16));
for (int i = data.length; i < padded.length; i++) {
padded[i] = paddingByte;
}
return padded;
}
private byte[] encryptAesInputData(byte[] data) throws Exception { private byte[] encryptAesInputData(byte[] data) throws Exception {
// Input data is rounded to units of 32 bytes return riCipher.update(padData(data));
byte[] blockRoundedData = Arrays.copyOf(data, 32);
return riCipher.update(blockRoundedData);
} }
private void sendPacket(InputPacket packet) throws IOException { private void sendPacket(InputPacket packet) throws IOException {
out.write(ENCRYPTED_HEADER); byte[] toWire = packet.toWire();
byte[] encryptedInput;
// Pad to 16 byte chunks
int paddedLength = getPaddedSize(toWire.length);
// Allocate a byte buffer to represent the final packet
ByteBuffer bb = ByteBuffer.allocate(4 + paddedLength);
bb.putInt(paddedLength);
try { try {
encryptedInput = encryptAesInputData(packet.toWire()); bb.put(encryptAesInputData(toWire));
} catch (Exception e) { } catch (Exception e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return; return;
} }
out.write(encryptedInput);
// Send the packet
out.write(bb.array());
out.flush(); out.flush();
} }