2015-07-20 16:16:58 +02:00
/ *
* Copyright 2010 dorkbox , llc
*
* Licensed under the Apache License , Version 2 . 0 ( the " License " ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an " AS IS " BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2014-08-20 23:44:59 +02:00
package dorkbox.network.connection.registration.remote ;
2018-02-16 21:02:05 +01:00
import java.math.BigInteger ;
import java.net.InetSocketAddress ;
import java.security.SecureRandom ;
import java.util.concurrent.TimeUnit ;
2019-06-13 20:36:46 +02:00
import javax.crypto.spec.SecretKeySpec ;
2018-02-16 21:02:05 +01:00
import org.bouncycastle.crypto.AsymmetricCipherKeyPair ;
import org.bouncycastle.crypto.BasicAgreement ;
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement ;
import org.bouncycastle.crypto.digests.SHA384Digest ;
import org.bouncycastle.crypto.params.ECPublicKeyParameters ;
import org.bouncycastle.jce.ECNamedCurveTable ;
import org.bouncycastle.jce.spec.ECParameterSpec ;
import org.bouncycastle.util.Arrays ;
import com.esotericsoftware.kryo.KryoException ;
import com.esotericsoftware.kryo.io.Input ;
import com.esotericsoftware.kryo.io.Output ;
2019-06-14 20:36:26 +02:00
import dorkbox.network.connection.EndPoint ;
2019-01-16 11:43:46 +01:00
import dorkbox.network.connection.RegistrationWrapper.STATE ;
2019-06-14 20:36:26 +02:00
import dorkbox.network.connection.RegistrationWrapperServer ;
2014-08-20 23:44:59 +02:00
import dorkbox.network.connection.registration.MetaChannel ;
2018-02-16 21:02:05 +01:00
import dorkbox.network.connection.registration.Registration ;
import dorkbox.util.crypto.CryptoECC ;
import dorkbox.util.serialization.EccPublicKeySerializer ;
import io.netty.channel.Channel ;
2019-06-13 22:08:50 +02:00
import io.netty.channel.ChannelHandler ;
2018-04-01 14:51:13 +02:00
import io.netty.channel.ChannelHandlerContext ;
import io.netty.channel.EventLoopGroup ;
2014-08-20 23:44:59 +02:00
2015-07-20 14:18:34 +02:00
public
2019-06-14 20:36:26 +02:00
class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler < RegistrationWrapperServer > {
2018-02-16 21:02:05 +01:00
private static final long ECDH_TIMEOUT = TimeUnit . MINUTES . toNanos ( 10L ) ; // 10 minutes in nanoseconds
private static final ECParameterSpec eccSpec = ECNamedCurveTable . getParameterSpec ( CryptoECC . curve25519 ) ;
private final Object ecdhKeyLock = new Object ( ) ;
private AsymmetricCipherKeyPair ecdhKeyPair ;
private volatile long ecdhTimeout = System . nanoTime ( ) ;
2014-08-20 23:44:59 +02:00
2019-06-14 20:36:26 +02:00
RegistrationRemoteHandlerServer ( final String name , final RegistrationWrapperServer registrationWrapper , final EventLoopGroup workerEventLoop ) {
2018-04-01 14:51:13 +02:00
super ( name , registrationWrapper , workerEventLoop ) ;
2014-08-20 23:44:59 +02:00
}
2019-06-14 20:36:26 +02:00
/ * *
* STEP 1 : Channel is first created
* /
@Override
protected
void initChannel ( final Channel channel ) {
// check to see if this connection is permitted.
final InetSocketAddress remoteAddress = ( InetSocketAddress ) channel . remoteAddress ( ) ;
if ( ! registrationWrapper . acceptRemoteConnection ( remoteAddress ) ) {
StringBuilder stringBuilder = new StringBuilder ( ) ;
2020-07-03 01:45:18 +02:00
EndPoint . Companion . getHostDetails ( stringBuilder , remoteAddress ) ;
2019-06-14 20:36:26 +02:00
logger . error ( " Remote connection [{}] is not permitted! Aborting connection process. " , stringBuilder . toString ( ) ) ;
shutdown ( channel , 0 ) ;
return ;
}
super . initChannel ( channel ) ;
}
2014-08-20 23:44:59 +02:00
/ * *
2015-07-20 14:18:34 +02:00
* @return the direction that traffic is going to this handler ( " <== " or " ==> " )
2014-08-20 23:44:59 +02:00
* /
@Override
2015-07-20 14:18:34 +02:00
protected
String getConnectionDirection ( ) {
return " <== " ;
2014-08-20 23:44:59 +02:00
}
2018-02-16 21:02:05 +01:00
2014-08-20 23:44:59 +02:00
/ * *
2018-02-16 21:02:05 +01:00
* Rotates the ECDH key every 10 minutes , as this is a VERY expensive calculation to keep on doing for every connection .
2014-08-20 23:44:59 +02:00
* /
2018-02-16 21:02:05 +01:00
private
AsymmetricCipherKeyPair getEchdKeyOnRotate ( final SecureRandom secureRandom ) {
if ( this . ecdhKeyPair = = null | | System . nanoTime ( ) - this . ecdhTimeout > ECDH_TIMEOUT ) {
synchronized ( this . ecdhKeyLock ) {
this . ecdhTimeout = System . nanoTime ( ) ;
this . ecdhKeyPair = CryptoECC . generateKeyPair ( eccSpec , secureRandom ) ;
}
}
return this . ecdhKeyPair ;
}
/ *
* UDP has a VERY limited size , so we have to break up registration steps into the following
* 1 ) session ID = = 0 - > exchange session ID and public keys ( session ID ! = 0 now )
* 2 ) session ID ! = 0 - > establish ECDH shared secret as AES key / iv
* /
@SuppressWarnings ( " Duplicates " )
2018-04-01 14:51:13 +02:00
void readServer ( final ChannelHandlerContext context , final Channel channel , final Registration registration , final String type , final MetaChannel metaChannel ) {
final InetSocketAddress remoteAddress = ( InetSocketAddress ) channel . remoteAddress ( ) ;
2018-02-16 21:02:05 +01:00
// IN: session ID == 0 (start of new connection)
// OUT: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are)
if ( registration . sessionID = = 0 ) {
// whoa! Didn't send valid public key info!
if ( invalidPublicKey ( registration , type ) ) {
shutdown ( channel , registration . sessionID ) ;
return ;
}
// 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.
if ( invalidRemoteAddress ( metaChannel , registration , type , remoteAddress ) ) {
// whoa! abort since something messed up! (log and recording if key changed happens inside of validate method)
shutdown ( channel , registration . sessionID ) ;
return ;
}
// save off remote public key. This is ALWAYS the same, where the ECDH changes every time...
metaChannel . publicKey = registration . publicKey ;
// tell the client to continue it's registration process.
Registration outboundRegister = new Registration ( metaChannel . sessionId ) ;
outboundRegister . publicKey = registrationWrapper . getPublicKey ( ) ;
outboundRegister . eccParameters = CryptoECC . generateSharedParameters ( registrationWrapper . getSecureRandom ( ) ) ;
channel . writeAndFlush ( outboundRegister ) ;
return ;
}
// IN: remote ECDH shared payload
// OUT: server ECDH shared payload
2019-06-13 20:36:46 +02:00
if ( metaChannel . secretKey = = null ) {
2018-02-16 21:02:05 +01:00
/ *
* Diffie - Hellman - Merkle key exchange for the AES key
* http : //en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
* /
// the ECDH key will ROTATE every 10 minutes, since generating it for EVERY connection is expensive
// and since we are combining ECDHE+ECC public/private keys for each connection, other
// connections cannot break someone else's connection, since they are still protected by their own private keys.
metaChannel . ecdhKey = getEchdKeyOnRotate ( registrationWrapper . getSecureRandom ( ) ) ;
byte [ ] ecdhPubKeyBytes = java . util . Arrays . copyOfRange ( registration . payload , 0 , registration . payload . length ) ;
ECPublicKeyParameters ecdhPubKey ;
try {
ecdhPubKey = EccPublicKeySerializer . read ( new Input ( ecdhPubKeyBytes ) ) ;
} catch ( KryoException e ) {
logger . error ( " Invalid decode of ECDH public key. Aborting. " ) ;
shutdown ( channel , registration . sessionID ) ;
return ;
}
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 ) ;
2019-06-13 20:36:46 +02:00
byte [ ] key = Arrays . copyOfRange ( digest , 0 , 32 ) ; // 256bit keysize (32 bytes)
metaChannel . secretKey = new SecretKeySpec ( key , " AES " ) ;
2018-02-16 21:02:05 +01:00
Registration outboundRegister = new Registration ( metaChannel . sessionId ) ;
Output output = new Output ( 1024 ) ;
EccPublicKeySerializer . write ( output , ( ECPublicKeyParameters ) metaChannel . ecdhKey . getPublic ( ) ) ;
outboundRegister . payload = output . toBytes ( ) ;
channel . writeAndFlush ( outboundRegister ) ;
return ;
}
2018-04-01 14:51:13 +02:00
// NOTE: if we have more registrations, we will "bounce back" that status so the client knows what to do.
2018-02-16 21:02:05 +01:00
// IN: hasMore=true if we have more registrations to do, false otherwise
2019-06-13 22:08:50 +02:00
// Some cases we want to SKIP encryption (ie, loopback or specific IP/CIDR addresses)
// OTHERWISE ALWAYS upgrade the connection at this point.
2019-01-11 09:56:36 +01:00
// IN: upgraded=false if we haven't upgraded to encryption yet (this will always be the case right after encryption is setup)
2018-04-01 14:51:13 +02:00
if ( ! registration . upgraded ) {
2019-06-13 22:08:50 +02:00
// this is the last protocol registered
if ( ! registration . hasMore ) {
// this can ONLY be created when all protocols are registered!
// this must happen before we verify class registrations.
metaChannel . connection = this . registrationWrapper . connection0 ( metaChannel , remoteAddress ) ;
if ( metaChannel . tcpChannel ! = null ) {
2020-07-03 01:45:18 +02:00
// metaChannel.tcpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
2019-06-13 22:08:50 +02:00
}
if ( metaChannel . udpChannel ! = null ) {
2020-07-03 01:45:18 +02:00
// metaChannel.udpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
2019-06-13 22:08:50 +02:00
}
}
// If we are loopback or the client is a specific IP/CIDR address, then we do things differently. The LOOPBACK address will never encrypt or compress the traffic.
byte upgradeType = registrationWrapper . getConnectionUpgradeType ( remoteAddress ) ;
registration . upgradeType = upgradeType ;
// upgrade the connection to a none/compression/encrypted connection
upgradeDecoders ( upgradeType , channel , metaChannel ) ;
2018-02-16 21:02:05 +01:00
2018-04-01 14:51:13 +02:00
// bounce back to the client so it knows we received it
channel . write ( registration ) ;
2018-02-16 21:02:05 +01:00
2019-06-13 22:08:50 +02:00
upgradeEncoders ( upgradeType , channel , metaChannel ) ;
2018-04-01 14:51:13 +02:00
2019-06-13 22:08:50 +02:00
logChannelUpgrade ( upgradeType , channel , metaChannel ) ;
2018-04-01 14:51:13 +02:00
channel . flush ( ) ;
return ;
2018-02-16 21:02:05 +01:00
}
2019-06-13 22:08:50 +02:00
//
//
// we only get this when we are 100% done with encrypting/etc the connections
//
//
// upgraded=true when the client will send their class registration data. VERIFY IT IS CORRECT!
2019-01-16 11:43:46 +01:00
STATE state = registrationWrapper . verifyClassRegistration ( metaChannel , registration ) ;
if ( state = = STATE . ERROR ) {
// abort! There was an error
shutdown ( channel , registration . sessionID ) ;
return ;
2019-01-11 09:56:36 +01:00
}
2019-01-16 11:43:46 +01:00
else if ( state = = STATE . WAIT ) {
return ;
2019-01-11 09:56:36 +01:00
}
2019-01-16 11:43:46 +01:00
// else, continue.
2019-01-11 09:56:36 +01:00
2018-04-01 14:51:13 +02:00
//
//
2019-06-13 22:08:50 +02:00
// we only get this when we are 100% done with validation of class registrations. The last protocol to register gets us here.
2018-04-01 14:51:13 +02:00
//
//
2019-06-13 22:08:50 +02:00
// remove ourselves from handling any more messages, because we are done.
ChannelHandler handler = context . handler ( ) ;
channel . pipeline ( ) . remove ( handler ) ;
// since only the LAST registration gets here, setup other ones as well (since they are no longer needed)
if ( channel = = metaChannel . tcpChannel & & metaChannel . udpChannel ! = null ) {
// the "other" channel is the UDP channel that we have to cleanup
metaChannel . udpChannel . pipeline ( ) . remove ( RegistrationRemoteHandlerServerUDP . class ) ;
}
else if ( channel = = metaChannel . udpChannel & & metaChannel . tcpChannel ! = null ) {
// the "other" channel is the TCP channel that we have to cleanup
metaChannel . tcpChannel . pipeline ( ) . remove ( RegistrationRemoteHandlerServerTCP . class ) ;
}
2018-04-04 15:01:08 +02:00
// remove the ConnectionWrapper (that was used to upgrade the connection) and cleanup the pipeline
2019-06-13 22:08:50 +02:00
Runnable preConnectRunnable = new Runnable ( ) {
2018-04-04 15:01:08 +02:00
@Override
public
void run ( ) {
2019-06-13 22:08:50 +02:00
// this method BEFORE the "onConnect()" runs and only after all of the channels have be correctly updated
2018-04-04 15:01:08 +02:00
2019-06-13 22:08:50 +02:00
// this tells the client we are ready to connect (we just bounce back "upgraded" over TCP, preferably).
// only the FIRST one to arrive at the client will actually setup the pipeline
Registration reg = new Registration ( registration . sessionID ) ;
reg . upgraded = true ;
// there is a risk of UDP losing the packet, so if we can send via TCP, then we do so.
2018-04-04 15:01:08 +02:00
if ( metaChannel . tcpChannel ! = null ) {
logger . trace ( " Sending TCP upgraded command " ) ;
metaChannel . tcpChannel . writeAndFlush ( reg ) ;
}
2019-06-13 22:08:50 +02:00
else if ( metaChannel . udpChannel ! = null ) {
2018-04-04 15:01:08 +02:00
logger . trace ( " Sending UDP upgraded command " ) ;
metaChannel . udpChannel . writeAndFlush ( reg ) ;
}
2019-06-13 22:08:50 +02:00
else {
logger . error ( " This shouldn't happen! " ) ;
}
2018-04-04 15:01:08 +02:00
}
2019-06-13 22:08:50 +02:00
} ;
cleanupPipeline ( channel , metaChannel , preConnectRunnable , null ) ;
2014-08-20 23:44:59 +02:00
}
2015-07-20 14:18:34 +02:00
}