Implement full handshake and login over DMAP2.0

This commit is contained in:
Tobias Eidelpes 2020-12-29 16:45:32 +01:00
parent e311064c48
commit a9eab7cfb9
4 changed files with 185 additions and 20 deletions

View File

@ -77,7 +77,9 @@ public class MessageClient implements IMessageClient, Runnable {
String input = null; String input = null;
String message = null; String message = null;
if (!(input = this.dmapIn.readLine()).startsWith("ok DMAP2.0")) input = dmapIn.readLine();
logger.info("Received message from server: " + input);
if (!input.startsWith("ok DMAP2.0"))
shutdown(); shutdown();
startSecure(); startSecure();
@ -110,7 +112,9 @@ public class MessageClient implements IMessageClient, Runnable {
String componentId; String componentId;
PublicKey serverPublicKey; PublicKey serverPublicKey;
this.dmapOut.println("startsecure"); this.dmapOut.println("startsecure");
logger.info("Sent command 'startsecure'");
input = this.dmapIn.readLine(); input = this.dmapIn.readLine();
logger.info("Server's response: " + input);
if (input.startsWith("ok") && (input.split("\\s+").length == 2)) { if (input.startsWith("ok") && (input.split("\\s+").length == 2)) {
// Get the component-id from the server // Get the component-id from the server
componentId = input.split("\\s+")[1]; componentId = input.split("\\s+")[1];
@ -127,12 +131,14 @@ public class MessageClient implements IMessageClient, Runnable {
logger.info("Server's Public Key: " + serverPublicKey); logger.info("Server's Public Key: " + serverPublicKey);
String clientChallenge = generateChallengeMessage(serverPublicKey); String clientChallenge = generateChallengeMessage(serverPublicKey);
// Send clientChallenge to server // Send clientChallenge to server
logger.info("Send clientchallenge to Server: " + clientChallenge);
this.dmapOut.println(clientChallenge); this.dmapOut.println(clientChallenge);
// Receive AES encrypted message saying "ok <client-challenge>" // Receive AES encrypted message saying "ok <client-challenge>"
String response = this.dmapIn.readLine(); String response = this.dmapIn.readLine();
// Compare received client challenge with generated client challenge // Compare received client challenge with generated client challenge
verifyChallenge(response); verifyChallenge(response);
// Answer with AES encrypted "ok" if matching and use AES cipher for subsequent communication // Answer with AES encrypted "ok" if matching and use AES cipher for subsequent communication
logger.info("Send ciphered 'ok' to Server: " + getAesCiphertext("ok"));
this.dmapOut.println(getAesCiphertext("ok")); this.dmapOut.println(getAesCiphertext("ok"));
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalBlockSizeException | BadPaddingException | FailedVerificationException e) { } catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalBlockSizeException | BadPaddingException | FailedVerificationException e) {
logger.severe(e.getMessage()); logger.severe(e.getMessage());
@ -146,6 +152,7 @@ public class MessageClient implements IMessageClient, Runnable {
/** /**
* Takes a plaintext message, encrypts and encodes it and returns the encoded ciphertext ready for sending. * Takes a plaintext message, encrypts and encodes it and returns the encoded ciphertext ready for sending.
*
* @param message Plaintext message * @param message Plaintext message
* @return B64 encoded and AES encrypted ciphertext * @return B64 encoded and AES encrypted ciphertext
*/ */
@ -156,6 +163,7 @@ public class MessageClient implements IMessageClient, Runnable {
/** /**
* Takes an AES encrypted message, decrypts and decodes it and returns the plaintext. * Takes an AES encrypted message, decrypts and decodes it and returns the plaintext.
*
* @param message B64 encoded and AES encrypted message * @param message B64 encoded and AES encrypted message
* @return B64 decoded and AES decrypted plaintext * @return B64 decoded and AES decrypted plaintext
*/ */
@ -178,11 +186,12 @@ public class MessageClient implements IMessageClient, Runnable {
// Decode <client-challenge> // Decode <client-challenge>
if (plainText.split("\\s+").length != 2) if (plainText.split("\\s+").length != 2)
shutdown(); shutdown();
String clientChallenge = new String(Base64.getDecoder().decode(plainText.split("\\s+")[1])); byte[] clientChallenge = Base64.getDecoder().decode(plainText.split("\\s+")[1]);
// Compare received challenge with sent // Compare received challenge with sent
if (!clientChallenge.equals(new String(this.challenge))) if (!Arrays.equals(clientChallenge, this.challenge))
throw new FailedVerificationException("Server's clientChallenge " + clientChallenge + " does not match sent clientChallenge " + new String(this.challenge)); throw new FailedVerificationException("Server's clientChallenge " + new String(clientChallenge) +
" does not match sent clientChallenge " + new String(this.challenge));
} }
private SecretKeySpec generateSecretKey() { private SecretKeySpec generateSecretKey() {
@ -205,7 +214,7 @@ public class MessageClient implements IMessageClient, Runnable {
return new IvParameterSpec(iv); return new IvParameterSpec(iv);
} }
private void setAesCipher(SecretKeySpec secretKey, IvParameterSpec iv) { private void setAesCiphers(SecretKeySpec secretKey, IvParameterSpec iv) {
try { try {
this.aesEncryptCipher = Cipher.getInstance("AES/CTR/NoPadding"); this.aesEncryptCipher = Cipher.getInstance("AES/CTR/NoPadding");
this.aesDecryptCipher = Cipher.getInstance("AES/CTR/NoPadding"); this.aesDecryptCipher = Cipher.getInstance("AES/CTR/NoPadding");
@ -240,13 +249,13 @@ public class MessageClient implements IMessageClient, Runnable {
assert secretKeySpec != null; assert secretKeySpec != null;
IvParameterSpec iv = generateIv(); IvParameterSpec iv = generateIv();
// Save AES cipher for subsequent communication // Save AES cipher for subsequent communication
setAesCipher(secretKeySpec, iv); setAesCiphers(secretKeySpec, iv);
// Encode parameters to base64 // Encode parameters to base64
String clearTextChallengeEncoded = Base64.getEncoder().encodeToString(clearTextChallenge); String clearTextChallengeEncoded = Base64.getEncoder().encodeToString(clearTextChallenge);
String secretKeyEncoded = Base64.getEncoder().encodeToString(secretKeySpec.getEncoded()); String secretKeyEncoded = Base64.getEncoder().encodeToString(secretKeySpec.getEncoded());
String ivEncoded = Base64.getEncoder().encodeToString(iv.getIV()); String ivEncoded = Base64.getEncoder().encodeToString(iv.getIV());
// Concatenate command and parameters (challenge, secretKey and IV) // Concatenate command and parameters (challenge, secretKey and IV)
String concatenated = "ok" + clearTextChallengeEncoded + secretKeyEncoded + ivEncoded; String concatenated = "ok " + clearTextChallengeEncoded + " " + secretKeyEncoded + " " + ivEncoded;
// Encrypt "<base64Encoded>" // Encrypt "<base64Encoded>"
Cipher cipher; Cipher cipher;
byte[] cipherTextChallenge; byte[] cipherTextChallenge;

View File

@ -4,15 +4,33 @@ import dslab.Email;
import dslab.Message; import dslab.Message;
import dslab.exception.MessageNotFoundException; import dslab.exception.MessageNotFoundException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*; import java.io.*;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException; import java.net.SocketException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class DMAPConnection implements Runnable { public class DMAPConnection implements Runnable {
Logger logger = Logger.getLogger(DMAPConnection.class.getName()); Logger logger = Logger.getLogger(DMAPConnection.class.getName());
private final String componentId;
private final Socket socket; private final Socket socket;
private PrintWriter out; private PrintWriter out;
private BufferedReader in; private BufferedReader in;
@ -20,10 +38,15 @@ public class DMAPConnection implements Runnable {
private final ConcurrentHashMap<Email, LinkedList<Message>> storage; private final ConcurrentHashMap<Email, LinkedList<Message>> storage;
private final ConcurrentHashMap<String, String> userStorage; private final ConcurrentHashMap<String, String> userStorage;
public DMAPConnection(Socket connection, ConcurrentHashMap<Email, LinkedList<Message>> storage, ConcurrentHashMap<String, String> userStorage) { private Cipher aesEncryptCipher;
private Cipher aesDecryptCipher;
public DMAPConnection(Socket connection, ConcurrentHashMap<Email, LinkedList<Message>> storage, ConcurrentHashMap<String, String> userStorage, String componentId) {
this.componentId = componentId;
this.socket = connection; this.socket = connection;
this.storage = storage; this.storage = storage;
this.userStorage = userStorage; this.userStorage = userStorage;
logger.setLevel(Level.ALL);
} }
@Override @Override
@ -32,8 +55,9 @@ public class DMAPConnection implements Runnable {
try { try {
this.out = new PrintWriter(socket.getOutputStream(), true); this.out = new PrintWriter(socket.getOutputStream(), true);
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream())); this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out.println("ok DMAP"); out.println("ok DMAP2.0");
loginLoop(); startSecure();
login();
String userInput; String userInput;
while (!Thread.currentThread().isInterrupted() && (userInput = in.readLine()) != null) { while (!Thread.currentThread().isInterrupted() && (userInput = in.readLine()) != null) {
@ -42,8 +66,7 @@ public class DMAPConnection implements Runnable {
shutdown(); shutdown();
} else if ("logout".equals(userInput)) { } else if ("logout".equals(userInput)) {
out.println("ok"); out.println("ok");
currentUser = null; shutdown();
loginLoop();
} else if ("list".equals(userInput)) { } else if ("list".equals(userInput)) {
listMessages(); listMessages();
} else if ("delete".equals(userInput.split("\\s+")[0])) { } else if ("delete".equals(userInput.split("\\s+")[0])) {
@ -81,14 +104,135 @@ public class DMAPConnection implements Runnable {
} }
} }
private void loginLoop() { /**
* Handles the startsecure command issued by the MessageClient.
*/
private void startSecure() {
String userInput;
try {
userInput = in.readLine(); // Should always be "startsecure"
// Send componentId
out.println("ok " + componentId);
// Receive client challenge, secret key and iv
userInput = in.readLine();
logger.info("Received clientchallenge from Client: " + userInput);
String[] rsaPlaintext = getRsaPlaintext(userInput).split("\\s+");
logger.info("Plaintext of clientchallenge: " + Arrays.toString(rsaPlaintext));
// Initialize AES cipher(s) (encryption and decryption)
setAesCiphers(rsaPlaintext[2], rsaPlaintext[3]);
// Encrypt client challenge with AES and send to MessageClient
out.println(encryptClientChallenge(rsaPlaintext[1]));
// Decrypt AES encrypted "ok" from MessageClient
userInput = getAesPlaintext(in.readLine());
if (!userInput.equals("ok")) {
logger.severe("Received " + userInput + "from MessageClient. Did not match 'ok'");
shutdown();
}
} catch (InterruptedIOException ioe) {
logger.info("Received interrupt from parent. Shutting down...");
shutdown();
} catch (SocketException e) {
logger.finer("Received interrupt. Exiting " + this.toString());
shutdown();
} catch (IOException e) {
logger.severe("Failed to get IO-Stream");
e.printStackTrace();
shutdown();
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | BadPaddingException |
InvalidKeySpecException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) {
logger.severe("Error during encryption/decryption. Aborting...");
e.printStackTrace();
shutdown();
}
}
/**
* Takes a plaintext message, encrypts and encodes it and returns the encoded ciphertext ready for sending.
*
* @param message Plaintext message
* @return B64 encoded and AES encrypted ciphertext
*/
private String getAesCiphertext(String message) throws IllegalBlockSizeException, BadPaddingException {
byte[] cipherText = aesEncryptCipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(cipherText);
}
/**
* Takes an AES encrypted message, decrypts and decodes it and returns the plaintext.
*
* @param message B64 encoded and AES encrypted message
* @return B64 decoded and AES decrypted plaintext
*/
private String getAesPlaintext(String message) throws BadPaddingException, IllegalBlockSizeException {
byte[] cipherText = Base64.getDecoder().decode(message.getBytes(StandardCharsets.UTF_8));
return new String(aesDecryptCipher.doFinal(cipherText));
}
/**
* Encrypts the client challenge with the AES cipher.
*
* @param clientChallenge Base64 encoded client challenge. Supplied by the MessageClient.
* @return Return the AES encrypted and encoded clientChallenge ready for sending.
*/
private String encryptClientChallenge(String clientChallenge) throws BadPaddingException, IllegalBlockSizeException {
String completeClientChallenge = "ok " + clientChallenge;
return Base64.getEncoder().encodeToString(aesEncryptCipher.doFinal(completeClientChallenge.getBytes(StandardCharsets.UTF_8)));
}
/**
* Takes an RSA encrypted message and constructs AES ciphers.
*
* @param message Has the form "ok <client-challenge> <secret-key> <iv>".
* @return Returns the decrypted and (partially) decoded challenge
*/
private String getRsaPlaintext(String message) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException,
NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] rsaEncrypted = Base64.getDecoder().decode(message);
byte[] keyBytes = Files.readAllBytes(Paths.get("keys", "server", componentId + ".der"));
logger.info("Read bytes from path " + Paths.get("keys", "server", componentId + ".der") + ": " + Arrays.toString(keyBytes));
// Create X509 spec object from key
// TODO Use readKey function provided by LVA-Team
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
// Create generator for RSA scheme
KeyFactory kf = KeyFactory.getInstance("RSA");
// Make generator generate private key from X509 spec
PrivateKey serverPrivateKey = kf.generatePrivate(spec);
Cipher cipher;
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, serverPrivateKey);
byte[] cipherTextChallenge = cipher.doFinal(rsaEncrypted);
return new String(cipherTextChallenge);
}
/**
* Sets global AES ciphers for encryption and decryption.
*
* @param secretKey Base64 encoded secret key. Supplied by the MessageClient.
* @param iv Base64 encoded IV. Supplied by the MessageClient.
*/
private void setAesCiphers(String secretKey, String iv) throws InvalidAlgorithmParameterException,
InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
SecretKeySpec decodedSecretKey = new SecretKeySpec(Base64.getDecoder().decode(secretKey), "AES");
IvParameterSpec decodedIv = new IvParameterSpec(Base64.getDecoder().decode(iv));
this.aesEncryptCipher = Cipher.getInstance("AES/CTR/NoPadding");
this.aesDecryptCipher = Cipher.getInstance("AES/CTR/NoPadding");
this.aesEncryptCipher.init(Cipher.ENCRYPT_MODE, decodedSecretKey, decodedIv);
this.aesDecryptCipher.init(Cipher.DECRYPT_MODE, decodedSecretKey, decodedIv);
}
/**
* Handles the login command issued by the MessageClient which is already encrypted.
*/
private void login() {
String userInput; String userInput;
try { try {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
userInput = in.readLine(); userInput = getAesPlaintext(in.readLine());
if ("quit".equals(userInput.split("\\s+")[0])) { if ("quit".equals(userInput.split("\\s+")[0])) {
out.println("ok bye"); out.println(getAesCiphertext("ok bye"));
shutdown(); shutdown();
} else if ("login".equals(userInput.split("\\s+")[0])) { } else if ("login".equals(userInput.split("\\s+")[0])) {
String[] args = userInput.split("\\s+"); String[] args = userInput.split("\\s+");
@ -103,7 +247,7 @@ public class DMAPConnection implements Runnable {
// Set current user if login successful // Set current user if login successful
currentUser = email; currentUser = email;
logger.info("User successfully logged in: " + currentUser.toString()); logger.info("User successfully logged in: " + currentUser.toString());
out.println("ok"); out.println(getAesCiphertext("ok"));
return; return;
} }
} }
@ -127,6 +271,10 @@ public class DMAPConnection implements Runnable {
logger.severe("Failed to get IO-Stream"); logger.severe("Failed to get IO-Stream");
e.printStackTrace(); e.printStackTrace();
shutdown(); shutdown();
} catch (BadPaddingException | IllegalBlockSizeException e) {
logger.severe("Error while encrypting/decrypting. Aborting...");
e.printStackTrace();
shutdown();
} }
} }
@ -159,7 +307,7 @@ public class DMAPConnection implements Runnable {
} }
} }
public void deleteMessage (String id) throws MessageNotFoundException { public void deleteMessage(String id) throws MessageNotFoundException {
int i; int i;
try { try {
i = Integer.parseInt(id); i = Integer.parseInt(id);

View File

@ -14,9 +14,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class DMAPListener extends Thread { public class DMAPListener extends Thread {
private final String componentId;
private final ServerSocket serverSocket; private final ServerSocket serverSocket;
private final Logger logger = Logger.getLogger(DMAPListener.class.getName()); private final Logger logger = Logger.getLogger(DMAPListener.class.getName());
private final ConcurrentHashMap<Email, LinkedList<Message>> storage; private final ConcurrentHashMap<Email, LinkedList<Message>> storage;
@ -24,10 +26,12 @@ public class DMAPListener extends Thread {
private final ArrayList<DMAPConnection> clients = new ArrayList<>(); private final ArrayList<DMAPConnection> clients = new ArrayList<>();
private final ExecutorService executorService = Executors.newCachedThreadPool(); private final ExecutorService executorService = Executors.newCachedThreadPool();
public DMAPListener(ServerSocket serverSocket, ConcurrentHashMap<Email, LinkedList<Message>> storage, ConcurrentHashMap<String, String> userStorage) { public DMAPListener(ServerSocket serverSocket, ConcurrentHashMap<Email, LinkedList<Message>> storage, ConcurrentHashMap<String, String> userStorage, String componentId) {
this.componentId = componentId;
this.serverSocket = serverSocket; this.serverSocket = serverSocket;
this.storage = storage; this.storage = storage;
this.userStorage = userStorage; this.userStorage = userStorage;
logger.setLevel(Level.ALL);
} }
@Override @Override
@ -37,7 +41,7 @@ public class DMAPListener extends Thread {
try { try {
Socket s = serverSocket.accept(); Socket s = serverSocket.accept();
logger.fine("Processing incoming socket " + s.toString()); logger.fine("Processing incoming socket " + s.toString());
DMAPConnection dmapConnection = new DMAPConnection(s, storage, userStorage); DMAPConnection dmapConnection = new DMAPConnection(s, storage, userStorage, componentId);
clients.add(dmapConnection); clients.add(dmapConnection);
executorService.submit(dmapConnection); executorService.submit(dmapConnection);
} catch (InterruptedIOException | SocketException e) { } catch (InterruptedIOException | SocketException e) {

View File

@ -6,6 +6,7 @@ import java.io.PrintStream;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import at.ac.tuwien.dsg.orvell.Shell; import at.ac.tuwien.dsg.orvell.Shell;
@ -18,6 +19,7 @@ import dslab.util.Config;
public class MailboxServer implements IMailboxServer, Runnable { public class MailboxServer implements IMailboxServer, Runnable {
private static final Logger logger = Logger.getLogger(MailboxServer.class.getName()); private static final Logger logger = Logger.getLogger(MailboxServer.class.getName());
private final String componentId;
private final String domain; private final String domain;
private ServerSocket dmtpServerSocket; private ServerSocket dmtpServerSocket;
private ServerSocket dmapServerSocket; private ServerSocket dmapServerSocket;
@ -42,6 +44,7 @@ public class MailboxServer implements IMailboxServer, Runnable {
*/ */
public MailboxServer(String componentId, Config config, InputStream in, PrintStream out) { public MailboxServer(String componentId, Config config, InputStream in, PrintStream out) {
this.domain = config.getString("domain"); this.domain = config.getString("domain");
this.componentId = componentId;
Config userConfig = new Config(config.getString("users.config")); Config userConfig = new Config(config.getString("users.config"));
// Load users from config into userStorage for authentication // Load users from config into userStorage for authentication
@ -61,6 +64,7 @@ public class MailboxServer implements IMailboxServer, Runnable {
this.shell.setPrompt("Mailboxserver> "); this.shell.setPrompt("Mailboxserver> ");
this.dmtpServerPort = config.getInt("dmtp.tcp.port"); this.dmtpServerPort = config.getInt("dmtp.tcp.port");
this.dmapServerPort = config.getInt("dmap.tcp.port"); this.dmapServerPort = config.getInt("dmap.tcp.port");
logger.setLevel(Level.ALL);
} }
@Override @Override
@ -76,7 +80,7 @@ public class MailboxServer implements IMailboxServer, Runnable {
} }
this.dmtpListener = new DMTPListener(this.dmtpServerSocket, this.messageStorage, this.userStorage, this.domain); this.dmtpListener = new DMTPListener(this.dmtpServerSocket, this.messageStorage, this.userStorage, this.domain);
this.dmtpListener.start(); this.dmtpListener.start();
this.dmapListener = new DMAPListener(this.dmapServerSocket, this.messageStorage, this.userStorage); this.dmapListener = new DMAPListener(this.dmapServerSocket, this.messageStorage, this.userStorage, this.componentId);
this.dmapListener.start(); this.dmapListener.start();
this.shell.run(); this.shell.run();
} }