Upload files to "src/main/java/com/miniapps/messaging/controller"
This commit is contained in:
@@ -0,0 +1,239 @@
|
|||||||
|
package com.miniapps.messaging.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
@Controller("/")
|
||||||
|
public class MessageServer {
|
||||||
|
|
||||||
|
private final static String KEY_ALGORITHM = "RSA";
|
||||||
|
private final static String SIGN_ALGORITHM = "SHA256withRSA";
|
||||||
|
|
||||||
|
// Instance Data
|
||||||
|
private final Set<Message> storedMessages = new HashSet<>();
|
||||||
|
private final Set<String> allowedPeers = new HashSet<>();
|
||||||
|
private String masterKey = "";
|
||||||
|
|
||||||
|
// --- Data Transfer Objects (DTOs) ---
|
||||||
|
// Public fields are kept for simplicity as requested.
|
||||||
|
|
||||||
|
public static class Message {
|
||||||
|
public String encryptedMessage;
|
||||||
|
public String senderPublicKey;
|
||||||
|
public String recipientPublicKey;
|
||||||
|
public String timestamp; // Should ideally be Instant or long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The canonical string representation of the message content for signing.
|
||||||
|
* The order is crucial.
|
||||||
|
*/
|
||||||
|
public String getStringToSign() {
|
||||||
|
// Note: timestamp is included here. On the client, this is the creation time.
|
||||||
|
return this.encryptedMessage + ";" +
|
||||||
|
this.senderPublicKey + ";" +
|
||||||
|
this.recipientPublicKey + ";" +
|
||||||
|
this.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MessageEnvelope {
|
||||||
|
public Set<Message> messages;
|
||||||
|
public String publicKey;
|
||||||
|
public String signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The canonical string representation of the entire envelope content for signing.
|
||||||
|
* Order: All messages (sorted and joined) + sender's public key.
|
||||||
|
*/
|
||||||
|
public String getStringToSign() {
|
||||||
|
// Sort messages to ensure deterministic signing (crucial for Sets)
|
||||||
|
String messageData = messages.stream()
|
||||||
|
.map(Message::getStringToSign)
|
||||||
|
.sorted() // Ensure a consistent signing order
|
||||||
|
.collect(Collectors.joining("|"));
|
||||||
|
|
||||||
|
return messageData + ";" + this.publicKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Server Initialization ---
|
||||||
|
|
||||||
|
public void init(String keysStr) {
|
||||||
|
// FIX: Corrected fixed-size list issue and used get(0) for compatibility
|
||||||
|
List<String> keys = new ArrayList<>(Arrays.asList(keysStr.split(";")));
|
||||||
|
if (keys.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Initialization string cannot be empty.");
|
||||||
|
}
|
||||||
|
masterKey = keys.get(0);
|
||||||
|
allowedPeers.addAll(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Core Utility Methods ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a digital signature against a message using a public key.
|
||||||
|
* @param messageToVerify The canonical string of the data that was signed.
|
||||||
|
* @param publicKey The base64-encoded X.509 public key of the signer.
|
||||||
|
* @param signature The base64-encoded signature.
|
||||||
|
* @return true if the signature is valid, false otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean verifySignature(String messageToVerify, String publicKey, String signature) {
|
||||||
|
// Renamed variables for clarity
|
||||||
|
try {
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
|
||||||
|
// Remove whitespace before decoding
|
||||||
|
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
|
||||||
|
Base64.getDecoder().decode(publicKey.replaceAll("\\s+", ""))
|
||||||
|
);
|
||||||
|
PublicKey pubKey = keyFactory.generatePublic(keySpec);
|
||||||
|
|
||||||
|
Signature sig = Signature.getInstance(SIGN_ALGORITHM);
|
||||||
|
sig.initVerify(pubKey);
|
||||||
|
sig.update(messageToVerify.getBytes());
|
||||||
|
|
||||||
|
return sig.verify(Base64.getDecoder().decode(signature.replaceAll("\\s+", "")));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// BEST PRACTICE: Log the exception to aid debugging security failures
|
||||||
|
System.err.println("Signature verification failed: " + e.getMessage());
|
||||||
|
// e.printStackTrace(); // Use proper logging in a real app
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the timestamps in the messages are fresh (e.g., within 5 minutes).
|
||||||
|
* This is a critical step to prevent replay attacks.
|
||||||
|
*/
|
||||||
|
private boolean isFresh(MessageEnvelope envelope) {
|
||||||
|
Instant fiveMinutesAgo = Instant.now().minusSeconds(300);
|
||||||
|
for (Message m : envelope.messages) {
|
||||||
|
try {
|
||||||
|
if (Instant.parse(m.timestamp).isBefore(fiveMinutesAgo)) {
|
||||||
|
return false; // Message is too old
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Invalid message timestamp format: " + m.timestamp);
|
||||||
|
return false; // Invalid timestamp format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Peer List Operations ---
|
||||||
|
|
||||||
|
public String addPeerList(MessageEnvelope envelope) {
|
||||||
|
// FIX: Signature verification now uses the envelope's content string
|
||||||
|
if (!envelope.publicKey.equals(masterKey)) {
|
||||||
|
return "permission denied: only master key can add peers";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifySignature(envelope.getStringToSign(), envelope.publicKey, envelope.signature)) {
|
||||||
|
return "invalid signature";
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedPeers.addAll(
|
||||||
|
envelope.messages.stream().map(m -> m.recipientPublicKey).collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String removePeers(MessageEnvelope envelope) {
|
||||||
|
// FIX: Corrected master key and signature logic
|
||||||
|
if (!envelope.publicKey.equals(masterKey)) {
|
||||||
|
return "permission denied: only master key can remove peers";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifySignature(envelope.getStringToSign(), envelope.publicKey, envelope.signature)) {
|
||||||
|
return "invalid signature";
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedPeers.removeAll(
|
||||||
|
envelope.messages.stream().map(m -> m.recipientPublicKey).collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String listPeers() {
|
||||||
|
return allowedPeers.stream().sorted().collect(Collectors.joining("|"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Server Message Operations ---
|
||||||
|
@ResponseBody
|
||||||
|
@PostMapping("/messages/receiveMessage")
|
||||||
|
public String receiveMessage(@RequestBody MessageEnvelope envelope)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(!masterKey.isEmpty() && !allowedPeers.contains(envelope.publicKey))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX: Signature verification now uses the envelope's content string
|
||||||
|
if (!verifySignature(envelope.getStringToSign(), envelope.publicKey, envelope.signature)) {
|
||||||
|
return "invalid signature";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// SECURITY CHECK: Replay protection
|
||||||
|
if (!isFresh(envelope)) {
|
||||||
|
return "invalid timestamp: message is too old";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Message message : envelope.messages) {
|
||||||
|
// Check: Ensure the sender's public key inside the message matches the envelope signer
|
||||||
|
if (message.senderPublicKey.equals(envelope.publicKey)) {
|
||||||
|
|
||||||
|
// FIX: Removed side effect. Creating a new Message object to store.
|
||||||
|
Message storedMessage = new Message();
|
||||||
|
storedMessage.encryptedMessage = message.encryptedMessage;
|
||||||
|
storedMessage.senderPublicKey = message.senderPublicKey;
|
||||||
|
storedMessage.recipientPublicKey = message.recipientPublicKey;
|
||||||
|
// Add the server's time (optional, but good for tracking delivery)
|
||||||
|
storedMessage.timestamp = Instant.now().toString();
|
||||||
|
|
||||||
|
storedMessages.add(storedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@PostMapping("/messages/retrieveMessages")
|
||||||
|
public Set<Message> retrieveMessages(@RequestBody MessageEnvelope envelope)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(!masterKey.isEmpty() && !allowedPeers.contains(envelope.publicKey))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// FIX: Signature verification now uses the envelope's content string
|
||||||
|
if (!verifySignature(envelope.getStringToSign(), envelope.publicKey, envelope.signature)) {
|
||||||
|
// Using null to clearly signal a verification/authorization failure
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter messages for the requesting recipient
|
||||||
|
Set<Message> retrieved = storedMessages.stream()
|
||||||
|
.filter(m -> m.recipientPublicKey.equals(envelope.publicKey))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// Remove the retrieved messages from storage
|
||||||
|
//comment it if messages get cancelled every day at midnight
|
||||||
|
storedMessages.removeAll(retrieved);
|
||||||
|
|
||||||
|
return retrieved;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user