Skip to content
This repository has been archived by the owner on Aug 21, 2019. It is now read-only.

Commit

Permalink
Pairing support for Gen 7 servers
Browse files Browse the repository at this point in the history
  • Loading branch information
cgutman committed Mar 28, 2016
1 parent c7b4133 commit 4560fa4
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 54 deletions.
6 changes: 6 additions & 0 deletions src/com/limelight/nvstream/ConnectionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
64 changes: 32 additions & 32 deletions src/com/limelight/nvstream/NvConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,43 +103,43 @@ private boolean startApp() throws XmlPullParserException, IOException
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;
Expand Down
18 changes: 16 additions & 2 deletions src/com/limelight/nvstream/http/NvHTTP.java
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ public NvApp getAppByName(String appName) throws IOException, XmlPullParserExcep
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 {
Expand Down Expand Up @@ -536,6 +536,20 @@ public InputStream getBoxArt(NvApp app) throws IOException {
"&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) {
Expand Down
84 changes: 64 additions & 20 deletions src/com/limelight/nvstream/http/PairingManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import org.xmlpull.v1.XmlPullParserException;

import com.limelight.LimeLog;

import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.io.*;
Expand Down Expand Up @@ -86,18 +88,6 @@ private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodin
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());
Expand Down Expand Up @@ -139,8 +129,8 @@ private static byte[] encryptAes(byte[] data, SecretKey secretKey) throws NoSuch
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");
}

Expand All @@ -166,13 +156,26 @@ public PairState getPairState(String serverInfo) throws MalformedURLException, I
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
Expand Down Expand Up @@ -203,12 +206,12 @@ public PairState pair(String pin) throws MalformedURLException, IOException, Xml
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),
Expand All @@ -233,7 +236,7 @@ public PairState pair(String pin) throws MalformedURLException, IOException, Xml
}

// 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);
Expand Down Expand Up @@ -262,4 +265,45 @@ public PairState pair(String pin) throws MalformedURLException, IOException, Xml

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

0 comments on commit 4560fa4

Please sign in to comment.