mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-19 11:03:01 +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+
|
||||
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 StreamConfiguration streamConfig;
|
||||
public VideoDecoderRenderer videoDecoderRenderer;
|
||||
|
@ -103,43 +103,43 @@ public class NvConnection {
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, null, cryptoProvider);
|
||||
|
||||
String serverInfo = h.getServerInfo();
|
||||
String serverVersion = h.getServerVersion(serverInfo);
|
||||
if (serverVersion == null || serverVersion.indexOf('.') < 0) {
|
||||
context.connListener.displayMessage("Server major version not present");
|
||||
|
||||
int majorVersion = h.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Server major version: "+majorVersion);
|
||||
|
||||
if (majorVersion == 0) {
|
||||
context.connListener.displayMessage("Server version malformed");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
int majorVersion = Integer.parseInt(serverVersion.substring(0, serverVersion.indexOf('.')));
|
||||
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);
|
||||
else 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 > 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) {
|
||||
context.connListener.displayMessage("Device not paired with computer");
|
||||
return false;
|
||||
|
@ -444,8 +444,8 @@ public class NvHTTP {
|
||||
return null;
|
||||
}
|
||||
|
||||
public PairingManager.PairState pair(String pin) throws Exception {
|
||||
return pm.pair(pin);
|
||||
public PairingManager.PairState pair(String serverInfo, String pin) throws Exception {
|
||||
return pm.pair(serverInfo, pin);
|
||||
}
|
||||
|
||||
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
|
||||
@ -536,6 +536,20 @@ public class NvHTTP {
|
||||
"&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
||||
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();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
|
@ -10,6 +10,8 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.io.*;
|
||||
@ -86,18 +88,6 @@ public class PairingManager {
|
||||
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 {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
@ -139,8 +129,8 @@ public class PairingManager {
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(toSHA1Bytes(keyData), 16);
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
@ -166,13 +156,26 @@ public class PairingManager {
|
||||
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
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
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
|
||||
// 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[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, 20);
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, 20, 36);
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
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
|
||||
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);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/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)
|
||||
byte[] serverChallengeRespHash = toSHA1Bytes(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
@ -262,4 +265,45 @@ public class PairingManager {
|
||||
|
||||
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