mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-20 11:33:06 +00:00
Remote input encryption changes for GFE 2.1.1
This commit is contained in:
parent
8f53b6f233
commit
ae8cb18f63
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user