mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-21 12:03:02 +00:00
Fix input encryption on Gen 7 servers
This commit is contained in:
parent
8b395bb29f
commit
faa0cba39d
@ -28,6 +28,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
private static final int IDX_START_B = 1;
|
private static final int IDX_START_B = 1;
|
||||||
private static final int IDX_INVALIDATE_REF_FRAMES = 2;
|
private static final int IDX_INVALIDATE_REF_FRAMES = 2;
|
||||||
private static final int IDX_LOSS_STATS = 3;
|
private static final int IDX_LOSS_STATS = 3;
|
||||||
|
private static final int IDX_INPUT_DATA = 5;
|
||||||
|
|
||||||
private static final short packetTypesGen3[] = {
|
private static final short packetTypesGen3[] = {
|
||||||
0x140b, // Start A
|
0x140b, // Start A
|
||||||
@ -35,6 +36,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
0x1404, // Invalidate reference frames
|
0x1404, // Invalidate reference frames
|
||||||
0x140c, // Loss Stats
|
0x140c, // Loss Stats
|
||||||
0x1417, // Frame Stats (unused)
|
0x1417, // Frame Stats (unused)
|
||||||
|
-1, // Input data (unused)
|
||||||
};
|
};
|
||||||
private static final short packetTypesGen4[] = {
|
private static final short packetTypesGen4[] = {
|
||||||
0x0606, // Request IDR frame
|
0x0606, // Request IDR frame
|
||||||
@ -42,6 +44,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
0x0604, // Invalidate reference frames
|
0x0604, // Invalidate reference frames
|
||||||
0x060a, // Loss Stats
|
0x060a, // Loss Stats
|
||||||
0x0611, // Frame Stats (unused)
|
0x0611, // Frame Stats (unused)
|
||||||
|
-1, // Input data (unused)
|
||||||
};
|
};
|
||||||
private static final short packetTypesGen5[] = {
|
private static final short packetTypesGen5[] = {
|
||||||
0x0305, // Start A
|
0x0305, // Start A
|
||||||
@ -49,6 +52,15 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
0x0301, // Invalidate reference frames
|
0x0301, // Invalidate reference frames
|
||||||
0x0201, // Loss Stats
|
0x0201, // Loss Stats
|
||||||
0x0204, // Frame Stats (unused)
|
0x0204, // Frame Stats (unused)
|
||||||
|
0x0207, // Input data
|
||||||
|
};
|
||||||
|
private static final short packetTypesGen7[] = {
|
||||||
|
0x0305, // Start A
|
||||||
|
0x0307, // Start B
|
||||||
|
0x0301, // Invalidate reference frames
|
||||||
|
0x0201, // Loss Stats
|
||||||
|
0x0204, // Frame Stats (unused)
|
||||||
|
0x0206, // Input data
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final short payloadLengthsGen3[] = {
|
private static final short payloadLengthsGen3[] = {
|
||||||
@ -57,6 +69,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
24, // Invalidate reference frames
|
24, // Invalidate reference frames
|
||||||
32, // Loss Stats
|
32, // Loss Stats
|
||||||
64, // Frame Stats
|
64, // Frame Stats
|
||||||
|
-1, // Input Data
|
||||||
};
|
};
|
||||||
private static final short payloadLengthsGen4[] = {
|
private static final short payloadLengthsGen4[] = {
|
||||||
-1, // Request IDR frame
|
-1, // Request IDR frame
|
||||||
@ -64,6 +77,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
24, // Invalidate reference frames
|
24, // Invalidate reference frames
|
||||||
32, // Loss Stats
|
32, // Loss Stats
|
||||||
64, // Frame Stats
|
64, // Frame Stats
|
||||||
|
-1, // Input Data
|
||||||
};
|
};
|
||||||
private static final short payloadLengthsGen5[] = {
|
private static final short payloadLengthsGen5[] = {
|
||||||
-1, // Start A
|
-1, // Start A
|
||||||
@ -71,6 +85,15 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
24, // Invalidate reference frames
|
24, // Invalidate reference frames
|
||||||
32, // Loss Stats
|
32, // Loss Stats
|
||||||
80, // Frame Stats
|
80, // Frame Stats
|
||||||
|
-1, // Input Data
|
||||||
|
};
|
||||||
|
private static final short payloadLengthsGen7[] = {
|
||||||
|
-1, // Start A
|
||||||
|
16, // Start B
|
||||||
|
24, // Invalidate reference frames
|
||||||
|
32, // Loss Stats
|
||||||
|
80, // Frame Stats
|
||||||
|
-1, // Input Data
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final byte[] precontructedPayloadsGen3[] = {
|
private static final byte[] precontructedPayloadsGen3[] = {
|
||||||
@ -79,6 +102,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
null, // Invalidate reference frames
|
null, // Invalidate reference frames
|
||||||
null, // Loss Stats
|
null, // Loss Stats
|
||||||
null, // Frame Stats
|
null, // Frame Stats
|
||||||
|
null, // Input Data
|
||||||
};
|
};
|
||||||
private static final byte[] precontructedPayloadsGen4[] = {
|
private static final byte[] precontructedPayloadsGen4[] = {
|
||||||
new byte[]{0, 0}, // Request IDR frame
|
new byte[]{0, 0}, // Request IDR frame
|
||||||
@ -86,6 +110,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
null, // Invalidate reference frames
|
null, // Invalidate reference frames
|
||||||
null, // Loss Stats
|
null, // Loss Stats
|
||||||
null, // Frame Stats
|
null, // Frame Stats
|
||||||
|
null, // Input Data
|
||||||
};
|
};
|
||||||
private static final byte[] precontructedPayloadsGen5[] = {
|
private static final byte[] precontructedPayloadsGen5[] = {
|
||||||
new byte[]{0, 0}, // Start A
|
new byte[]{0, 0}, // Start A
|
||||||
@ -93,6 +118,15 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
null, // Invalidate reference frames
|
null, // Invalidate reference frames
|
||||||
null, // Loss Stats
|
null, // Loss Stats
|
||||||
null, // Frame Stats
|
null, // Frame Stats
|
||||||
|
null, // Input Data
|
||||||
|
};
|
||||||
|
private static final byte[] precontructedPayloadsGen7[] = {
|
||||||
|
new byte[]{0, 0}, // Start A
|
||||||
|
null, // Start B
|
||||||
|
null, // Invalidate reference frames
|
||||||
|
null, // Loss Stats
|
||||||
|
null, // Frame Stats
|
||||||
|
null, // Input Data
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final int LOSS_REPORT_INTERVAL_MS = 50;
|
public static final int LOSS_REPORT_INTERVAL_MS = 50;
|
||||||
@ -154,11 +188,16 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
preconstructedPayloads = precontructedPayloadsGen4;
|
preconstructedPayloads = precontructedPayloadsGen4;
|
||||||
break;
|
break;
|
||||||
case ConnectionContext.SERVER_GENERATION_5:
|
case ConnectionContext.SERVER_GENERATION_5:
|
||||||
default:
|
|
||||||
packetTypes = packetTypesGen5;
|
packetTypes = packetTypesGen5;
|
||||||
payloadLengths = payloadLengthsGen5;
|
payloadLengths = payloadLengthsGen5;
|
||||||
preconstructedPayloads = precontructedPayloadsGen5;
|
preconstructedPayloads = precontructedPayloadsGen5;
|
||||||
break;
|
break;
|
||||||
|
case ConnectionContext.SERVER_GENERATION_7:
|
||||||
|
default:
|
||||||
|
packetTypes = packetTypesGen7;
|
||||||
|
payloadLengths = payloadLengthsGen7;
|
||||||
|
preconstructedPayloads = precontructedPayloadsGen7;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.videoDecoderRenderer != null) {
|
if (context.videoDecoderRenderer != null) {
|
||||||
@ -225,7 +264,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void sendInputPacket(byte[] data, short length) throws IOException {
|
public void sendInputPacket(byte[] data, short length) throws IOException {
|
||||||
sendPacket(new NvCtlPacket((short) 0x0207, length, data));
|
sendPacket(new NvCtlPacket(packetTypes[IDX_INPUT_DATA], length, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void abort()
|
public void abort()
|
||||||
@ -565,7 +604,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
serializationBuffer.limit(serializationBuffer.capacity());
|
serializationBuffer.limit(serializationBuffer.capacity());
|
||||||
serializationBuffer.putShort(type);
|
serializationBuffer.putShort(type);
|
||||||
serializationBuffer.putShort(paylen);
|
serializationBuffer.putShort(paylen);
|
||||||
serializationBuffer.put(payload);
|
serializationBuffer.put(payload, 0, paylen);
|
||||||
|
|
||||||
out.write(serializationBuffer.array(), 0, serializationBuffer.position());
|
out.write(serializationBuffer.array(), 0, serializationBuffer.position());
|
||||||
}
|
}
|
||||||
@ -578,7 +617,7 @@ public class ControlStream implements ConnectionStatusListener, InputPacketSende
|
|||||||
serializationBuffer.rewind();
|
serializationBuffer.rewind();
|
||||||
serializationBuffer.limit(serializationBuffer.capacity());
|
serializationBuffer.limit(serializationBuffer.capacity());
|
||||||
serializationBuffer.putShort(type);
|
serializationBuffer.putShort(type);
|
||||||
serializationBuffer.put(payload);
|
serializationBuffer.put(payload, 0, paylen);
|
||||||
serializationBuffer.limit(serializationBuffer.position());
|
serializationBuffer.limit(serializationBuffer.position());
|
||||||
|
|
||||||
conn.writePacket(serializationBuffer);
|
conn.writePacket(serializationBuffer);
|
||||||
|
@ -12,8 +12,12 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.ShortBufferException;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
|
||||||
import com.limelight.nvstream.ConnectionContext;
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
@ -34,7 +38,7 @@ public class ControllerStream {
|
|||||||
// Used on Gen 5+ servers
|
// Used on Gen 5+ servers
|
||||||
private InputPacketSender controlSender;
|
private InputPacketSender controlSender;
|
||||||
|
|
||||||
private Cipher riCipher;
|
private InputCipher cipher;
|
||||||
|
|
||||||
private Thread inputThread;
|
private Thread inputThread;
|
||||||
private LinkedBlockingQueue<InputPacket> inputQueue = new LinkedBlockingQueue<InputPacket>();
|
private LinkedBlockingQueue<InputPacket> inputQueue = new LinkedBlockingQueue<InputPacket>();
|
||||||
@ -45,23 +49,19 @@ public class ControllerStream {
|
|||||||
public ControllerStream(ConnectionContext context)
|
public ControllerStream(ConnectionContext context)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
try {
|
|
||||||
// This cipher is guaranteed to be supported
|
if (context.serverGeneration >= ConnectionContext.SERVER_GENERATION_7) {
|
||||||
this.riCipher = Cipher.getInstance("AES/CBC/NoPadding");
|
// Newer GFE versions use AES GCM
|
||||||
|
cipher = new AesGcmCipher();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Older versions used AES CBC
|
||||||
|
cipher = new AesCbcCipher();
|
||||||
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(16);
|
ByteBuffer bb = ByteBuffer.allocate(16);
|
||||||
bb.putInt(context.riKeyId);
|
bb.putInt(context.riKeyId);
|
||||||
|
cipher.initialize(context.riKey, bb.array());
|
||||||
this.riCipher.init(Cipher.ENCRYPT_MODE, context.riKey, new IvParameterSpec(bb.array()));
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (NoSuchPaddingException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InvalidAlgorithmParameterException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize(InputPacketSender controlSender) throws IOException
|
public void initialize(InputPacketSender controlSender) throws IOException
|
||||||
@ -217,47 +217,19 @@ public class ControllerStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getPaddedSize(int length) {
|
|
||||||
return ((length + 15) / 16) * 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int inPlacePadData(byte[] data, int length) {
|
|
||||||
// This implements the PKCS7 padding algorithm
|
|
||||||
|
|
||||||
if ((length % 16) == 0) {
|
|
||||||
// Already a multiple of 16
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
int paddedLength = getPaddedSize(length);
|
|
||||||
byte paddingByte = (byte)(16 - (length % 16));
|
|
||||||
|
|
||||||
for (int i = length; i < paddedLength; i++) {
|
|
||||||
data[i] = paddingByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
return paddedLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int encryptAesInputData(byte[] inputData, int inputLength, byte[] outputData, int outputOffset) throws Exception {
|
|
||||||
int encryptedLength = inPlacePadData(inputData, inputLength);
|
|
||||||
riCipher.update(inputData, 0, encryptedLength, outputData, outputOffset);
|
|
||||||
return encryptedLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendPacket(InputPacket packet) throws IOException {
|
private void sendPacket(InputPacket packet) throws IOException {
|
||||||
// Store the packet in wire form in the byte buffer
|
// Store the packet in wire form in the byte buffer
|
||||||
packet.toWire(stagingBuffer);
|
packet.toWire(stagingBuffer);
|
||||||
int packetLen = packet.getPacketLength();
|
int packetLen = packet.getPacketLength();
|
||||||
|
|
||||||
// Pad to 16 byte chunks
|
// Get final encrypted size of this block
|
||||||
int paddedLength = getPaddedSize(packetLen);
|
int paddedLength = cipher.getEncryptedSize(packetLen);
|
||||||
|
|
||||||
// Allocate a byte buffer to represent the final packet
|
// Allocate a byte buffer to represent the final packet
|
||||||
sendBuffer.rewind();
|
sendBuffer.rewind();
|
||||||
sendBuffer.putInt(paddedLength);
|
sendBuffer.putInt(paddedLength);
|
||||||
try {
|
try {
|
||||||
encryptAesInputData(stagingBuffer.array(), packetLen, sendBuffer.array(), 4);
|
cipher.encrypt(stagingBuffer.array(), packetLen, sendBuffer.array(), 4);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@ -339,4 +311,107 @@ public class ControllerStream {
|
|||||||
{
|
{
|
||||||
queuePacket(new MouseScrollPacket(context, scrollClicks));
|
queuePacket(new MouseScrollPacket(context, scrollClicks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static interface InputCipher {
|
||||||
|
public void initialize(SecretKey key, byte[] iv);
|
||||||
|
public int getEncryptedSize(int plaintextSize);
|
||||||
|
public void encrypt(byte[] inputData, int inputLength, byte[] outputData, int outputOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AesCbcCipher implements InputCipher {
|
||||||
|
private Cipher cipher;
|
||||||
|
|
||||||
|
public void initialize(SecretKey key, byte[] iv) {
|
||||||
|
try {
|
||||||
|
cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (NoSuchPaddingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidAlgorithmParameterException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEncryptedSize(int plaintextSize) {
|
||||||
|
// CBC requires padding to the next multiple of 16
|
||||||
|
return ((plaintextSize + 15) / 16) * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int inPlacePadData(byte[] data, int length) {
|
||||||
|
// This implements the PKCS7 padding algorithm
|
||||||
|
|
||||||
|
if ((length % 16) == 0) {
|
||||||
|
// Already a multiple of 16
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
int paddedLength = getEncryptedSize(length);
|
||||||
|
byte paddingByte = (byte)(16 - (length % 16));
|
||||||
|
|
||||||
|
for (int i = length; i < paddedLength; i++) {
|
||||||
|
data[i] = paddingByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return paddedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void encrypt(byte[] inputData, int inputLength, byte[] outputData, int outputOffset) {
|
||||||
|
int encryptedLength = inPlacePadData(inputData, inputLength);
|
||||||
|
try {
|
||||||
|
cipher.update(inputData, 0, encryptedLength, outputData, outputOffset);
|
||||||
|
} catch (ShortBufferException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AesGcmCipher implements InputCipher {
|
||||||
|
private SecretKey key;
|
||||||
|
private byte[] iv;
|
||||||
|
|
||||||
|
public int getEncryptedSize(int plaintextSize) {
|
||||||
|
// GCM uses no padding + 16 bytes tag for message authentication
|
||||||
|
return plaintextSize + 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(SecretKey key, byte[] iv) {
|
||||||
|
this.key = key;
|
||||||
|
this.iv = iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void encrypt(byte[] inputData, int inputLength, byte[] outputData, int outputOffset) {
|
||||||
|
// Reconstructing the cipher on every invocation really sucks but we have to do it
|
||||||
|
// because of the way NVIDIA is using GCM where each message is tagged. Java doesn't
|
||||||
|
// have an easy way that I know of to get a tag out mid-stream.
|
||||||
|
Cipher cipher;
|
||||||
|
try {
|
||||||
|
cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
|
||||||
|
|
||||||
|
// This is also non-ideal. Java gives us <ciphertext><tag> but we want to send <tag><ciphertext>
|
||||||
|
// so we'll take the output and arraycopy it into the right spot in the output buffer
|
||||||
|
byte[] rawCipherOut = cipher.doFinal(inputData, 0, inputLength);
|
||||||
|
System.arraycopy(rawCipherOut, inputLength, outputData, outputOffset, 16);
|
||||||
|
System.arraycopy(rawCipherOut, 0, outputData, outputOffset + 16, inputLength);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (NoSuchPaddingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidAlgorithmParameterException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalBlockSizeException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (BadPaddingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user