diff --git a/libs/bcpkix-jdk15on-150.jar b/libs/bcpkix-jdk15on-150.jar new file mode 100644 index 00000000..5dc125fc Binary files /dev/null and b/libs/bcpkix-jdk15on-150.jar differ diff --git a/libs/bcprov-jdk15on-150.jar b/libs/bcprov-jdk15on-150.jar new file mode 100644 index 00000000..d4b510d7 Binary files /dev/null and b/libs/bcprov-jdk15on-150.jar differ diff --git a/libs/limelight-common.jar b/libs/limelight-common.jar index 596e6d0e..a77ff91b 100644 Binary files a/libs/limelight-common.jar and b/libs/limelight-common.jar differ diff --git a/src/com/limelight/Connection.java b/src/com/limelight/Connection.java index bd18c6a7..abb351a8 100644 --- a/src/com/limelight/Connection.java +++ b/src/com/limelight/Connection.java @@ -1,16 +1,14 @@ package com.limelight; -import java.io.IOException; - import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; -import org.xmlpull.v1.XmlPullParserException; - import com.limelight.binding.PlatformBinding; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.http.NvHTTP; +import com.limelight.nvstream.http.PairingManager; +import com.limelight.utils.Dialog; import android.os.Bundle; import android.view.View; @@ -52,6 +50,13 @@ public class Connection extends Activity { super.onPause(); } + @Override + protected void onStop() { + super.onStop(); + + Dialog.closeDialogs(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -267,29 +272,39 @@ public class Connection extends Activity { String message; try { httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()), - macAddress, PlatformBinding.getDeviceName()); - try { - if (httpConn.getPairState()) { - message = "Already paired"; + macAddress, PlatformBinding.getDeviceName(), PlatformBinding.getCryptoProvider(Connection.this)); + if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { + message = "Already paired"; + } + else { + final String pinStr = PairingManager.generatePinString(); + + // Spin the dialog off in a thread because it blocks + Dialog.displayDialog(Connection.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false); + + PairingManager.PairState pairState = httpConn.pair(pinStr); + if (pairState == PairingManager.PairState.PIN_WRONG) { + message = "Incorrect PIN"; + } + else if (pairState == PairingManager.PairState.FAILED) { + message = "Pairing failed"; + } + else if (pairState == PairingManager.PairState.PAIRED) { + message = "Paired successfully"; } else { - int session = httpConn.getSessionId(); - if (session == 0) { - message = "Pairing was declined by the target"; - } - else { - message = "Pairing was successful"; - } + // Should be no other values + message = null; } - } catch (IOException e) { - message = e.getMessage(); - } catch (XmlPullParserException e) { - message = e.getMessage(); } } catch (UnknownHostException e1) { message = "Failed to resolve host"; + } catch (Exception e) { + message = e.getMessage(); } + Dialog.closeDialogs(); + final String toastMessage = message; runOnUiThread(new Runnable() { @Override diff --git a/src/com/limelight/Game.java b/src/com/limelight/Game.java index 1428c0c0..c55d50e0 100644 --- a/src/com/limelight/Game.java +++ b/src/com/limelight/Game.java @@ -155,7 +155,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM // Start the connection conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this, - new StreamConfiguration(width, height, refreshRate, bitrate * 1000)); + new StreamConfiguration(width, height, refreshRate, bitrate * 1000), PlatformBinding.getCryptoProvider(this)); keybTranslator = new KeyboardTranslator(conn); controllerHandler = new ControllerHandler(conn); diff --git a/src/com/limelight/binding/PlatformBinding.java b/src/com/limelight/binding/PlatformBinding.java index 487f4e73..e159dd4e 100644 --- a/src/com/limelight/binding/PlatformBinding.java +++ b/src/com/limelight/binding/PlatformBinding.java @@ -1,7 +1,11 @@ package com.limelight.binding; +import android.content.Context; + import com.limelight.binding.audio.AndroidAudioRenderer; +import com.limelight.binding.crypto.AndroidCryptoProvider; import com.limelight.nvstream.av.audio.AudioRenderer; +import com.limelight.nvstream.http.LimelightCryptoProvider; public class PlatformBinding { public static String getDeviceName() { @@ -13,4 +17,8 @@ public class PlatformBinding { public static AudioRenderer getAudioRenderer() { return new AndroidAudioRenderer(); } + + public static LimelightCryptoProvider getCryptoProvider(Context c) { + return new AndroidCryptoProvider(c); + } } diff --git a/src/com/limelight/binding/crypto/AndroidCryptoProvider.java b/src/com/limelight/binding/crypto/AndroidCryptoProvider.java new file mode 100644 index 00000000..e34356f1 --- /dev/null +++ b/src/com/limelight/binding/crypto/AndroidCryptoProvider.java @@ -0,0 +1,272 @@ +package com.limelight.binding.crypto; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; + +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMWriter; +import org.bouncycastle.x509.X509V3CertificateGenerator; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Base64; + +import com.limelight.LimeLog; +import com.limelight.nvstream.http.LimelightCryptoProvider; + +@SuppressWarnings("deprecation") +public class AndroidCryptoProvider implements LimelightCryptoProvider { + + private File certFile; + private File keyFile; + + private X509Certificate cert; + private RSAPrivateKey key; + private byte[] pemCertBytes; + + static { + // Install the Bouncy Castle provider + Security.addProvider(new BouncyCastleProvider()); + } + + public AndroidCryptoProvider(Context c) { + String dataPath = c.getFilesDir().getAbsolutePath(); + + certFile = new File(dataPath + File.separator + "client.crt"); + keyFile = new File(dataPath + File.separator + "client.key"); + } + + private byte[] loadFileToBytes(File f) { + if (!f.exists()) { + return null; + } + + try { + FileInputStream fin = new FileInputStream(f); + byte[] fileData = new byte[(int) f.length()]; + fin.read(fileData); + fin.close(); + return fileData; + } catch (IOException e) { + return null; + } + } + + private boolean loadCertKeyPair() { + byte[] certBytes = loadFileToBytes(certFile); + byte[] keyBytes = loadFileToBytes(keyFile); + + // If either file was missing, we definitely can't succeed + if (certBytes == null || keyBytes == null) { + LimeLog.info("Missing cert or key; need to generate a new one"); + return false; + } + + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); + cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes)); + pemCertBytes = certBytes; + KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC"); + key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + } catch (CertificateException e) { + // May happen if the cert is corrupt + LimeLog.warning("Corrupted certificate"); + return false; + } catch (NoSuchAlgorithmException e) { + // Should never happen + e.printStackTrace(); + return false; + } catch (InvalidKeySpecException e) { + // May happen if the key is corrupt + LimeLog.warning("Corrupted key"); + return false; + } catch (NoSuchProviderException e) { + // Should never happen + e.printStackTrace(); + return false; + } + + LimeLog.info("Loaded key pair from disk"); + return true; + } + + @SuppressLint("TrulyRandom") + private boolean generateCertKeyPair() { + X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator(); + X500Principal principalName = new X500Principal("CN=NVIDIA GameStream Client"); + + byte[] snBytes = new byte[8]; + new SecureRandom().nextBytes(snBytes); + + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } catch (NoSuchAlgorithmException e1) { + // Should never happen + e1.printStackTrace(); + return false; + } catch (NoSuchProviderException e) { + // Should never happen + e.printStackTrace(); + return false; + } + + Date now = new Date(); + Date expirationDate = new Date(); + + // Expires in 20 years + expirationDate.setYear(expirationDate.getYear() + 20); + + certGenerator.setSerialNumber(new BigInteger(snBytes).abs()); + certGenerator.setIssuerDN(principalName); + certGenerator.setNotBefore(now); + certGenerator.setNotAfter(expirationDate); + certGenerator.setSubjectDN(principalName); + certGenerator.setPublicKey(keyPair.getPublic()); + certGenerator.setSignatureAlgorithm("SHA1withRSA"); + + try { + cert = certGenerator.generate(keyPair.getPrivate(), "BC"); + key = (RSAPrivateKey) keyPair.getPrivate(); + } catch (Exception e) { + // Nothing should go wrong here + e.printStackTrace(); + return false; + } + + LimeLog.info("Generated a new key pair"); + + // Save the resulting pair + saveCertKeyPair(); + + return true; + } + + private void saveCertKeyPair() { + try { + FileOutputStream certOut = new FileOutputStream(certFile); + FileOutputStream keyOut = new FileOutputStream(keyFile); + + // Write the certificate in OpenSSL PEM format (important for the server) + StringWriter strWriter = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(strWriter); + pemWriter.writeObject(cert); + pemWriter.close(); + + // Line endings MUST be UNIX for the PC to accept the cert properly + OutputStreamWriter certWriter = new OutputStreamWriter(certOut); + String pemStr = strWriter.getBuffer().toString(); + for (int i = 0; i < pemStr.length(); i++) { + char c = pemStr.charAt(i); + if (c != '\r') + certWriter.append(c); + } + certWriter.close(); + + // Write the private out in PKCS8 format + keyOut.write(key.getEncoded()); + + certOut.close(); + keyOut.close(); + + LimeLog.info("Saved generated key pair to disk"); + } catch (IOException e) { + // This isn't good because it means we'll have + // to re-pair next time + e.printStackTrace(); + } + } + + public X509Certificate getClientCertificate() { + // Use a lock here to ensure only one guy will be generating or loading + // the certificate and key at a time + synchronized (this) { + // Return a loaded cert if we have one + if (cert != null) { + return cert; + } + + // No loaded cert yet, let's see if we have one on disk + if (loadCertKeyPair()) { + // Got one + return cert; + } + + // Try to generate a new key pair + if (!generateCertKeyPair()) { + // Failed + return null; + } + + // Load the generated pair + loadCertKeyPair(); + return cert; + } + } + + public RSAPrivateKey getClientPrivateKey() { + // Use a lock here to ensure only one guy will be generating or loading + // the certificate and key at a time + synchronized (this) { + // Return a loaded key if we have one + if (key != null) { + return key; + } + + // No loaded key yet, let's see if we have one on disk + if (loadCertKeyPair()) { + // Got one + return key; + } + + // Try to generate a new key pair + if (!generateCertKeyPair()) { + // Failed + return null; + } + + // Load the generated pair + loadCertKeyPair(); + return key; + } + } + + public byte[] getPemEncodedClientCertificate() { + synchronized (this) { + // Call our helper function to do the cert loading/generation for us + getClientCertificate(); + + // Return a cached value if we have it + return pemCertBytes; + } + } + + @Override + public String encodeBase64String(byte[] data) { + return Base64.encodeToString(data, Base64.NO_WRAP); + } +}