* More work on miniserv

This commit is contained in:
Nicolas BARBOTIN 2018-02-06 02:35:41 +01:00
parent d77587ca38
commit a83601b427
11 changed files with 560 additions and 107 deletions

View File

@ -0,0 +1,135 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.miniserv;
import net.montoyo.wd.utilities.Log;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayDeque;
public abstract class AbstractClient {
private final ByteBuffer sendBuffer = ByteBuffer.allocateDirect(8192);
private final ArrayDeque<OutgoingPacket> sendQueue = new ArrayDeque<>();
private final PacketReader packetReader = new PacketReader();
private final PacketWriter packetWriter = new PacketWriter();
private final Method[] packetHandlers = new Method[PacketID.values().length];
protected SocketChannel socket;
protected Selector selector;
private SelectionKey writeKey;
public AbstractClient() {
sendBuffer.limit(0);
Method[] methods = getClass().getMethods();
for(Method m: methods) {
PacketHandler ph = m.getAnnotation(PacketHandler.class);
if(ph != null) {
if(packetHandlers[ph.value().ordinal()] != null)
Log.warning("AbstractClient: several packet handlers for %s, ignoring %s", ph.value().toString(), m.getName());
else if(m.getParameterCount() != 1 || m.getParameterTypes()[0] != DataInputStream.class)
Log.warning("AbstractClient: found invalid packet handler %s", m.getName());
else
packetHandlers[ph.value().ordinal()] = m;
}
}
for(int i = 0; i < packetHandlers.length; i++) {
if(packetHandlers[i] == null)
Log.warning("AbstractClient: no packet handler for %s", PacketID.fromInt(i).toString());
}
}
protected abstract void onWriteError();
public void readyRead(ByteBuffer bb) {
while(bb.remaining() > 0) {
if(packetReader.readFrom(bb)) { //End of packet
byte[] pkt = packetReader.getPacketData();
if(pkt != null) {
try {
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(pkt));
int pid = dis.readByte() & 0xFF;
if(pid >= packetHandlers.length)
Log.error("Caught invalid packet ID %d", pid);
else if(packetHandlers[pid] != null) {
try {
packetHandlers[pid].invoke(this, dis); //This is slow, I know... sorry
} catch(IllegalAccessException ex) {
Log.errorEx("This shouldn't have happened", ex);
} catch(InvocationTargetException ex) {
Log.warningEx("Caught exception while handling packet %d", ex.getTargetException(), pid);
}
}
} catch(IOException ex) {
Log.warningEx("IOException while trying to handle packet", ex);
}
}
packetReader.reset();
}
}
}
public void readyWrite() throws Throwable {
if(sendBuffer.remaining() > 0 || fillSendBuffer()) {
if(socket.write(sendBuffer) < 0)
onWriteError();
}
}
private boolean fillSendBuffer() {
sendBuffer.clear();
do {
if(packetWriter.writeTo(sendBuffer)) {
OutgoingPacket pkt;
synchronized(sendQueue) {
pkt = sendQueue.poll();
if(pkt == null) {
if(writeKey != null) {
writeKey.cancel();
writeKey = null;
}
return sendBuffer.remaining() > 0;
}
}
packetWriter.reset(pkt.finish());
}
} while(sendBuffer.remaining() > 0);
return true;
}
public void sendPacket(OutgoingPacket pkt) {
synchronized(sendQueue) {
sendQueue.offer(pkt);
if(writeKey == null) {
try {
writeKey = socket.register(selector, SelectionKey.OP_WRITE);
} catch(ClosedChannelException ex) {
Log.warningEx("Couldn't send packet", ex);
}
}
}
}
}

View File

