338 lines
15 KiB
HTML
338 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Secure Messaging Client</title>
|
|
<style>
|
|
body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
h2 { border-bottom: 2px solid #ccc; padding-bottom: 5px; }
|
|
label { display: block; margin-top: 10px; font-weight: bold; }
|
|
input[type="text"], textarea, select { width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box; }
|
|
button { padding: 10px 15px; margin-top: 10px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px; }
|
|
button:hover { background-color: #0056b3; }
|
|
#keys { background-color: #f4f4f4; padding: 15px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 20px; }
|
|
.key-pair div { margin-bottom: 15px; }
|
|
#log { margin-top: 20px; padding: 10px; background-color: #e9ecef; border: 1px solid #ced4da; height: 150px; overflow-y: scroll; white-space: pre-wrap; }
|
|
.message-box { border: 1px solid #ddd; padding: 15px; margin-bottom: 10px; border-radius: 4px; }
|
|
.encrypted-message { color: #888; font-style: italic; font-size: 0.9em; }
|
|
.decrypted-message { color: #000; font-weight: bold; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>Encrypted Messaging Client</h1>
|
|
|
|
<div id="keys">
|
|
<h2>🔑 Your Identity (Alice)</h2>
|
|
<p>Generate a new RSA key pair for Alice, or paste existing keys (in Base64 X.509/PKCS#8 format).</p>
|
|
<button id="generateKeysBtn">1. Generate New Key Pair</button>
|
|
<div class="key-pair">
|
|
<label for="publicKey">Your Public Key (X.509 Base64 - for sharing):</label>
|
|
<textarea id="publicKey" rows="5" placeholder="Public Key"></textarea>
|
|
<label for="privateKey">Your Private Key (PKCS#8 Base64 - MUST BE KEPT SECRET):</label>
|
|
<textarea id="privateKey" rows="5" placeholder="Private Key"></textarea>
|
|
</div>
|
|
<p>Use a single line for the keys to match the server's clean processing of Base64 strings.</p>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div id="send">
|
|
<h2>✉️ Send Message</h2>
|
|
<label for="recipientKey">Recipient's Public Key (The key you encrypt FOR):</label>
|
|
<textarea id="recipientKey" rows="3" placeholder="Paste Recipient's Public Key here (e.g., Bob's Key)"></textarea>
|
|
|
|
<label for="messageContent">Message Content (Plaintext):</label>
|
|
<textarea id="messageContent" rows="3" placeholder="Type your secret message here..."></textarea>
|
|
|
|
<button onclick="sendMessage()">2. Encrypt, Sign, & Send Message</button>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div id="receive">
|
|
<h2>📥 Retrieve Messages</h2>
|
|
<p>The server logic requires a signature even for a retrieve request.</p>
|
|
<button onclick="retrieveMessages()">3. Retrieve & Decrypt Messages</button>
|
|
<div id="incomingMessages">
|
|
<p>Decrypted messages will appear here.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h2>Console Log</h2>
|
|
<div id="log">Awaiting action...</div>
|
|
|
|
<script>
|
|
// --- CONFIGURATION ---
|
|
const SERVER_URL = "http://localhost:8080/messages"; // **CHANGE THIS TO YOUR SERVER URL**
|
|
|
|
// RSA Algorithm Constants for Web Crypto API
|
|
const SIGN_ALGORITHM = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
const ENCRYPT_ALGORITHM = { name: "RSA-OAEP", hash: "SHA-256" };
|
|
const KEY_PARAMS = {
|
|
name: "RSA-OAEP",
|
|
modulusLength: 2048,
|
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
|
|
hash: "SHA-256"
|
|
};
|
|
const KEY_USAGES = ["encrypt", "decrypt"];
|
|
|
|
// Imported keys stored internally as CryptoKey objects
|
|
let cryptoKeyPair = null;
|
|
|
|
// --- UTILITY FUNCTIONS ---
|
|
|
|
/** Logs a message to the console and the HTML log area. */
|
|
function log(message) {
|
|
console.log(message);
|
|
const logDiv = document.getElementById('log');
|
|
logDiv.innerHTML = new Date().toLocaleTimeString() + " | " + message + "\n" + logDiv.innerHTML;
|
|
}
|
|
|
|
/** Converts ArrayBuffer to Base64 (needed for crypto exports/imports). */
|
|
function bufferToBase64(buffer) {
|
|
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
|
|
}
|
|
|
|
/** Converts Base64 to ArrayBuffer (needed for crypto exports/imports). */
|
|
function base64ToBuffer(b64) {
|
|
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
}
|
|
|
|
/** Converts a string to ArrayBuffer for signing/encrypting. */
|
|
function strToBuffer(str) {
|
|
return new TextEncoder().encode(str);
|
|
}
|
|
|
|
/** Converts ArrayBuffer to string for decryption result. */
|
|
function bufferToStr(buffer) {
|
|
return new TextDecoder().decode(buffer);
|
|
}
|
|
|
|
// --- KEY MANAGEMENT ---
|
|
|
|
document.getElementById('generateKeysBtn').onclick = async () => {
|
|
try {
|
|
// Generate a full key pair suitable for both signing and encryption/decryption
|
|
cryptoKeyPair = await crypto.subtle.generateKey(KEY_PARAMS, true, KEY_USAGES);
|
|
|
|
// Export Public Key (SPKI format, required by Java's X509EncodedKeySpec)
|
|
const pubBuffer = await crypto.subtle.exportKey('spki', cryptoKeyPair.publicKey);
|
|
document.getElementById('publicKey').value = bufferToBase64(pubBuffer);
|
|
|
|
// Export Private Key (PKCS8 format, required for server-side compatibility)
|
|
const privBuffer = await crypto.subtle.exportKey('pkcs8', cryptoKeyPair.privateKey);
|
|
document.getElementById('privateKey').value = bufferToBase64(privBuffer);
|
|
|
|
log("New key pair generated successfully. Public key is ready for sharing.");
|
|
} catch (e) {
|
|
log("ERROR: Could not generate keys. " + e.message);
|
|
}
|
|
};
|
|
|
|
/** Imports a public key from Base64 string for ENCRYPTION. */
|
|
async function importRecipientPublicKey(b64Key) {
|
|
const keyBuffer = base64ToBuffer(b64Key.replaceAll(/\s/g, ''));
|
|
return crypto.subtle.importKey(
|
|
'spki', // Use 'spki' for X.509 public key format
|
|
keyBuffer,
|
|
ENCRYPT_ALGORITHM,
|
|
false, // not extractable
|
|
['encrypt']
|
|
);
|
|
}
|
|
|
|
/** Imports the user's private key from Base64 string for DECRYPTION. */
|
|
async function importUserPrivateKey(b64Key) {
|
|
const keyBuffer = base64ToBuffer(b64Key.replaceAll(/\s/g, ''));
|
|
return crypto.subtle.importKey(
|
|
'pkcs8', // Use 'pkcs8' for private key format
|
|
keyBuffer,
|
|
ENCRYPT_ALGORITHM,
|
|
false, // not extractable
|
|
['decrypt']
|
|
);
|
|
}
|
|
|
|
/** Imports the user's private key from Base64 string for SIGNING. */
|
|
async function importUserSigningKey(b64Key) {
|
|
const keyBuffer = base64ToBuffer(b64Key.replaceAll(/\s/g, ''));
|
|
return crypto.subtle.importKey(
|
|
'pkcs8',
|
|
keyBuffer,
|
|
SIGN_ALGORITHM,
|
|
false,
|
|
['sign']
|
|
);
|
|
}
|
|
|
|
// --- MESSAGE LOGIC ---
|
|
|
|
/** Canonicalizes the set of messages for deterministic signing. */
|
|
function getEnvelopeStringToSign(messages, publicKey) {
|
|
const messageData = messages
|
|
.map(m => m.encryptedMessage + ";" + m.senderPublicKey + ";" + m.recipientPublicKey + ";" + m.timestamp)
|
|
.sort() // CRUCIAL: Must sort for deterministic signing
|
|
.join("|");
|
|
|
|
return messageData + ";" + publicKey;
|
|
}
|
|
|
|
/** Sends a message to the server. */
|
|
async function sendMessage() {
|
|
const messageContent = document.getElementById('messageContent').value.trim();
|
|
const recipientKey = document.getElementById('recipientKey').value.trim();
|
|
const senderKey = document.getElementById('publicKey').value.trim();
|
|
const privateKeyB64 = document.getElementById('privateKey').value.trim();
|
|
|
|
if (!messageContent || !recipientKey || !senderKey || !privateKeyB64) {
|
|
return log("ERROR: All fields (message, recipient key, your public/private keys) must be filled.");
|
|
}
|
|
|
|
try {
|
|
// 1. Import Recipient's Public Key for ENCRYPTION
|
|
const pubKey = await importRecipientPublicKey(recipientKey);
|
|
|
|
// 2. Encrypt the Message
|
|
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
ENCRYPT_ALGORITHM,
|
|
pubKey,
|
|
strToBuffer(messageContent)
|
|
);
|
|
const encryptedMessage = bufferToBase64(encryptedBuffer);
|
|
log("Message encrypted. Ciphertext size: " + encryptedMessage.length + " bytes.");
|
|
|
|
// 3. Construct the Message and Envelope DTOs
|
|
const message = {
|
|
encryptedMessage: encryptedMessage,
|
|
senderPublicKey: senderKey,
|
|
recipientPublicKey: recipientKey,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
const envelope = {
|
|
messages: [message],
|
|
publicKey: senderKey,
|
|
signature: ""
|
|
};
|
|
|
|
// 4. Canonicalize and Sign the Envelope
|
|
const stringToSign = getEnvelopeStringToSign(envelope.messages, envelope.publicKey);
|
|
const signingKey = await importUserSigningKey(privateKeyB64);
|
|
|
|
const signatureBuffer = await crypto.subtle.sign(
|
|
SIGN_ALGORITHM,
|
|
signingKey,
|
|
strToBuffer(stringToSign)
|
|
);
|
|
envelope.signature = bufferToBase64(signatureBuffer);
|
|
|
|
log("Envelope signed. Signature length: " + envelope.signature.length + " bytes.");
|
|
|
|
// 5. Send to Server
|
|
const response = await fetch(SERVER_URL + "/receiveMessage", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(envelope)
|
|
});
|
|
|
|
const result = await response.text();
|
|
log(`Server Response (SEND): ${result}`);
|
|
} catch (e) {
|
|
log("FATAL ERROR during send: " + e.message);
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
/** Retrieves and decrypts messages from the server. */
|
|
async function retrieveMessages() {
|
|
const userKey = document.getElementById('publicKey').value.trim();
|
|
const privateKeyB64 = document.getElementById('privateKey').value.trim();
|
|
const incomingDiv = document.getElementById('incomingMessages');
|
|
incomingDiv.innerHTML = ''; // Clear previous messages
|
|
|
|
if (!userKey || !privateKeyB64) {
|
|
return log("ERROR: Your public and private keys must be filled to retrieve messages.");
|
|
}
|
|
|
|
try {
|
|
// 1. Prepare Envelope for Retrieve Request (uses an empty message set)
|
|
const envelope = {
|
|
messages: [], // Message list is empty for a retrieval request
|
|
publicKey: userKey,
|
|
signature: ""
|
|
};
|
|
|
|
// 2. Canonicalize and Sign the Envelope
|
|
const stringToSign = getEnvelopeStringToSign(envelope.messages, envelope.publicKey);
|
|
const signingKey = await importUserSigningKey(privateKeyB64);
|
|
|
|
const signatureBuffer = await crypto.subtle.sign(
|
|
SIGN_ALGORITHM,
|
|
signingKey,
|
|
strToBuffer(stringToSign)
|
|
);
|
|
envelope.signature = bufferToBase64(signatureBuffer);
|
|
|
|
// 3. Send to Server
|
|
const response = await fetch(SERVER_URL + "/retrieveMessages", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(envelope)
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
return log(`Server Error (${response.status}): ${await response.text()}`);
|
|
}
|
|
|
|
const messages = await response.json();
|
|
if (!messages || messages.length === 0) {
|
|
incomingDiv.innerHTML = "<p>No new messages found.</p>";
|
|
return log("Successfully checked for messages. None found.");
|
|
}
|
|
|
|
log(`Retrieved ${messages.length} message(s). Attempting decryption...`);
|
|
|
|
// 4. Import User's Private Key for DECRYPTION
|
|
const privateKey = await importUserPrivateKey(privateKeyB64);
|
|
|
|
// 5. Decrypt Each Message
|
|
for (const msg of messages) {
|
|
const box = document.createElement('div');
|
|
box.className = 'message-box';
|
|
let decryptedContent = "--- Decryption Failed ---";
|
|
|
|
try {
|
|
const encryptedBuffer = base64ToBuffer(msg.encryptedMessage);
|
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
ENCRYPT_ALGORITHM,
|
|
privateKey,
|
|
encryptedBuffer
|
|
);
|
|
decryptedContent = bufferToStr(decryptedBuffer);
|
|
} catch (e) {
|
|
log(`Decryption failed for a message from ${msg.senderPublicKey}: ${e.message}`);
|
|
}
|
|
|
|
box.innerHTML = `
|
|
<p><strong>Sender:</strong> ${msg.senderPublicKey.substring(0, 30)}...</p>
|
|
<p><strong>Timestamp:</strong> ${new Date(msg.timestamp).toLocaleString()}</p>
|
|
<p class="encrypted-message">Encrypted Data: ${msg.encryptedMessage.substring(0, 50)}...</p>
|
|
<p><strong>Decrypted Message:</strong> <span class="decrypted-message">${decryptedContent}</span></p>
|
|
`;
|
|
incomingDiv.appendChild(box);
|
|
}
|
|
|
|
log(`Successfully retrieved and processed ${messages.length} message(s).`);
|
|
|
|
} catch (e) {
|
|
log("FATAL ERROR during retrieval: " + e.message);
|
|
console.error(e);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html> |