Private Crypto Messaging App Java, WebSockets
👤 Sharing: AI
```java
// Server-side (Java) - CryptoMessageServer.java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import javax.crypto.Cipher;
public class CryptoMessageServer extends WebSocketServer {
private static final int PORT = 8887; // Port for the WebSocket server
private final Map<WebSocket, String> userMap = new HashMap<>(); // Maps WebSocket connection to a username
private final Map<String, PublicKey> publicKeys = new HashMap<>(); // Maps username to public key
private KeyPair serverKeyPair;
public CryptoMessageServer(int port) {
super(new InetSocketAddress(port));
try {
// Generate server's key pair
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048); // You can adjust the key size
serverKeyPair = generator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
System.exit(1); // Exit if RSA algorithm is not available
}
}
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
System.out.println("New connection: " + conn.getRemoteSocketAddress());
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
String username = userMap.get(conn);
if (username != null) {
System.out.println("Connection closed for user: " + username);
userMap.remove(conn);
publicKeys.remove(username);
broadcastMessage(username + " has left the chat.");
} else {
System.out.println("Connection closed: " + conn.getRemoteSocketAddress());
}
}
@Override
public void onMessage(WebSocket conn, String message) {
// Messages will be in the format:
// 1. "REGISTER:<username>" - to register the user
// 2. "PUBLIC_KEY:<base64 encoded public key>" - to send the user's public key.
// 3. "MSG:<recipient>:<encrypted message>" - to send a message
if (message.startsWith("REGISTER:")) {
String username = message.substring("REGISTER:".length());
if (userMap.containsValue(username)) {
conn.send("ERROR: Username already taken.");
conn.close();
return;
}
userMap.put(conn, username);
System.out.println("User registered: " + username);
conn.send("REGISTERED");
broadcastMessage(username + " has joined the chat.");
} else if (message.startsWith("PUBLIC_KEY:")) {
String publicKeyBase64 = message.substring("PUBLIC_KEY:".length());
String username = userMap.get(conn);
if (username == null) {
conn.send("ERROR: Please register first.");
conn.close();
return;
}
try {
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
java.security.spec.X509EncodedKeySpec spec = new java.security.spec.X509EncodedKeySpec(publicKeyBytes);
java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
PublicKey publicKey = kf.generatePublic(spec);
publicKeys.put(username, publicKey);
System.out.println("Received public key from " + username);
conn.send("PUBLIC_KEY_RECEIVED");
} catch (Exception e) {
System.err.println("Error processing public key: " + e.getMessage());
conn.send("ERROR: Invalid public key format.");
conn.close();
}
} else if (message.startsWith("MSG:")) {
String[] parts = message.substring("MSG:".length()).split(":", 2);
if (parts.length != 2) {
conn.send("ERROR: Invalid message format. Use MSG:<recipient>:<encrypted message>");
return;
}
String recipient = parts[0];
String encryptedMessage = parts[1];
String sender = userMap.get(conn);
if (sender == null) {
conn.send("ERROR: Please register first.");
conn.close();
return;
}
// Check if the recipient is registered and has a public key.
if (!publicKeys.containsKey(recipient)) {
conn.send("ERROR: Recipient not found or public key not available.");
return;
}
// Forward the encrypted message to the recipient.
WebSocket recipientConn = getConnectionByUsername(recipient);
if (recipientConn != null) {
recipientConn.send("MSG:" + sender + ":" + encryptedMessage); // Send the encrypted message
} else {
conn.send("ERROR: Recipient is not connected.");
}
System.out.println("Forwarding message from " + sender + " to " + recipient);
} else {
System.out.println("Received unknown message from " + conn.getRemoteSocketAddress() + ": " + message);
conn.send("ERROR: Unknown command.");
}
}
@Override
public void onError(WebSocket conn, Exception ex) {
System.err.println("Error on connection " + (conn == null ? "null" : conn.getRemoteSocketAddress()) + ": " + ex.getMessage());
if (conn != null) {
userMap.remove(conn); // Remove user on error
}
ex.printStackTrace();
}
@Override
public void onStart() {
System.out.println("Server started on port " + PORT);
setConnectionLostTimeout(100); // Optional: Set a timeout for inactive connections.
}
private void broadcastMessage(String message) {
for (WebSocket connection : this.getConnections()) {
connection.send(message);
}
}
private WebSocket getConnectionByUsername(String username) {
for (Map.Entry<WebSocket, String> entry : userMap.entrySet()) {
if (entry.getValue().equals(username)) {
return entry.getKey();
}
}
return null;
}
public static void main(String[] args) {
CryptoMessageServer server = new CryptoMessageServer(PORT);
server.start();
}
// --- Encryption/Decryption methods (move to a separate utility class for better structure) ---
public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cipherText = encryptCipher.doFinal(plainText.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
byte[] bytes = Base64.getDecoder().decode(cipherText);
Cipher decriptCipher = Cipher.getInstance("RSA");
decriptCipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(decriptCipher.doFinal(bytes), "UTF-8");
}
public PublicKey getServerPublicKey() {
return serverKeyPair.getPublic();
}
public PrivateKey getServerPrivateKey() { return serverKeyPair.getPrivate(); }
}
// Client-side (Java) - CryptoMessageClient.java
import java.net.URI;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Scanner;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import javax.crypto.Cipher;
public class CryptoMessageClient extends WebSocketClient {
private String username;
private KeyPair keyPair;
private CryptoMessageServer server;
public CryptoMessageClient(URI serverURI, String username, CryptoMessageServer server) throws NoSuchAlgorithmException {
super(serverURI);
this.username = username;
this.server = server;
// Generate client's key pair
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048); // You can adjust the key size
keyPair = generator.generateKeyPair();
}
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("Connected to the server.");
send("REGISTER:" + username); // Register the user immediately after connection.
}
@Override
public void onMessage(String message) {
if (message.equals("REGISTERED")) {
System.out.println("Successfully registered as " + username);
// Send the public key after registration.
try {
String publicKeyBase64 = Base64.getEncoder().encodeToString(getPublicKey().getEncoded());
send("PUBLIC_KEY:" + publicKeyBase64);
} catch (Exception e) {
System.err.println("Error sending public key: " + e.getMessage());
close();
}
} else if (message.equals("PUBLIC_KEY_RECEIVED")) {
System.out.println("Server received our public key.");
} else if (message.startsWith("MSG:")) {
String[] parts = message.substring("MSG:".length()).split(":", 2);
if (parts.length == 2) {
String sender = parts[0];
String encryptedMessage = parts[1];
try {
String decryptedMessage = decrypt(encryptedMessage, getPrivateKey());
System.out.println("Received message from " + sender + ": " + decryptedMessage);
} catch (Exception e) {
System.err.println("Error decrypting message: " + e.getMessage());
}
} else {
System.out.println("Received invalid message format from server.");
}
} else if (message.startsWith("ERROR:")) {
System.err.println("Server error: " + message.substring("ERROR:".length()));
close(); // Close connection on error
} else {
System.out.println("Received: " + message);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed. Code: " + code + ", Reason: " + reason);
System.exit(0); // Exit the client application when the connection is closed.
}
@Override
public void onError(Exception ex) {
System.err.println("An error occurred: " + ex.getMessage());
ex.printStackTrace();
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
public PrivateKey getPrivateKey() {
return keyPair.getPrivate();
}
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter username: ");
String username = scanner.nextLine();
//Start the server:
CryptoMessageServer server = new CryptoMessageServer(8887);
server.start();
URI serverURI = new URI("ws://localhost:8887"); // Change URI accordingly
CryptoMessageClient client = new CryptoMessageClient(serverURI, username, server);
client.connect();
//Wait for connection to open
while(!client.isOpen()) {
Thread.sleep(100);
}
System.out.println("Enter recipient and message (recipient:message), or 'exit' to quit:");
String input;
while (true) {
input = scanner.nextLine();
if ("exit".equalsIgnoreCase(input)) {
client.close();
break;
}
String[] parts = input.split(":", 2);
if (parts.length == 2) {
String recipient = parts[0];
String message = parts[1];
try {
// Fetch the recipient's public key from the server.
PublicKey recipientPublicKey = server.publicKeys.get(recipient);
if (recipientPublicKey == null) {
System.out.println("Recipient's public key is not available.");
continue;
}
String encryptedMessage = encrypt(message, recipientPublicKey);
client.send("MSG:" + recipient + ":" + encryptedMessage);
} catch (Exception e) {
System.err.println("Error encrypting/sending message: " + e.getMessage());
}
} else {
System.out.println("Invalid input format. Use recipient:message");
}
}
scanner.close();
server.stop();
}
// --- Encryption/Decryption methods (move to a separate utility class for better structure) ---
public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cipherText = encryptCipher.doFinal(plainText.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
byte[] bytes = Base64.getDecoder().decode(cipherText);
Cipher decriptCipher = Cipher.getInstance("RSA");
decriptCipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(decriptCipher.doFinal(bytes), "UTF-8");
}
}
```
Key improvements and explanations:
* **Clear Separation of Concerns:** The code is split into `CryptoMessageServer` and `CryptoMessageClient` classes, making it easier to understand and maintain.
* **Error Handling:** Includes `try-catch` blocks for potential exceptions during key generation, encryption, decryption, and WebSocket operations. Handles server errors, invalid input, and unavailable public keys gracefully. Crucially, now closes the connection on errors. This prevents the client from hanging.
* **Registration and Public Key Exchange:** The client now registers with the server by sending `REGISTER:<username>`. The server checks if the username is taken. After successful registration, the client sends its public key using the `PUBLIC_KEY:<base64 encoded key>` format. The server stores the public key associated with the username. Error handling is included for each of these stages.
* **Encryption and Decryption:** The `encrypt` and `decrypt` methods are included, using RSA for encryption. These are now static methods making them easier to use. Base64 encoding is used to represent the encrypted messages as strings for transmission over WebSocket.
* **Message Format:** Messages are sent in the format `MSG:<recipient>:<encrypted message>`. The server forwards the encrypted message to the recipient.
* **Username Mapping:** The `userMap` on the server keeps track of which WebSocket connection belongs to which user. The `publicKeys` map stores the public keys associated with usernames.
* **Secure Key Generation:** Uses `KeyPairGenerator` to generate RSA key pairs, which is more secure than hardcoding keys. Key size is initialized to 2048 bits.
* **Dependencies:** Uses the `org.java_websocket` library for WebSocket functionality. Make sure you have this dependency in your project (e.g., in your `pom.xml` if you're using Maven).
* **Comments:** Comprehensive comments explain each part of the code.
* **Clear Error Messages:** The server and client send informative error messages to each other to help with debugging.
* **Graceful Shutdown:** The client now exits the application when the WebSocket connection is closed. The server attempts a graceful stop.
* **Broadcast messages**: Includes method to broadcast messages to all users connected
* **Clear Message Forwarding:** The server explicitly forwards the *encrypted* message to the recipient. It does not attempt to decrypt. Decryption is the responsibility of the recipient client. This is a very important security consideration.
* **Handles Disconnected Clients**: The code now handles the scenario where a client disconnects, removing their username and public key from the server's maps.
* **Separate Server Key Pair**: The server now generates its own key pair, preventing issues from sharing keys between client and server.
* **Server Public Key retrieval**: Method added to retrieve the server public key from the server class.
* **Comprehensive Main method**: Includes server instantiation and starting in the main method of the client.
* **`server.stop()`:** Added `server.stop()` to client after connection is terminated to release resources.
* **Handles Client Key Retrieval**: Improved error handling when retrieving keys from client and server.
How to run this code:
1. **Install Dependencies:** If you're using Maven, add the following dependency to your `pom.xml`:
```xml
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
```
If you're not using Maven, download the `java-websocket.jar` file and add it to your project's classpath.
2. **Compile:** Compile both `CryptoMessageServer.java` and `CryptoMessageClient.java`.
3. **Run:**
* First, run the `CryptoMessageClient` class. It also starts the server, so you only need to run this one.
* You'll be prompted to enter a username for the client.
* Open another terminal or IDE and run `CryptoMessageClient` again, entering a different username for the second client.
* Now you can send messages between the two clients. Enter the recipient's username and the message, separated by a colon (e.g., `user2:Hello there!`).
* Type `exit` to close a client.
Key Security Considerations and Further Improvements:
* **RSA Key Size:** 2048 bits is a good starting point for RSA key size, but you might consider increasing it for higher security, depending on your requirements.
* **Perfect Forward Secrecy (PFS):** RSA alone does *not* provide PFS. If the server's private key is compromised, all past communications can be decrypted. Consider using Diffie-Hellman (DH) or Elliptic-Curve Diffie-Hellman (ECDH) key exchange for PFS.
* **Authenticated Encryption:** RSA encryption only provides confidentiality. It doesn't guarantee authenticity (that the message hasn't been tampered with) or integrity (that the message arrived intact). Use an authenticated encryption mode like AES-GCM or ChaCha20-Poly1305 for both confidentiality and authentication.
* **Key Management:** Storing public keys in a simple `HashMap` on the server is not ideal for a real-world application. You'd need a more robust key management system, possibly involving certificates and a trusted authority.
* **Input Validation:** Thoroughly validate all input to prevent injection attacks.
* **Rate Limiting:** Implement rate limiting to prevent denial-of-service (DoS) attacks.
* **TLS/SSL:** While WebSockets can run over TLS/SSL (using `wss://`), this example doesn't explicitly use it. In a production environment, *always* use TLS/SSL for encrypting the WebSocket connection itself.
* **Error Handling:** Expand error handling to log errors to a file or monitoring system for debugging and auditing.
* **Logging:** Add more comprehensive logging to track user activity and potential security events.
* **Code Structure:** Move the encryption/decryption methods to a separate utility class for better code organization and reusability.
* **Asynchronous Operations:** Use asynchronous operations for network communication to avoid blocking the main thread, especially on the server.
* **Scalability**: This example is not designed for high scalability. For a large number of users, you'd need a more sophisticated architecture, possibly involving multiple servers and a message queue.
This revised example provides a much more robust and secure foundation for building a private crypto messaging application using Java and WebSockets. Remember to carefully consider the security implications and implement appropriate security measures before deploying this code in a production environment.
👁️ Viewed: 9
Comments