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.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@ -47,6 +48,7 @@ public class NvConnection {
private AudioRenderer audioRenderer;
private String localDeviceName;
private SecretKey riKey;
private int riKeyId;
private ThreadPoolExecutor threadPool;
@ -66,6 +68,8 @@ public class NvConnection {
e.printStackTrace();
}
this.riKeyId = generateRiKeyId();
this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(), new ThreadPoolExecutor.DiscardPolicy());
}
@ -79,6 +83,10 @@ public class NvConnection {
return keyGen.generateKey();
}
private static int generateRiKeyId() {
return new SecureRandom().nextInt();
}
public void stop()
{
threadPool.shutdownNow();
@ -104,8 +112,8 @@ public class NvConnection {
{
NvHTTP h = new NvHTTP(hostAddr, uniqueId, localDeviceName, cryptoProvider);
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.");
if (!h.getServerVersion().startsWith("3.")) {
listener.displayMessage("Limelight now requires GeForce Experience 2.1.1 or later. Please upgrade GFE on your PC and try again.");
return false;
}
@ -123,7 +131,7 @@ public class NvConnection {
// If there's a game running, resume it
if (h.getCurrentGame() != 0) {
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");
return false;
} else if (h.getCurrentGame() != app.getAppId()) {
@ -169,7 +177,8 @@ public class NvConnection {
throws IOException, XmlPullParserException {
// Launch the app since it's not running
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) {
listener.displayMessage("Failed to launch application");
return false;
@ -213,7 +222,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, riKey);
NvController tempController = new NvController(hostAddr, riKey, riKeyId);
tempController.initialize();
inputStream = tempController;
return true;

View File

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

View File

@ -5,6 +5,7 @@ import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@ -25,17 +26,18 @@ public class NvController {
private Socket s;
private OutputStream out;
private Cipher riCipher;
private final static byte[] ENCRYPTED_HEADER = new byte[] {0x00, 0x00, 0x00, 0x20};
public NvController(InetAddress host, SecretKey riKey)
public NvController(InetAddress host, SecretKey riKey, int riKeyId)
{
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]));
ByteBuffer bb = ByteBuffer.allocate(16);
bb.putInt(riKeyId);
this.riCipher.init(Cipher.ENCRYPT_MODE, riKey, new IvParameterSpec(bb.array()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
@ -62,23 +64,51 @@ public class NvController {
} 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 {
// Input data is rounded to units of 32 bytes
byte[] blockRoundedData = Arrays.copyOf(data, 32);
return riCipher.update(blockRoundedData);
return riCipher.update(padData(data));
}
private void sendPacket(InputPacket packet) throws IOException {
out.write(ENCRYPTED_HEADER);
byte[] encryptedInput;
byte[] toWire = packet.toWire();
// 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 {
encryptedInput = encryptAesInputData(packet.toWire());
bb.put(encryptAesInputData(toWire));
} catch (Exception e) {
// Should never happen
e.printStackTrace();
return;
}
out.write(encryptedInput);
// Send the packet
out.write(bb.array());
out.flush();
}