@ -0,0 +1,15 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.miniserv;
public abstract class KeyParameters {
public static final int RSA_KEY_SIZE = 2048;
public static final int KEY_SIZE = 32;
public static final int CHALLENGE_SIZE = 32;
public static final String MAC_ALGORITHM = "HmacSHA256";
public static final String RSA_CIPHER = "RSA/ECB/PKCS1Padding";
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.miniserv;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PacketHandler {
PacketID value();
}

View File

@ -6,6 +6,7 @@ package net.montoyo.wd.miniserv;
public enum PacketID {
AUTHENTICATE, //C->S and S->C
PING, //C->S and S->C
BEGIN_FILE_UPLOAD, //C->S
FILE_PART, //C->S and S->C

View File

@ -0,0 +1,163 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.miniserv.client;
import net.montoyo.wd.miniserv.*;
import net.montoyo.wd.net.SMessageMiniservConnect;
import net.montoyo.wd.utilities.Log;
import net.montoyo.wd.utilities.Util;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.*;
import java.security.interfaces.RSAPublicKey;
public class Client extends AbstractClient implements Runnable {
private static Client instance;
public static Client getInstance() {
if(instance == null)
instance = new Client();
return instance;
}
private final SecureRandom random = new SecureRandom();
private KeyPair keyPair;
private byte[] key;
private SocketAddress address;
private boolean running = true;
private final ByteBuffer readBuffer = ByteBuffer.allocateDirect(8192);
private final Thread thread = new Thread(this);
private boolean authenticated = false;
public SMessageMiniservConnect beginConnection() {
if(keyPair == null) {
try {
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(KeyParameters.RSA_KEY_SIZE);
keyPair = keygen.generateKeyPair();
} catch(NoSuchAlgorithmException ex) {
Log.warningEx("RSA is unsupported?!?!", ex);
return null;
}
}
RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
return new SMessageMiniservConnect(pubKey.getModulus().toByteArray(), pubKey.getPublicExponent().toByteArray());
}
public boolean decryptKey(byte[] encKey) {
try {
Cipher cipher = Cipher.getInstance(KeyParameters.RSA_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), random);
key = cipher.doFinal(encKey);
return true;
} catch(NoSuchAlgorithmException | NoSuchPaddingException ex) {
Log.warningEx("%s unsupported...", ex, KeyParameters.RSA_CIPHER);
} catch(InvalidKeyException ex) {
Log.warningEx("The generated key is invalid...", ex);
} catch(IllegalBlockSizeException | BadPaddingException ex) {
Log.warningEx("Could not decrypt key", ex);
}
return false;
}
public byte[] authenticate(byte[] challenge) {
try {
Mac mac = Mac.getInstance(KeyParameters.MAC_ALGORITHM);
mac.init(new SecretKeySpec(key, KeyParameters.MAC_ALGORITHM));
return mac.doFinal(challenge);
} catch(NoSuchAlgorithmException ex) {
Log.warningEx("%s unsupported...", ex, KeyParameters.MAC_ALGORITHM);
} catch(InvalidKeyException ex) {
Log.warningEx("The key given by the server is invalid", ex);
}
return null;
}
public void start(SocketAddress addr) {
address = addr;
thread.start();
}
@Override
public void run() {
try {
socket = SocketChannel.open();
socket.connect(address);
socket.configureBlocking(false);
selector = Selector.open();
socket.register(selector, SelectionKey.OP_READ);
} catch(IOException ex) {
Log.errorEx("Couldn't start client", ex);
return;
}
while(running) {
try {
unsafeLoop();
} catch(Throwable t) {
Log.errorEx("MiniServ Client crashed", t);
break;
}
}
Util.silentClose(socket);
Util.silentClose(selector);
}
private void unsafeLoop() throws Throwable {
selector.select();
for(SelectionKey key: selector.selectedKeys()) {
if(key.isReadable()) {
readBuffer.clear();
int rd = socket.read(readBuffer);
if(rd <= 0) {
Log.warning("Connection was closed, stopping...");
running = false;
} else {
readBuffer.position(0);
readBuffer.limit(rd);
readyRead(readBuffer);
}
}
if(key.isWritable()) {
try {
readyWrite();
} catch(Throwable t) {
Log.errorEx("Caught error while sending data to miniserv, stopping...", t);
running = false;
}
}
}
}
@Override
protected void onWriteError() {
running = false;
Log.error("Write error, stopping...");
}
@PacketHandler(PacketID.AUTHENTICATE)
public void handleAuth(DataInputStream dis) {
//TODO: Do some stuff
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.miniserv.server;
import net.montoyo.wd.miniserv.KeyParameters;
import net.montoyo.wd.utilities.Log;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ClientManager {
private final SecureRandom random = new SecureRandom(); //SecureRandom is thread safe
private final HashMap<UUID, byte[]> keys = new HashMap<>();
private final ReentrantReadWriteLock keyLock = new ReentrantReadWriteLock();
public byte[] getClientKey(UUID uuid) {
keyLock.readLock().lock();
byte[] key = keys.get(uuid);
keyLock.readLock().unlock();
if(key == null) {
key = new byte[KeyParameters.KEY_SIZE];
random.nextBytes(key);
keyLock.writeLock().lock();
keys.put(uuid, key);
keyLock.writeLock().unlock();
}
return key;
}
public void revokeClientKey(UUID id) {
keyLock.writeLock().lock();
keys.remove(id);
keyLock.writeLock().unlock();
}
public byte[] generateChallenge() {
byte[] ret = new byte[KeyParameters.CHALLENGE_SIZE];
random.nextBytes(ret);
return ret;
}
public boolean verifyClient(UUID client, byte[] challenge, byte[] hmac) {
keyLock.readLock().lock();
byte[] key = keys.get(client);
keyLock.readLock().unlock();
if(challenge == null || hmac == null || key == null)
return false;
try {
Mac mac = Mac.getInstance(KeyParameters.MAC_ALGORITHM);
mac.init(new SecretKeySpec(key, KeyParameters.MAC_ALGORITHM));
byte[] result = mac.doFinal(challenge);
return Arrays.equals(hmac, result);
} catch(NoSuchAlgorithmException ex) {
Log.warningEx("%s is not supported?!?!", ex, KeyParameters.MAC_ALGORITHM);
} catch(InvalidKeyException ex) {
Log.warningEx("The generated key is invalid", ex);
}
return false;
}
public byte[] encryptClientKey(UUID client, byte[] modulus, byte[] exponent) {
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent));
try {
PublicKey key = KeyFactory.getInstance("RSA").generatePublic(keySpec);
Cipher cipher = Cipher.getInstance(KeyParameters.RSA_CIPHER);
cipher.init(Cipher.ENCRYPT_MODE, key, random);
return cipher.doFinal(getClientKey(client));
} catch(NoSuchAlgorithmException | NoSuchPaddingException ex) {
Log.warningEx("%s is not supported?!?!", ex, KeyParameters.RSA_CIPHER);
} catch(InvalidKeySpecException | InvalidKeyException ex) {
Log.warningEx("A client sent a malicious key", ex);
} catch(IllegalBlockSizeException | BadPaddingException ex) {
Log.warningEx("Could not encrypt client key", ex);
}
return null;
}
}

View File

@ -5,6 +5,7 @@
package net.montoyo.wd.miniserv.server;
import net.montoyo.wd.utilities.Log;
import net.montoyo.wd.utilities.Util;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
@ -32,8 +33,8 @@ public class Server extends Thread {
public void start() {
try {
server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(port));
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(port));
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
@ -81,7 +82,9 @@ public class Server extends Thread {
clientMap.put(chan, toAdd);
clientList.add(toAdd);
}
} else if(key.isReadable()) {
}
if(key.isReadable()) {
ServerClient cli = clientMap.get(key.channel());
if(cli == null)
@ -103,7 +106,9 @@ public class Server extends Thread {
cli.setShouldRemove();
}
}
} else if(key.isWritable()) {
}
if(key.isWritable()) {
ServerClient cli = clientMap.get(key.channel());
if(cli == null)
@ -130,10 +135,7 @@ public class Server extends Thread {
private void removeClient(ServerClient cli) {
clientMap.remove(cli.getChannel());
clientList.remove(cli);
try {
cli.getChannel().close();
} catch(Throwable t) {}
Util.silentClose(cli.getChannel());
}
}

View File

@ -4,95 +4,26 @@
package net.montoyo.wd.miniserv.server;
import net.montoyo.wd.miniserv.OutgoingPacket;
import net.montoyo.wd.miniserv.PacketID;
import net.montoyo.wd.miniserv.PacketReader;
import net.montoyo.wd.miniserv.PacketWriter;
import net.montoyo.wd.utilities.Log;
import net.montoyo.wd.miniserv.*;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayDeque;
public class ServerClient {
public class ServerClient extends AbstractClient {
private final SocketChannel socket;
private final Selector selector;
private SelectionKey writeKey;
private boolean remove;
private final ByteBuffer sendBuffer = ByteBuffer.allocateDirect(8192);
private final ArrayDeque<OutgoingPacket> sendQueue = new ArrayDeque<>();
private final PacketReader packetReader = new PacketReader();
private final PacketWriter packetWriter = new PacketWriter();
private boolean isAuthenticated;
ServerClient(SocketChannel s, Selector ss) {
socket = s;
selector = ss;
sendBuffer.limit(0); //Set empty
}
void readyRead(ByteBuffer bb) {
while(bb.remaining() > 0) {
if(packetReader.readFrom(bb)) { //End of packet
byte[] pkt = packetReader.getPacketData();
if(pkt != null) {
try {
handlePacket(pkt);
} catch(IOException ex) {
Log.warningEx("IOException while trying to handle packet", ex);
}
}
packetReader.reset();
}
}
}
private void handlePacket(byte[] pkt) throws IOException {
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(pkt));
PacketID pid = PacketID.fromInt(dis.readByte());
if(pid == null) {
Log.warning("Caught packet with invalid ID from client");
return;
}
OutgoingPacket response = null;
switch(pid) {
case PING:
response = new OutgoingPacket();
response.writeByte(PacketID.PING.ordinal());
break;
case BEGIN_FILE_UPLOAD:
break;
case FILE_PART:
break;
case GET_FILE:
break;
}
if(response != null)
sendPacket(response);
}
void readyWrite() throws Throwable {
if(sendBuffer.remaining() > 0 || fillSendBuffer()) {
if(socket.write(sendBuffer) < 0)
remove = true;
} else if(writeKey != null) {
writeKey.cancel();
writeKey = null;
}
@Override
protected void onWriteError() {
remove = true;
}
void setShouldRemove() {
@ -107,32 +38,16 @@ public class ServerClient {
return socket;
}
private boolean fillSendBuffer() {
sendBuffer.clear();
do {
if(packetWriter.writeTo(sendBuffer)) {
OutgoingPacket pkt = sendQueue.poll();
if(pkt == null)
return sendBuffer.remaining() > 0;
packetWriter.reset(pkt.finish());
}
} while(sendBuffer.remaining() > 0);
return true;
@PacketHandler(PacketID.AUTHENTICATE)
public void handleAuthPacket(DataInputStream dis) throws IOException {
//TODO: Do some stuff
}
public void sendPacket(OutgoingPacket pkt) {
sendQueue.offer(pkt);
if(writeKey == null) {
try {
writeKey = socket.register(selector, SelectionKey.OP_WRITE);
} catch(ClosedChannelException ex) {
Log.warningEx("Couldn't send packet", ex);
}
}
@PacketHandler(PacketID.PING)
public void handlePing(DataInputStream dis) {
OutgoingPacket pkt = new OutgoingPacket();
pkt.writeByte(PacketID.PING.ordinal());
sendPacket(pkt);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.net;
import io.netty.buffer.ByteBuf;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;
import net.minecraftforge.fml.relauncher.Side;
@Message(messageId = 11, side = Side.CLIENT)
public class CMessageMiniservKey implements IMessage {
private byte[] encryptedKey;
public CMessageMiniservKey() {
}
public CMessageMiniservKey(byte[] key) {
encryptedKey = key;
}
@Override
public void fromBytes(ByteBuf buf) {
encryptedKey = new byte[buf.readByte() & 0xFF];
buf.readBytes(encryptedKey);
}
@Override
public void toBytes(ByteBuf buf) {
buf.writeByte(encryptedKey.length);
buf.writeBytes(encryptedKey);
}
public static class Handler implements IMessageHandler<CMessageMiniservKey, IMessage> {
@Override
public IMessage onMessage(CMessageMiniservKey message, MessageContext ctx) {
//TODO: Start client thread
return null;
}
}
}

View File

@ -11,7 +11,7 @@ import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
public class Messages {
public abstract class Messages {
private static DefaultHandler DEFAULT_HANDLER = new DefaultHandler();
private static Class<? extends IMessage>[] messages;
@ -27,6 +27,8 @@ public class Messages {
l.add(SMessagePadCtrl.class);
l.add(SMessageRedstoneCtrl.class);
l.add(CMessageJSResponse.class);
l.add(SMessageMiniservConnect.class);
l.add(CMessageMiniservKey.class);
messages = l.toArray(new Class[0]);
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.net;
import io.netty.buffer.ByteBuf;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;
import net.minecraftforge.fml.relauncher.Side;
@Message(messageId = 10, side = Side.SERVER)
public class SMessageMiniservConnect implements IMessage {
private byte[] modulus;
private byte[] exponent;
public SMessageMiniservConnect() {
}
public SMessageMiniservConnect(byte[] mod, byte[] exp) {
modulus = mod;
exponent = exp;
}
@Override
public void fromBytes(ByteBuf buf) {
int sz = buf.readByte() & 0xFF;
modulus = new byte[sz];
buf.readBytes(modulus);
sz = buf.readByte() & 0xFF;
exponent = new byte[sz];
buf.readBytes(exponent);
}
@Override
public void toBytes(ByteBuf buf) {
buf.writeByte(modulus.length);
buf.writeBytes(modulus);
buf.writeByte(exponent.length);
buf.writeBytes(exponent);
}
public static class Handler implements IMessageHandler<SMessageMiniservConnect, IMessage> {
@Override
public IMessage onMessage(SMessageMiniservConnect message, MessageContext ctx) {
//TODO: Generate key
return null;
}
}
}