mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-21 12:03:02 +00:00
Pairing support for Gen 7 servers
This commit is contained in:
parent
31d7f237eb
commit
8b395bb29f
@ -17,6 +17,12 @@ public class ConnectionContext {
|
|||||||
// Gen 5 servers are 2.10.2+
|
// Gen 5 servers are 2.10.2+
|
||||||
public static final int SERVER_GENERATION_5 = 5;
|
public static final int SERVER_GENERATION_5 = 5;
|
||||||
|
|
||||||
|
// Gen 6 servers haven't been seen in the wild
|
||||||
|
public static final int SERVER_GENERATION_6 = 6;
|
||||||
|
|
||||||
|
// Gen 7 servers are GFE 2.11.2.46+
|
||||||
|
public static final int SERVER_GENERATION_7 = 7;
|
||||||
|
|
||||||
public InetAddress serverAddress;
|
public InetAddress serverAddress;
|
||||||
public StreamConfiguration streamConfig;
|
public StreamConfiguration streamConfig;
|
||||||
public VideoDecoderRenderer videoDecoderRenderer;
|
public VideoDecoderRenderer videoDecoderRenderer;
|
||||||
|
@ -103,42 +103,42 @@ public class NvConnection {
|
|||||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, null, cryptoProvider);
|
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, null, cryptoProvider);
|
||||||
|
|
||||||
String serverInfo = h.getServerInfo();
|
String serverInfo = h.getServerInfo();
|
||||||
String serverVersion = h.getServerVersion(serverInfo);
|
|
||||||
if (serverVersion == null || serverVersion.indexOf('.') < 0) {
|
int majorVersion = h.getServerMajorVersion(serverInfo);
|
||||||
context.connListener.displayMessage("Server major version not present");
|
LimeLog.info("Server major version: "+majorVersion);
|
||||||
|
|
||||||
|
if (majorVersion == 0) {
|
||||||
|
context.connListener.displayMessage("Server version malformed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
else if (majorVersion < 3) {
|
||||||
try {
|
// Even though we support major version 3 (2.1.x), GFE 2.2.2 is preferred.
|
||||||
int majorVersion = Integer.parseInt(serverVersion.substring(0, serverVersion.indexOf('.')));
|
context.connListener.displayMessage("This app requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again.");
|
||||||
if (majorVersion < 3) {
|
|
||||||
// Even though we support major version 3 (2.1.x), GFE 2.2.2 is preferred.
|
|
||||||
context.connListener.displayMessage("This app requires GeForce Experience 2.2.2 or later. Please upgrade GFE on your PC and try again.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (majorVersion > 5) {
|
|
||||||
// Warn the user but allow them to continue
|
|
||||||
context.connListener.displayTransientMessage("This version of GFE is not currently supported. You may experience issues until this app is updated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (majorVersion) {
|
|
||||||
case 3:
|
|
||||||
context.serverGeneration = ConnectionContext.SERVER_GENERATION_3;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.serverGeneration = ConnectionContext.SERVER_GENERATION_4;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
default:
|
|
||||||
context.serverGeneration = ConnectionContext.SERVER_GENERATION_5;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Server major version: "+majorVersion);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
context.connListener.displayMessage("Server version malformed: "+serverVersion);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
else if (majorVersion > 7) {
|
||||||
|
// Warn the user but allow them to continue
|
||||||
|
context.connListener.displayTransientMessage("This version of GFE is not currently supported. You may experience issues until this app is updated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (majorVersion) {
|
||||||
|
case 3:
|
||||||
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_3;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_4;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_5;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_6;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
default:
|
||||||
|
context.serverGeneration = ConnectionContext.SERVER_GENERATION_7;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||||
context.connListener.displayMessage("Device not paired with computer");
|
context.connListener.displayMessage("Device not paired with computer");
|
||||||
|
@ -444,8 +444,8 @@ public class NvHTTP {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PairingManager.PairState pair(String pin) throws Exception {
|
public PairingManager.PairState pair(String serverInfo, String pin) throws Exception {
|
||||||
return pm.pair(pin);
|
return pm.pair(serverInfo, pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
|
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
|
||||||
@ -537,6 +537,20 @@ public class NvHTTP {
|
|||||||
return resp.byteStream();
|
return resp.byteStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
try {
|
||||||
|
String serverVersion = getServerVersion(serverInfo);
|
||||||
|
if (serverVersion == null || serverVersion.indexOf('.') < 0) {
|
||||||
|
LimeLog.warning("Malformed server version field");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(serverVersion.substring(0, serverVersion.indexOf('.')));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||||
private static String bytesToHex(byte[] bytes) {
|
private static String bytesToHex(byte[] bytes) {
|
||||||
char[] hexChars = new char[bytes.length * 2];
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
|
@ -10,6 +10,8 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@ -86,18 +88,6 @@ public class PairingManager {
|
|||||||
return saltedPin;
|
return saltedPin;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] toSHA1Bytes(byte[] data) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
|
||||||
return md.digest(data);
|
|
||||||
}
|
|
||||||
catch (NoSuchAlgorithmException e) {
|
|
||||||
// Shouldn't ever happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
|
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
|
||||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||||
sig.initVerify(cert.getPublicKey());
|
sig.initVerify(cert.getPublicKey());
|
||||||
@ -139,8 +129,8 @@ public class PairingManager {
|
|||||||
return cipher.doFinal(blockRoundedData);
|
return cipher.doFinal(blockRoundedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SecretKey generateAesKey(byte[] keyData) {
|
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||||
byte[] aesTruncated = Arrays.copyOf(toSHA1Bytes(keyData), 16);
|
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||||
return new SecretKeySpec(aesTruncated, "AES");
|
return new SecretKeySpec(aesTruncated, "AES");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,13 +156,26 @@ public class PairingManager {
|
|||||||
return PairState.PAIRED;
|
return PairState.PAIRED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PairState pair(String pin) throws MalformedURLException, IOException, XmlPullParserException, CertificateException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException {
|
public PairState pair(String serverInfo, String pin) throws MalformedURLException, IOException, XmlPullParserException, CertificateException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException {
|
||||||
|
PairingHashAlgorithm hashAlgo;
|
||||||
|
|
||||||
|
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||||
|
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||||
|
if (serverMajorVersion >= 7) {
|
||||||
|
// Gen 7+ uses SHA-256 hashing
|
||||||
|
hashAlgo = new Sha256PairingHash();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Prior to Gen 7, SHA-1 is used
|
||||||
|
hashAlgo = new Sha1PairingHash();
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a salt for hashing the PIN
|
// Generate a salt for hashing the PIN
|
||||||
byte[] salt = generateRandomBytes(16);
|
byte[] salt = generateRandomBytes(16);
|
||||||
|
|
||||||
// Combine the salt and pin, then create an AES key from them
|
// Combine the salt and pin, then create an AES key from them
|
||||||
byte[] saltAndPin = saltPin(salt, pin);
|
byte[] saltAndPin = saltPin(salt, pin);
|
||||||
aesKey = generateAesKey(saltAndPin);
|
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||||
|
|
||||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||||
// because the user must enter the PIN before the server responds
|
// because the user must enter the PIN before the server responds
|
||||||
@ -203,12 +206,12 @@ public class PairingManager {
|
|||||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||||
|
|
||||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, 20);
|
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, 20, 36);
|
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||||
|
|
||||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||||
byte[] clientSecret = generateRandomBytes(16);
|
byte[] clientSecret = generateRandomBytes(16);
|
||||||
byte[] challengeRespHash = toSHA1Bytes(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||||
@ -233,7 +236,7 @@ public class PairingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||||
byte[] serverChallengeRespHash = toSHA1Bytes(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||||
// Cancel the pairing process
|
// Cancel the pairing process
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
@ -262,4 +265,45 @@ public class PairingManager {
|
|||||||
|
|
||||||
return PairState.PAIRED;
|
return PairState.PAIRED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static interface PairingHashAlgorithm {
|
||||||
|
public int getHashLength();
|
||||||
|
public byte[] hashData(byte[] data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||||
|
public int getHashLength() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] hashData(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
return md.digest(data);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
// Shouldn't ever happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||||
|
public int getHashLength() {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] hashData(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
return md.digest(data);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
// Shouldn't ever happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user