Pairing support for Gen 7 servers

This commit is contained in:
Cameron Gutman 2016-03-28 18:38:11 -04:00
parent 31d7f237eb
commit 8b395bb29f
4 changed files with 118 additions and 54 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}
}
}
}