mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-23 21:12:44 +00:00
284 lines
9.4 KiB
Java
284 lines
9.4 KiB
Java
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.Calendar;
|
|
import java.util.Date;
|
|
import java.util.Locale;
|
|
|
|
import org.bouncycastle.asn1.x500.X500Name;
|
|
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
|
import org.bouncycastle.operator.ContentSigner;
|
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.util.Base64;
|
|
|
|
import com.limelight.LimeLog;
|
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
|
|
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|
|
|
private final File certFile;
|
|
private final File keyFile;
|
|
|
|
private X509Certificate cert;
|
|
private RSAPrivateKey key;
|
|
private byte[] pemCertBytes;
|
|
|
|
private static final Object globalCryptoLock = new Object();
|
|
|
|
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()];
|
|
if (fin.read(fileData) != f.length()) {
|
|
// Failed to read
|
|
fileData = null;
|
|
}
|
|
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;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@SuppressLint("TrulyRandom")
|
|
private boolean generateCertKeyPair() {
|
|
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();
|
|
|
|
// Expires in 20 years
|
|
Calendar calendar = Calendar.getInstance();
|
|
calendar.setTime(now);
|
|
calendar.add(Calendar.YEAR, 20);
|
|
Date expirationDate = calendar.getTime();
|
|
|
|
BigInteger serial = new BigInteger(snBytes).abs();
|
|
|
|
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
|
X500Name name = nameBuilder.build();
|
|
|
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
|
|
|
try {
|
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
|
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
|
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();
|
|
JcaPEMWriter pemWriter = new JcaPEMWriter(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 (globalCryptoLock) {
|
|
// 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 (globalCryptoLock) {
|
|
// 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 (globalCryptoLock) {
|
|
// 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);
|
|
}
|
|
}
|