Network/Dorkbox-Network/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerCl...

337 lines
15 KiB
Java

package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.bouncycastle.crypto.BasicAgreement;
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
import org.bouncycastle.crypto.digests.SHA384Digest;
import org.bouncycastle.crypto.engines.IESEngine;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
import dorkbox.util.crypto.serialization.EccPublicKeySerializer;
public class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient {
private static final String DELETE_IP = "eleteIP"; // purposefully missing the "D", since that is a system parameter, which starts with "-D"
private ThreadLocal<IESEngine> eccEngineLocal = new ThreadLocal<IESEngine>();
private final static ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(Crypto.ECC.p521_curve);
public RegistrationRemoteHandlerClientTCP(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
// check to see if we need to delete an IP address as commanded from the user prompt
String ipAsString = System.getProperty(DELETE_IP);
if (ipAsString != null) {
System.setProperty(DELETE_IP, "");
byte[] address = null;
try {
String[] split = ipAsString.split("\\.");
if (split.length == 4) {
address = new byte[4];
for (int i=0;i<split.length;i++) {
int asInt = Integer.parseInt(split[i]);
if (asInt >= 0 && asInt <= 255) {
address[i] = (byte) Integer.parseInt(split[i]);
} else {
address = null;
break;
}
}
}
} catch (Exception e) {
address = null;
}
if (address != null) {
try {
registrationWrapper.removeRegisteredServerKey(address);
} catch (SecurityException e) {
this.logger.error(e.getMessage(), e);
}
}
}
// end command
}
private final IESEngine getEccEngine() {
IESEngine iesEngine = this.eccEngineLocal.get();
if (iesEngine == null) {
iesEngine = Crypto.ECC.createEngine();
this.eccEngineLocal.set(iesEngine);
}
return iesEngine;
}
/**
* STEP 1: Channel is first created
*/
@Override
protected void initChannel(Channel channel) {
this.logger.trace("Channel registered: {}", channel.getClass().getSimpleName());
// TCP & UDT
// use the default.
super.initChannel(channel);
}
/**
* STEP 2: Channel is now active. Start the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (this.logger.isDebugEnabled()) {
super.channelActive(context);
}
Channel channel = context.channel();
// look to see if we already have a connection (in progress) for the destined IP address.
// Note: our CHANNEL MAP can only have one item at a time, since we do NOT RELEASE the registration lock until it's complete!!
// The ORDER has to be TCP (always) -> UDP (optional) -> UDT (optional)
// TCP
MetaChannel metaChannel = new MetaChannel();
metaChannel.tcpChannel = channel;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
channelMap.put(channel.hashCode(), metaChannel);
} finally {
this.registrationWrapper.releaseChannelMap();
}
this.logger.trace("Start new TCP Connection. Sending request to server");
Registration registration = new Registration();
registration.publicKey = this.registrationWrapper.getPublicKey();
// client start the handshake with a registration packet
channel.writeAndFlush(registration);
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
if (message instanceof Registration) {
// make sure this connection was properly registered in the map. (IT SHOULD BE)
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
metaChannel = channelMap.get(channel.hashCode());
} finally {
this.registrationWrapper.releaseChannelMap();
}
if (metaChannel != null) {
metaChannel.updateTcpRoundTripTime();
Registration registration = (Registration) message;
if (metaChannel.connectionID == null) {
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
InetSocketAddress tcpRemoteServer = (InetSocketAddress) channel.remoteAddress();
boolean valid = this.registrationWrapper.validateRemoteServerAddress(tcpRemoteServer, registration.publicKey);
if (!valid) {
//whoa! abort since something messed up! (log happens inside of validate method)
String hostAddress = tcpRemoteServer.getAddress().getHostAddress();
this.logger.error("Invalid ECC public key for server IP {} during handshake. WARNING. The server has changed!", hostAddress);
this.logger.error("Fix by adding the argument -D{} {} when starting the client.", DELETE_IP, hostAddress);
metaChannel.changedRemoteKey = true;
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// setup crypto state
IESEngine decrypt = getEccEngine();
byte[] aesKeyBytes = Crypto.ECC.decrypt(decrypt, this.registrationWrapper.getPrivateKey(), registration.publicKey, registration.eccParameters,
registration.aesKey);
if (aesKeyBytes.length != 32) {
this.logger.error("Invalid decryption of aesKey. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// now decrypt payload using AES
byte[] payload = Crypto.AES.decrypt(getAesEngine(), aesKeyBytes, registration.aesIV, registration.payload);
if (payload.length == 0) {
this.logger.error("Invalid decryption of payload. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
OptimizeUtils optimizeUtils = OptimizeUtils.get();
if (!optimizeUtils.canReadInt(payload)) {
this.logger.error("Invalid decryption of connection ID. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
metaChannel.connectionID = optimizeUtils.readInt(payload, true);
int intLength = optimizeUtils.intLength(metaChannel.connectionID, true);
/*
* Diffie-Hellman-Merkle key exchange for the AES key
* see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
*/
byte[] ecdhPubKeyBytes = Arrays.copyOfRange(payload, intLength, payload.length);
ECPublicKeyParameters ecdhPubKey = EccPublicKeySerializer.read(new Input(ecdhPubKeyBytes));
if (ecdhPubKey == null) {
this.logger.error("Invalid decode of ecdh public key. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// It is OK that we generate a new ECC keypair for ECDHE everytime that we connect. The server rotates keys every XXXX
// seconds, since this step is expensive.
metaChannel.ecdhKey = Crypto.ECC.generateKeyPair(eccSpec, new SecureRandom());
// register the channel!
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
channelMap.put(metaChannel.connectionID, metaChannel);
} finally {
this.registrationWrapper.releaseChannelMap();
}
metaChannel.publicKey = registration.publicKey;
// now save our shared AES keys
BasicAgreement agreement = new ECDHCBasicAgreement();
agreement.init(metaChannel.ecdhKey.getPrivate());
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
// now we setup our AES key based on our shared secret! (from ECDH)
// the shared secret is different each time a connection is made
byte[] keySeed = shared.toByteArray();
SHA384Digest sha384 = new SHA384Digest();
byte[] digest = new byte[sha384.getDigestSize()];
sha384.update(keySeed, 0, keySeed.length);
sha384.doFinal(digest, 0);
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 48); // 128bit blocksize (16 bytes)
// abort if something messed up!
if (metaChannel.aesKey.length != 32) {
this.logger.error("Fatal error trying to use AES key (wrong key length).");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
Registration register = new Registration();
// encrypt the ECDH public key using our previous AES info
Output output = new Output(1024);
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
byte[] pubKeyAsBytes = output.toBytes();
register.payload = Crypto.AES.encrypt(getAesEngine(), aesKeyBytes, registration.aesIV, pubKeyAsBytes);
channel.write(register);
ReferenceCountUtil.release(message);
return;
}
// else, we are further along in our registration process
// REGISTRATION CONNECTED!
else {
if (metaChannel.connection == null) {
// STEP 1: do we have our aes keys?
if (metaChannel.ecdhKey != null) {
// wipe out our ECDH value.
metaChannel.ecdhKey = null;
// notify the client that we are ready to continue registering other session protocols (bootstraps)
boolean isDoneWithRegistration = this.registrationWrapper.continueRegistration0();
// tell the server we are done, and to setup crypto on it's side
if (isDoneWithRegistration) {
channel.write(registration);
// re-sync the TCP delta round trip time
metaChannel.updateTcpRoundTripTime();
}
// if we are NOT done, then we will continue registering other protocols, so do nothing else here.
}
// we only get this when we are 100% done with the registration of all connection types.
else {
setupConnectionCrypto(metaChannel);
// AES ENCRPYTION NOW USED
// this sets up the pipeline for the client, so all the necessary handlers are ready to go
establishConnection(metaChannel);
setupConnection(metaChannel);
final MetaChannel metaChannel2 = metaChannel;
// wait for a "round trip" amount of time, then notify the APP!
channel.eventLoop().schedule(new Runnable() {
@Override
public void run() {
RegistrationRemoteHandlerClientTCP.this.logger.trace("Notify Connection");
notifyConnection(metaChannel2);
}}, metaChannel.getNanoSecBetweenTCP() * 2, TimeUnit.NANOSECONDS);
}
}
}
} else {
// this means that UDP beat us to the "punch", and notified before we did. (notify removes all the entries from the map)
}
}
else {
this.logger.error("Error registering TCP with remote server!");
shutdown(this.registrationWrapper, channel);
}
ReferenceCountUtil.release(message);
}
}