diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/com/limelight/nvstream/NvConnection.java index 0f8ef5fb..23c16dc7 100644 --- a/moonlight-common/src/com/limelight/nvstream/NvConnection.java +++ b/moonlight-common/src/com/limelight/nvstream/NvConnection.java @@ -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(), 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; diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index 9fdb2176..b70352e7 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -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; } diff --git a/moonlight-common/src/com/limelight/nvstream/input/NvController.java b/moonlight-common/src/com/limelight/nvstream/input/NvController.java index 2848ce04..9e28f976 100644 --- a/moonlight-common/src/com/limelight/nvstream/input/NvController.java +++ b/moonlight-common/src/com/limelight/nvstream/input/NvController.java @@ -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(); }