/*
* Copyright 2013 MovingBlocks
*
* 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.
*/
package org.terasology.network.internal;
import com.google.common.primitives.Bytes;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.config.ClientIdentity;
import org.terasology.config.Config;
import org.terasology.registry.CoreRegistry;
import org.terasology.identity.IdentityConstants;
import org.terasology.identity.PrivateIdentityCertificate;
import org.terasology.identity.PublicIdentityCertificate;
import org.terasology.protobuf.NetData;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* Authentication handler for the client end of the authentication handshake.
*/
public class ClientHandshakeHandler extends SimpleChannelUpstreamHandler {
private static final Logger logger = LoggerFactory.getLogger(ClientHandshakeHandler.class);
private static final String AUTHENTICATION_FAILURE = "Authentication failure";
private Config config = CoreRegistry.get(Config.class);
private JoinStatusImpl joinStatus;
private byte[] serverRandom;
private byte[] clientRandom;
private byte[] masterSecret;
private NetData.HandshakeHello serverHello;
private NetData.HandshakeHello clientHello;
private boolean requestedCertificate;
private ClientIdentity identity;
private PublicIdentityCertificate serverCertificate;
public ClientHandshakeHandler(JoinStatusImpl joinStatus) {
this.joinStatus = joinStatus;
}
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
super.channelOpen(ctx, e);
joinStatus.setCurrentActivity("Authenticating with server");
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
NetData.NetMessage message = (NetData.NetMessage) e.getMessage();
if (message.hasHandshakeHello()) {
processServerHello(message.getHandshakeHello(), ctx);
} else if (message.hasProvisionIdentity()) {
processNewIdentity(message.getProvisionIdentity(), ctx);
} else if (message.hasHandshakeVerification()) {
processHandshakeVerification(message.getHandshakeVerification(), ctx);
}
}
private void processHandshakeVerification(NetData.HandshakeVerification handshakeVerification, ChannelHandlerContext ctx) {
logger.info("Received server verification");
if (serverHello == null || clientHello == null) {
logger.error("Received server verification without requesting it: cancelling authentication");
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
return;
}
if (!serverCertificate.verify(HandshakeCommon.getSignatureData(serverHello, clientHello), handshakeVerification.getSignature().toByteArray())) {
logger.error("Server failed verification: cancelling authentication");
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
return;
}
// And we're authenticated.
ctx.getPipeline().remove(this);
channelAuthenticated(ctx);
}
private void processNewIdentity(NetData.ProvisionIdentity provisionIdentity, ChannelHandlerContext ctx) {
logger.info("Received identity from server");
if (!requestedCertificate) {
logger.error("Received identity without requesting it: cancelling authentication");
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
return;
}
try {
byte[] decryptedCert = null;
try {
SecretKeySpec key = HandshakeCommon.generateSymmetricKey(masterSecret, clientRandom, serverRandom);
Cipher cipher = Cipher.getInstance(IdentityConstants.SYMMETRIC_ENCRYPTION_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
decryptedCert = cipher.doFinal(provisionIdentity.getEncryptedCertificates().toByteArray());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
logger.error("Unexpected error decrypting received certificate, ending connection attempt", e);
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
return;
}
NetData.CertificateSet certificateSet = NetData.CertificateSet.parseFrom(decryptedCert);
NetData.Certificate publicCertData = certificateSet.getPublicCertificate();
PublicIdentityCertificate publicCert = NetMessageUtil.convert(publicCertData);
if (!publicCert.verifySignedBy(serverCertificate)) {
logger.error("Received invalid certificate, not signed by server: cancelling authentication");
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
return;
}
BigInteger exponent = new BigInteger(certificateSet.getPrivateExponent().toByteArray());
PrivateIdentityCertificate privateCert = new PrivateIdentityCertificate(publicCert.getModulus(), exponent);
// Store identity for later use
identity = new ClientIdentity(publicCert, privateCert);
config.getSecurity().addIdentity(serverCertificate, identity);
config.save();
// And we're authenticated.
ctx.getPipeline().remove(this);
channelAuthenticated(ctx);
} catch (InvalidProtocolBufferException e) {
logger.error("Received invalid certificate data: cancelling authentication", e);
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
}
}
private void channelAuthenticated(ChannelHandlerContext ctx) {
ctx.getChannel().write(NetData.NetMessage.newBuilder()
.setServerInfoRequest(NetData.ServerInfoRequest.newBuilder()).build());
joinStatus.setCurrentActivity("Requesting server info");
}
private void processServerHello(NetData.HandshakeHello helloMessage, ChannelHandlerContext ctx) {
if (serverHello == null) {
logger.info("Received Server Hello");
serverHello = helloMessage;
serverRandom = helloMessage.getRandom().toByteArray();
NetData.Certificate cert = helloMessage.getCertificate();
serverCertificate = NetMessageUtil.convert(cert);
if (!serverCertificate.verifySelfSigned()) {
logger.error("Received invalid server certificate: cancelling authentication");
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
return;
}
clientRandom = new byte[IdentityConstants.SERVER_CLIENT_RANDOM_LENGTH];
identity = config.getSecurity().getIdentity(serverCertificate);
if (identity == null) {
requestIdentity(ctx);
} else {
sendCertificate(helloMessage, ctx);
}
} else {
logger.error("Received multiple hello messages from server: cancelling authentication");
joinStatus.setErrorMessage(AUTHENTICATION_FAILURE);
ctx.getChannel().close();
}
}
private void sendCertificate(NetData.HandshakeHello helloMessage, ChannelHandlerContext ctx) {
logger.info("Sending client certificate");
PublicIdentityCertificate pubClientCert = identity.getPlayerPublicCertificate();
clientHello = NetData.HandshakeHello.newBuilder()
.setRandom(ByteString.copyFrom(clientRandom))
.setCertificate(NetMessageUtil.convert(pubClientCert))
.setTimestamp(System.currentTimeMillis())
.build();
byte[] dataToSign = Bytes.concat(helloMessage.toByteArray(), clientHello.toByteArray());
byte[] signature = identity.getPlayerPrivateCertificate().sign(dataToSign);
ctx.getChannel().write(NetData.NetMessage.newBuilder()
.setHandshakeHello(clientHello)
.setHandshakeVerification(NetData.HandshakeVerification.newBuilder()
.setSignature(ByteString.copyFrom(signature)))
.build());
}
private void requestIdentity(ChannelHandlerContext ctx) {
logger.info("No existing identity, requesting one");
byte[] preMasterSecret = new byte[IdentityConstants.PREMASTER_SECRET_LENGTH];
new SecureRandom().nextBytes(preMasterSecret);
byte[] encryptedPreMasterSecret = serverCertificate.encrypt(preMasterSecret);
masterSecret = HandshakeCommon.generateMasterSecret(preMasterSecret, clientRandom, serverRandom);
ctx.getChannel().write(NetData.NetMessage.newBuilder()
.setNewIdentityRequest(NetData.NewIdentityRequest.newBuilder()
.setPreMasterSecret(ByteString.copyFrom(encryptedPreMasterSecret))
.setRandom(ByteString.copyFrom(clientRandom)))
.build());
requestedCertificate = true;
}
}