From 8b395bb29f45a5ccf480699e5863df95aa92f3ac Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 28 Mar 2016 18:38:11 -0400 Subject: [PATCH] Pairing support for Gen 7 servers --- .../limelight/nvstream/ConnectionContext.java | 6 ++ .../com/limelight/nvstream/NvConnection.java | 64 +++++++------- .../com/limelight/nvstream/http/NvHTTP.java | 18 +++- .../nvstream/http/PairingManager.java | 84 ++++++++++++++----- 4 files changed, 118 insertions(+), 54 deletions(-) diff --git a/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java b/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java index 1d1fe59c..3baf08f2 100644 --- a/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java +++ b/moonlight-common/src/com/limelight/nvstream/ConnectionContext.java @@ -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; diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/com/limelight/nvstream/NvConnection.java index 1744d372..bf9c5520 100644 --- a/moonlight-common/src/com/limelight/nvstream/NvConnection.java +++ b/moonlight-common/src/com/limelight/nvstream/NvConnection.java @@ -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; diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index 26f49639..3855fc73 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -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 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) { diff --git a/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java index 4d089a11..adbad79f 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java +++ b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java @@ -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; + } + } + } }