commit 096045fa4f0357373e1b2a1fb6f7262dd465a9dd
parent 0a70221443f19da24e773133c82f222a7a448c63
Author: Cody Lewis <luxdotsugi@gmail.com>
Date: Sun, 2 Sep 2018 17:02:47 +1000
Added a Diffie-Hellman key exchange and AES encryption over the messages
Diffstat:
A | AES.java | | | 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | Chat.java | | | 37 | ++++++++++++++++++++++++++++--------- |
M | ChatClient.java | | | 16 | ++++++++++++++++ |
M | ChatServer.java | | | 24 | ++++++++++++++++++++++-- |
A | DH.java | | | 88 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
5 files changed, 213 insertions(+), 11 deletions(-)
diff --git a/AES.java b/AES.java
@@ -0,0 +1,58 @@
+import java.util.Base64;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.MessageDigest;
+/**
+ * AES encryption wrapper library
+ *
+ * @author Cody Lewis
+ * @since 2018-08-29
+ */
+public class AES {
+ private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
+ /**
+ * Hash the input key
+ * @param key a secret key
+ * @return the hash of the secret key
+ */
+ private static byte[] hashKey(byte[] key) throws Exception {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ return md.digest(key);
+ }
+ /**
+ * Perform AES encryption
+ * @param sessionKey a secret key
+ * @param message the plaintext
+ * @return cipher-text of the message
+ */
+ public static String[] encrypt(byte[] sessionKey, String message) throws Exception {
+ // set up cipher
+ Cipher c = Cipher.getInstance(CIPHER_ALGORITHM);
+ SecretKeySpec secret = new SecretKeySpec(hashKey(sessionKey), "AES");
+ c.init(c.ENCRYPT_MODE, secret);
+ // produce ciphertext
+ int len = message.length() / 16 + ((message.length() % 16 == 0) ? 0 : 1);
+ String[] ciphertext = new String[len];
+ String[] result = new String[len];
+ for(int i = 0; i < len; ++i) {
+ ciphertext[i] = (i + 1) * 16 > message.length() ? message.substring(i * 16) : message.substring(i * 16, (i + 1) * 16);
+ result[i] = Base64.getEncoder().encodeToString(c.doFinal(ciphertext[i].getBytes("UTF-8")));
+ }
+ return result;
+ }
+ /**
+ * Perform AES decryption
+ * @param sessionKey a secret key
+ * @param ciphertext some AES encrypted text
+ * @return the plain-text
+ */
+ public static String decrypt(byte[] sessionKey, String ciphertext) throws Exception {
+ // set up cipher
+ Cipher c = Cipher.getInstance(CIPHER_ALGORITHM);
+ SecretKeySpec secret = new SecretKeySpec(hashKey(sessionKey), "AES");
+ c.init(c.DECRYPT_MODE, secret);
+ // produce plaintext
+ return new String(c.doFinal(Base64.getDecoder().decode(ciphertext.substring(0, ciphertext.indexOf("\n") == -1 ? ciphertext.length() : ciphertext.indexOf("\n")))));
+ }
+}+
\ No newline at end of file
diff --git a/Chat.java b/Chat.java
@@ -1,6 +1,7 @@
import java.util.Scanner;
import java.io.BufferedReader;
import java.io.PrintWriter;
+import java.math.BigInteger;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
@@ -35,6 +36,7 @@ public class Chat extends JFrame {
protected boolean disconnect;
protected Thread recvMsg;
protected boolean isRecieving;
+ protected BigInteger sessionKey;
/**
* Enum of the various error codes returned by the classes
*/
@@ -94,10 +96,16 @@ public class Chat extends JFrame {
private class Send implements ActionListener {
@Override
public void actionPerformed(ActionEvent evt) {
- String message = textToMessage();
- msgArea.append("Waiting for message to send...\n");
- out.println(SCP.message(address.getHostAddress(), port, message));
- msgArea.append("Message Sent: " + message + "\n");
+ try {
+ String[] messages = textToMessage();
+ for(String message : messages) {
+ msgArea.append("Waiting for message to send...\n");
+ out.println(SCP.message(address.getHostAddress(), port, message));
+ msgArea.append("Message Sent: " + AES.decrypt(sessionKey.toByteArray(), message) + "\n");
+ }
+ } catch(Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ }
}
}
/**
@@ -159,7 +167,13 @@ public class Chat extends JFrame {
if(SCP.parseAcknowledge(packet)) {
return "ACKNOWLEDGE";
}
- return SCP.parseMessage(packet, address.getHostAddress(), port);
+ String message = SCP.parseMessage(packet, address.getHostAddress(), port);
+ try {
+ return AES.decrypt(sessionKey.toByteArray(), message);
+ } catch(Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ return "";
+ }
}
/**
* Disconnection event handler
@@ -189,9 +203,14 @@ public class Chat extends JFrame {
* Take an input from the users and give an out suitable to put into a message
* @return A String of the input formatted to be embedded in a SCP message
*/
- protected String textToMessage() {
- String message = msgField.getText();
- msgField.setText("");
- return message;
+ protected String[] textToMessage() {
+ try {
+ String[] message = AES.encrypt(sessionKey.toByteArray(), msgField.getText());
+ msgField.setText("");
+ return message;
+ } catch(Exception e) {
+ System.out.println("Error: " + e.getMessage());
+ return null;
+ }
}
}
diff --git a/ChatClient.java b/ChatClient.java
@@ -1,3 +1,4 @@
+import java.math.BigInteger;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.InputStreamReader;
@@ -35,6 +36,8 @@ public class ChatClient extends Chat {
msgArea.append(String.format("Connecting to %s:%d\n", address.getHostAddress(), port));
connectToServer();
msgArea.append("Connected to server\n");
+ msgArea.append("Exchanging keys with the server\n");
+ keyExchange();
username = args.length > 2 ? args[2] : "Client";
SCPConnect();
msgArea.append("Connected to SCP\n");
@@ -70,6 +73,19 @@ public class ChatClient extends Chat {
return true;
}
/**
+ * perform a diffie-hellman key exchange
+ * @return true on completion
+ */
+ private boolean keyExchange() throws IOException {
+ BigInteger prime = DH.genPrime(16);
+ out.println(prime); // send prime to server
+ BigInteger priKey = DH.genPrivateKey(prime);
+ BigInteger pubKey = DH.genPublicKey(priKey, prime);
+ out.println(pubKey);
+ sessionKey = DH.genSessionKey(new BigInteger(in.readLine()), priKey, prime);
+ return true;
+ }
+ /**
* Send the SCP connection packet
* @param hostName the name of the server
* @param port the port number the server is running on
diff --git a/ChatServer.java b/ChatServer.java
@@ -1,5 +1,6 @@
import java.io.BufferedReader;
import java.io.PrintWriter;
+import java.math.BigInteger;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.ServerSocket;
@@ -64,6 +65,8 @@ public class ChatServer extends Chat {
msgArea.append("Waiting for client to connect\n");
acceptClient();
msgArea.append("Client successfully connected\n");
+ msgArea.append("Exchanging Keys");
+ keyExchange();
msgArea.append("Waiting for client to SCP connect\n");
username = clientConnect();
username = username.substring(1, username.length() - 1); // remove quotes
@@ -75,8 +78,11 @@ public class ChatServer extends Chat {
if(acknowledged()) {
msgArea.append(String.format("User %s has connected to SCP\n\n", username));
String message = welcomeMessage + "\n"; // send welcome message + chat rules to client
- msgArea.append("Waiting for message to send...\n");
- out.println(SCP.message(address.getHostAddress(), port, message));
+ String[] encryptedMessage = AES.encrypt(sessionKey.toByteArray(), message);
+ for(String em : encryptedMessage) {
+ msgArea.append("Waiting for message to send...\n");
+ out.println(SCP.message(address.getHostAddress(), port, em));
+ }
msgArea.append("Message Sent: " + message + "\n");
while(!disconnect) {
messageLoop();
@@ -86,6 +92,8 @@ public class ChatServer extends Chat {
}
} catch(NullPointerException npe) {
msgArea.append("\nError: unexpected cut-off from client, looking for new client\n");
+ } catch(Exception e) {
+ msgArea.append("\nError: " + e.getMessage() + "\n");
}
}
/**
@@ -109,6 +117,18 @@ public class ChatServer extends Chat {
return true;
}
/**
+ * Perform a diffie-hellman key exchange
+ * @return true on completion
+ */
+ private boolean keyExchange() throws IOException {
+ BigInteger prime = new BigInteger(in.readLine());
+ BigInteger priKey = DH.genPrivateKey(prime);
+ BigInteger pubKey = DH.genPublicKey(priKey, prime);
+ sessionKey = DH.genSessionKey(new BigInteger(in.readLine()), priKey, prime);
+ out.println(pubKey);
+ return true;
+ }
+ /**
* Recieve a SCP connection
* @return Client's username
*/
diff --git a/DH.java b/DH.java
@@ -0,0 +1,88 @@
+import java.math.BigInteger;
+import java.security.SecureRandom;
+/**
+ * Diff-Hellman Library
+ *
+ * @author Cody Lewis
+ * @since 2018-08-28
+ */
+public class DH {
+ /**
+ * Generate a random prime at a specified length
+ * @param bitLength the number of the bits long the prime should be
+ * @return a random prime with length: bitLength
+ */
+ public static BigInteger genPrime(int bitLength) {
+ BigInteger prime = BigInteger.probablePrime(bitLength, new SecureRandom());
+ byte b[] = new byte[1];
+ b[0] = 2;
+ prime = prime.multiply(new BigInteger(b));
+ prime = prime.add(BigInteger.ONE);
+ return prime;
+ }
+ /**
+ * Create the private key for Diffie Hellman
+ * @param prime the published prime
+ * @return the user's private key
+ */
+ public static BigInteger genPrivateKey(BigInteger prime) { // generate the private key
+ BigInteger priKey = new BigInteger(prime.bitLength(), new SecureRandom());
+ if(priKey.compareTo(BigInteger.ZERO) == 1 && priKey.compareTo(prime) == -1) {
+ return priKey;
+ }
+ return genPrivateKey(prime);
+ }
+ /**
+ * Create the public key for Diffie Hellman
+ * @param priKey the user's private key
+ * @param prime the published prime
+ * @return a public key corresponding to the private key and prime
+ */
+ public static BigInteger genPublicKey(BigInteger priKey, BigInteger prime) {
+ return genGenerator(prime).modPow(priKey, prime);
+ }
+ /**
+ * Generate a primitive root of a prime
+ * @param prime a prime
+ * @return the primitive root of the prime
+ */
+ public static BigInteger genGenerator(BigInteger prime) {
+ byte b[] = new byte[1];
+ b[0] = 2;
+ BigInteger generator = new BigInteger(b);
+ while(!isPrimRoot(generator, prime) && generator.compareTo(prime) < 1) {
+ generator = generator.add(BigInteger.ONE);
+ }
+ return generator;
+ }
+ /**
+ * Check whether alpha is a primitive root of prime
+ * @param alpha the possible primitive root
+ * @param prime a prime
+ * @return true if alpha is a primitive root of prime else false
+ */
+ public static boolean isPrimRoot(BigInteger alpha, BigInteger prime) {
+ BigInteger phi = prime.subtract(BigInteger.ONE); // phi(prime) is == to prime - 1
+ byte b[] = new byte[1];
+ b[0] = 2;
+ for(BigInteger i = new BigInteger(b); i.compareTo(phi) == -1; i = i.nextProbablePrime()) { // iterate by primes
+ if((phi.mod(i)).compareTo(BigInteger.ZERO) != 0) { // check if i is a factor of phi
+ continue;
+ }
+ if(alpha.modPow(phi.divide(i), prime).compareTo(BigInteger.ONE) == 0) { // if alpha^(phi/i) mod prime == 1 then it is not a primRoot
+ return false;
+ }
+ }
+ return true;
+ }
+ /**
+ * Calculate the session key
+ * @param pubKey the other user's public key
+ * @param priKey the callers private key
+ * @param prime the agreed prime
+ * @return a session key corresponding to the inputs
+ */
+ public static BigInteger genSessionKey(BigInteger pubKey, BigInteger priKey, BigInteger prime) {
+ return pubKey.modPow(priKey, prime);
+ }
+}