6a34ff2728
ECB is safe in this context because it's encrypting one-time messages using a one-time key. All input data going through encryptAes() is either random or partially random and passed through a secure hashing function (SHA-256 on modern GFE versions). Message authentication is not a concern either, because it is performed by the pairing process itself via RSA signature verification. Any ciphertext tampering would cause signature verification to fail later in the pairing process.
326 lines
12 KiB
Java
326 lines
12 KiB
Java
package com.limelight.nvstream.http;
|
|
|
|
import org.bouncycastle.crypto.BlockCipher;
|
|
import org.bouncycastle.crypto.engines.AESLightEngine;
|
|
import org.bouncycastle.crypto.params.KeyParameter;
|
|
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import com.limelight.LimeLog;
|
|
|
|
import java.security.cert.Certificate;
|
|
import java.io.*;
|
|
import java.security.*;
|
|
import java.security.cert.*;
|
|
import java.util.Arrays;
|
|
import java.util.Locale;
|
|
|
|
public class PairingManager {
|
|
|
|
private NvHTTP http;
|
|
|
|
private PrivateKey pk;
|
|
private X509Certificate cert;
|
|
private byte[] pemCertBytes;
|
|
|
|
private X509Certificate serverCert;
|
|
|
|
public enum PairState {
|
|
NOT_PAIRED,
|
|
PAIRED,
|
|
PIN_WRONG,
|
|
FAILED,
|
|
ALREADY_IN_PROGRESS
|
|
}
|
|
|
|
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
|
this.http = http;
|
|
this.cert = cryptoProvider.getClientCertificate();
|
|
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
|
this.pk = cryptoProvider.getClientPrivateKey();
|
|
}
|
|
|
|
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
|
private static String bytesToHex(byte[] bytes) {
|
|
char[] hexChars = new char[bytes.length * 2];
|
|
for ( int j = 0; j < bytes.length; j++ ) {
|
|
int v = bytes[j] & 0xFF;
|
|
hexChars[j * 2] = hexArray[v >>> 4];
|
|
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
|
}
|
|
return new String(hexChars);
|
|
}
|
|
|
|
private static byte[] hexToBytes(String s) {
|
|
int len = s.length();
|
|
if (len % 2 != 0) {
|
|
throw new IllegalArgumentException("Illegal string length: "+len);
|
|
}
|
|
|
|
byte[] data = new byte[len / 2];
|
|
for (int i = 0; i < len; i += 2) {
|
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
|
+ Character.digit(s.charAt(i+1), 16));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
|
{
|
|
// Plaincert may be null if another client is already trying to pair
|
|
String certText = NvHTTP.getXmlString(text, "plaincert", false);
|
|
if (certText != null) {
|
|
byte[] certBytes = hexToBytes(certText);
|
|
|
|
try {
|
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
|
} catch (CertificateException e) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private byte[] generateRandomBytes(int length)
|
|
{
|
|
byte[] rand = new byte[length];
|
|
new SecureRandom().nextBytes(rand);
|
|
return rand;
|
|
}
|
|
|
|
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
|
byte[] saltedPin = new byte[salt.length + pin.length()];
|
|
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
|
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
|
return saltedPin;
|
|
}
|
|
|
|
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
|
try {
|
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
sig.initVerify(cert.getPublicKey());
|
|
sig.update(data);
|
|
return sig.verify(signature);
|
|
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private static byte[] signData(byte[] data, PrivateKey key) {
|
|
try {
|
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
sig.initSign(key);
|
|
sig.update(data);
|
|
byte[] signature = new byte[256];
|
|
sig.sign(signature, 0, signature.length);
|
|
return signature;
|
|
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private static byte[] performBlockCipher(BlockCipher blockCipher, byte[] input) {
|
|
int blockSize = blockCipher.getBlockSize();
|
|
int blockRoundedSize = (input.length + (blockSize - 1)) & ~(blockSize - 1);
|
|
|
|
byte[] blockRoundedInputData = Arrays.copyOf(input, blockRoundedSize);
|
|
byte[] blockRoundedOutputData = new byte[blockRoundedSize];
|
|
|
|
for (int offset = 0; offset < blockRoundedSize; offset += blockSize) {
|
|
blockCipher.processBlock(blockRoundedInputData, offset, blockRoundedOutputData, offset);
|
|
}
|
|
|
|
return blockRoundedOutputData;
|
|
}
|
|
|
|
private static byte[] decryptAes(byte[] encryptedData, byte[] aesKey) {
|
|
BlockCipher aesEngine = new AESLightEngine();
|
|
aesEngine.init(false, new KeyParameter(aesKey));
|
|
return performBlockCipher(aesEngine, encryptedData);
|
|
}
|
|
|
|
private static byte[] encryptAes(byte[] plaintextData, byte[] aesKey) {
|
|
BlockCipher aesEngine = new AESLightEngine();
|
|
aesEngine.init(true, new KeyParameter(aesKey));
|
|
return performBlockCipher(aesEngine, plaintextData);
|
|
}
|
|
|
|
private static byte[] generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
|
return Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
|
}
|
|
|
|
private static byte[] concatBytes(byte[] a, byte[] b) {
|
|
byte[] c = new byte[a.length + b.length];
|
|
System.arraycopy(a, 0, c, 0, a.length);
|
|
System.arraycopy(b, 0, c, a.length, b.length);
|
|
return c;
|
|
}
|
|
|
|
public static String generatePinString() {
|
|
SecureRandom r = new SecureRandom();
|
|
return String.format((Locale)null, "%d%d%d%d",
|
|
r.nextInt(10), r.nextInt(10),
|
|
r.nextInt(10), r.nextInt(10));
|
|
}
|
|
|
|
public X509Certificate getPairedCert() {
|
|
return serverCert;
|
|
}
|
|
|
|
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
|
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[] aesKey = generateAesKey(hashAlgo, saltPin(salt, pin));
|
|
|
|
// 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
|
|
String getCert = http.executePairingCommand("phrase=getservercert&salt="+
|
|
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
|
false);
|
|
if (!NvHTTP.getXmlString(getCert, "paired", true).equals("1")) {
|
|
return PairState.FAILED;
|
|
}
|
|
|
|
// Save this cert for retrieval later
|
|
serverCert = extractPlainCert(getCert);
|
|
if (serverCert == null) {
|
|
// Attempting to pair while another device is pairing will cause GFE
|
|
// to give an empty cert in the response.
|
|
http.unpair();
|
|
return PairState.ALREADY_IN_PROGRESS;
|
|
}
|
|
|
|
// Require this cert for TLS to this host
|
|
http.setServerCert(serverCert);
|
|
|
|
// Generate a random challenge and encrypt it with our AES key
|
|
byte[] randomChallenge = generateRandomBytes(16);
|
|
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
|
|
|
// Send the encrypted challenge to the server
|
|
String challengeResp = http.executePairingCommand("clientchallenge="+bytesToHex(encryptedChallenge), true);
|
|
if (!NvHTTP.getXmlString(challengeResp, "paired", true).equals("1")) {
|
|
http.unpair();
|
|
return PairState.FAILED;
|
|
}
|
|
|
|
// Decode the server's response and subsequent challenge
|
|
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse", true));
|
|
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
|
|
|
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 = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
|
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
|
String secretResp = http.executePairingCommand("serverchallengeresp="+bytesToHex(challengeRespEncrypted), true);
|
|
if (!NvHTTP.getXmlString(secretResp, "paired", true).equals("1")) {
|
|
http.unpair();
|
|
return PairState.FAILED;
|
|
}
|
|
|
|
// Get the server's signed secret
|
|
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret", true));
|
|
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
|
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
|
|
|
// Ensure the authenticity of the data
|
|
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
|
// Cancel the pairing process
|
|
http.unpair();
|
|
|
|
// Looks like a MITM
|
|
return PairState.FAILED;
|
|
}
|
|
|
|
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
|
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
|
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
|
// Cancel the pairing process
|
|
http.unpair();
|
|
|
|
// Probably got the wrong PIN
|
|
return PairState.PIN_WRONG;
|
|
}
|
|
|
|
// Send the server our signed secret
|
|
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
|
String clientSecretResp = http.executePairingCommand("clientpairingsecret="+bytesToHex(clientPairingSecret), true);
|
|
if (!NvHTTP.getXmlString(clientSecretResp, "paired", true).equals("1")) {
|
|
http.unpair();
|
|
return PairState.FAILED;
|
|
}
|
|
|
|
// Do the initial challenge (seems necessary for us to show as paired)
|
|
String pairChallenge = http.executePairingChallenge();
|
|
if (!NvHTTP.getXmlString(pairChallenge, "paired", true).equals("1")) {
|
|
http.unpair();
|
|
return PairState.FAILED;
|
|
}
|
|
|
|
return PairState.PAIRED;
|
|
}
|
|
|
|
private interface PairingHashAlgorithm {
|
|
int getHashLength();
|
|
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) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
}
|
|
}
|