diff --git a/src/main/java/dslab/client/MessageClient.java b/src/main/java/dslab/client/MessageClient.java index 7dad433..0be948b 100644 --- a/src/main/java/dslab/client/MessageClient.java +++ b/src/main/java/dslab/client/MessageClient.java @@ -77,7 +77,9 @@ public class MessageClient implements IMessageClient, Runnable { String input = 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(); startSecure(); @@ -110,7 +112,9 @@ public class MessageClient implements IMessageClient, Runnable { String componentId; PublicKey serverPublicKey; this.dmapOut.println("startsecure"); + logger.info("Sent command 'startsecure'"); input = this.dmapIn.readLine(); + logger.info("Server's response: " + input); if (input.startsWith("ok") && (input.split("\\s+").length == 2)) { // Get the component-id from the server componentId = input.split("\\s+")[1]; @@ -127,12 +131,14 @@ public class MessageClient implements IMessageClient, Runnable { logger.info("Server's Public Key: " + serverPublicKey); String clientChallenge = generateChallengeMessage(serverPublicKey); // Send clientChallenge to server + logger.info("Send clientchallenge to Server: " + clientChallenge); this.dmapOut.println(clientChallenge); // Receive AES encrypted message saying "ok " String response = this.dmapIn.readLine(); // Compare received client challenge with generated client challenge verifyChallenge(response); // 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")); } catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalBlockSizeException | BadPaddingException | FailedVerificationException e) { 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. + * * @param message Plaintext message * @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. + * * @param message B64 encoded and AES encrypted message * @return B64 decoded and AES decrypted plaintext */ @@ -178,11 +186,12 @@ public class MessageClient implements IMessageClient, Runnable { // Decode if (plainText.split("\\s+").length != 2) 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 - if (!clientChallenge.equals(new String(this.challenge))) - throw new FailedVerificationException("Server's clientChallenge " + clientChallenge + " does not match sent clientChallenge " + new String(this.challenge)); + if (!Arrays.equals(clientChallenge, this.challenge)) + throw new FailedVerificationException("Server's clientChallenge " + new String(clientChallenge) + + " does not match sent clientChallenge " + new String(this.challenge)); } private SecretKeySpec generateSecretKey() { @@ -205,7 +214,7 @@ public class MessageClient implements IMessageClient, Runnable { return new IvParameterSpec(iv); } - private void setAesCipher(SecretKeySpec secretKey, IvParameterSpec iv) { + private void setAesCiphers(SecretKeySpec secretKey, IvParameterSpec iv) { try { this.aesEncryptCipher = 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; IvParameterSpec iv = generateIv(); // Save AES cipher for subsequent communication - setAesCipher(secretKeySpec, iv); + setAesCiphers(secretKeySpec, iv); // Encode parameters to base64 String clearTextChallengeEncoded = Base64.getEncoder().encodeToString(clearTextChallenge); String secretKeyEncoded = Base64.getEncoder().encodeToString(secretKeySpec.getEncoded()); String ivEncoded = Base64.getEncoder().encodeToString(iv.getIV()); // Concatenate command and parameters (challenge, secretKey and IV) - String concatenated = "ok" + clearTextChallengeEncoded + secretKeyEncoded + ivEncoded; + String concatenated = "ok " + clearTextChallengeEncoded + " " + secretKeyEncoded + " " + ivEncoded; // Encrypt "" Cipher cipher; byte[] cipherTextChallenge; diff --git a/src/main/java/dslab/mailbox/DMAPConnection.java b/src/main/java/dslab/mailbox/DMAPConnection.java index 6e6fcbd..88a6799 100644 --- a/src/main/java/dslab/mailbox/DMAPConnection.java +++ b/src/main/java/dslab/mailbox/DMAPConnection.java @@ -4,15 +4,33 @@ import dslab.Email; import dslab.Message; 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.net.Socket; 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.concurrent.ConcurrentHashMap; +import java.util.logging.Level; import java.util.logging.Logger; public class DMAPConnection implements Runnable { Logger logger = Logger.getLogger(DMAPConnection.class.getName()); + private final String componentId; private final Socket socket; private PrintWriter out; private BufferedReader in; @@ -20,10 +38,15 @@ public class DMAPConnection implements Runnable { private final ConcurrentHashMap> storage; private final ConcurrentHashMap userStorage; - public DMAPConnection(Socket connection, ConcurrentHashMap> storage, ConcurrentHashMap userStorage) { + private Cipher aesEncryptCipher; + private Cipher aesDecryptCipher; + + public DMAPConnection(Socket connection, ConcurrentHashMap> storage, ConcurrentHashMap userStorage, String componentId) { + this.componentId = componentId; this.socket = connection; this.storage = storage; this.userStorage = userStorage; + logger.setLevel(Level.ALL); } @Override @@ -32,8 +55,9 @@ public class DMAPConnection implements Runnable { try { this.out = new PrintWriter(socket.getOutputStream(), true); this.in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - out.println("ok DMAP"); - loginLoop(); + out.println("ok DMAP2.0"); + startSecure(); + login(); String userInput; while (!Thread.currentThread().isInterrupted() && (userInput = in.readLine()) != null) { @@ -42,8 +66,7 @@ public class DMAPConnection implements Runnable { shutdown(); } else if ("logout".equals(userInput)) { out.println("ok"); - currentUser = null; - loginLoop(); + shutdown(); } else if ("list".equals(userInput)) { listMessages(); } 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 ". + * @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; try { while (!Thread.currentThread().isInterrupted()) { - userInput = in.readLine(); + userInput = getAesPlaintext(in.readLine()); if ("quit".equals(userInput.split("\\s+")[0])) { - out.println("ok bye"); + out.println(getAesCiphertext("ok bye")); shutdown(); } else if ("login".equals(userInput.split("\\s+")[0])) { String[] args = userInput.split("\\s+"); @@ -103,7 +247,7 @@ public class DMAPConnection implements Runnable { // Set current user if login successful currentUser = email; logger.info("User successfully logged in: " + currentUser.toString()); - out.println("ok"); + out.println(getAesCiphertext("ok")); return; } } @@ -127,6 +271,10 @@ public class DMAPConnection implements Runnable { logger.severe("Failed to get IO-Stream"); e.printStackTrace(); 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; try { i = Integer.parseInt(id); diff --git a/src/main/java/dslab/mailbox/DMAPListener.java b/src/main/java/dslab/mailbox/DMAPListener.java index 7b686b5..accb494 100644 --- a/src/main/java/dslab/mailbox/DMAPListener.java +++ b/src/main/java/dslab/mailbox/DMAPListener.java @@ -14,9 +14,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; import java.util.logging.Logger; public class DMAPListener extends Thread { + private final String componentId; private final ServerSocket serverSocket; private final Logger logger = Logger.getLogger(DMAPListener.class.getName()); private final ConcurrentHashMap> storage; @@ -24,10 +26,12 @@ public class DMAPListener extends Thread { private final ArrayList clients = new ArrayList<>(); private final ExecutorService executorService = Executors.newCachedThreadPool(); - public DMAPListener(ServerSocket serverSocket, ConcurrentHashMap> storage, ConcurrentHashMap userStorage) { + public DMAPListener(ServerSocket serverSocket, ConcurrentHashMap> storage, ConcurrentHashMap userStorage, String componentId) { + this.componentId = componentId; this.serverSocket = serverSocket; this.storage = storage; this.userStorage = userStorage; + logger.setLevel(Level.ALL); } @Override @@ -37,7 +41,7 @@ public class DMAPListener extends Thread { try { Socket s = serverSocket.accept(); 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); executorService.submit(dmapConnection); } catch (InterruptedIOException | SocketException e) { diff --git a/src/main/java/dslab/mailbox/MailboxServer.java b/src/main/java/dslab/mailbox/MailboxServer.java index 7d89042..eb5360a 100644 --- a/src/main/java/dslab/mailbox/MailboxServer.java +++ b/src/main/java/dslab/mailbox/MailboxServer.java @@ -6,6 +6,7 @@ import java.io.PrintStream; import java.net.ServerSocket; import java.util.LinkedList; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; import java.util.logging.Logger; import at.ac.tuwien.dsg.orvell.Shell; @@ -18,6 +19,7 @@ import dslab.util.Config; public class MailboxServer implements IMailboxServer, Runnable { private static final Logger logger = Logger.getLogger(MailboxServer.class.getName()); + private final String componentId; private final String domain; private ServerSocket dmtpServerSocket; private ServerSocket dmapServerSocket; @@ -42,6 +44,7 @@ public class MailboxServer implements IMailboxServer, Runnable { */ public MailboxServer(String componentId, Config config, InputStream in, PrintStream out) { this.domain = config.getString("domain"); + this.componentId = componentId; Config userConfig = new Config(config.getString("users.config")); // Load users from config into userStorage for authentication @@ -61,6 +64,7 @@ public class MailboxServer implements IMailboxServer, Runnable { this.shell.setPrompt("Mailboxserver> "); this.dmtpServerPort = config.getInt("dmtp.tcp.port"); this.dmapServerPort = config.getInt("dmap.tcp.port"); + logger.setLevel(Level.ALL); } @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.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.shell.run(); }