scp-app

git clone git://git.codymlewis.com/scp-app.git
Log | Files | Refs | LICENSE

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:
AAES.java | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MChat.java | 37++++++++++++++++++++++++++++---------
MChatClient.java | 16++++++++++++++++
MChatServer.java | 24++++++++++++++++++++++--
ADH.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); + } +}