From 3fd5e03203d8708fcb3b2da80bc8b1fae0cbf338 Mon Sep 17 00:00:00 2001 From: Dominic Ricottone Date: Wed, 12 Oct 2022 23:49:26 -0500 Subject: [PATCH] Adding digital signatures. Messages are now signed by an ephemeral RSA key (specifically RSASSA-PKCS1-v1_5 with 4096-bit length) and this signature is included as the 512 bytes between the IV and message content. Support for adding pubkeys to a keychain is planned, as is storing that keychain between sessions. Support for user-provided RSA keys is planned, as is storing RSA keys (with encryption) between sessions. Some instructions for how to export a PGP key into a compatible format will be necessary. Also, I will need to check keys and reject those that are not 4096 bits long. Eventually the keychain will be extended with user-provided names for each key, and the names will be shown next to chat messages. --- client/chat.js | 126 ++++++++++++++++++++++++++++++++++++++-------- client/index.html | 1 + 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/client/chat.js b/client/chat.js index 6736b56..35c8052 100644 --- a/client/chat.js +++ b/client/chat.js @@ -1,6 +1,7 @@ const saltSize = 16; // recommended size for salt used in password derivation functions const ivSize = 12; // size for initial value array per AES GCM specification const iterationNum = 10000; // lowest recommendable number of iterations for password derivation +const sigSize = 4096; // recommended size for digital signatures // conversion functions function arrayBufferToArray(buf) { @@ -9,6 +10,9 @@ function arrayBufferToArray(buf) { function arrayToBase64(arr) { return btoa(String.fromCharCode.apply(null, arr)); }; +function arrayToArrayBuffer(arr) { + return arr.buffer; +} function base64ToArray(b64) { return Uint8Array.from(atob(b64), (c) => c.charCodeAt(null)); }; @@ -26,42 +30,98 @@ async function password(passwd) { async function derive(key, salt) { return window.crypto.subtle.deriveKey({ name: "PBKDF2", salt: salt, iterations: iterationNum, hash: "SHA-256" }, key, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); }; -function buildMessage(salt, iv, arr) { - let msg = new Uint8Array(saltSize+ivSize+arr.byteLength); - msg.set(salt, 0); - msg.set(iv, saltSize); - msg.set(arr, saltSize+ivSize); - return arrayToBase64(msg); +async function ephemeralKeyPair() { + return window.crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: sigSize, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, false, ["sign", "verify"]); +}; +async function pgpKeyPair(privateKey) { + return window.crypto.subtle.importKey("pkcs8", base64ToArray(privateKey), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign", "verify"]); }; async function encrypt(str) { const salt = window.crypto.getRandomValues(new Uint8Array(saltSize)); const iv = window.crypto.getRandomValues(new Uint8Array(ivSize)); + const arr = arrayToArrayBuffer(stringToArray(str)); try { const key = await derive(passkey, salt); - const buf = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, stringToArray(str)); - return buildMessage(salt, iv, arrayBufferToArray(buf)); + const buf = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, arr); + return [salt, iv, buf]; } catch (e) { console.log("encryption failed"); - return str; + return [new Uint8Array(saltSize), new ArrayBuffer(ivSize), arr]; } }; -async function decrypt(blob) { - const arr = base64ToArray(blob); - const salt = arr.slice(0, saltSize); - const iv = arr.slice(saltSize, saltSize+ivSize); - const msg = arr.slice(saltSize+ivSize); +async function sign(arr) { + try { + return await window.crypto.subtle.sign("RSASSA-PKCS1-v1_5", keypair.privateKey, arr); + } catch(e) { + console.log("signing failed"); + return new ArrayBuffer(sigSize / 8); + } +} +async function justSign(str) { + const arr = arrayToArrayBuffer(stringToArray(str)); + const sig = await sign(arr); + let msg = new Uint8Array(saltSize+ivSize+(sigSize/8)+arr.byteLength); + msg.set(arrayBufferToArray(sig), saltSize+ivSize); + msg.set(arrayBufferToArray(arr), saltSize+ivSize+(sigSize/8)); + + return arrayToBase64(msg); +} +async function encryptAndSign(str) { + const [salt, iv, arr] = await encrypt(str); + const sig = await sign(arr); + + let msg = new Uint8Array(saltSize+ivSize+(sigSize/8)+arr.byteLength); + msg.set(arrayBufferToArray(salt), 0); + msg.set(arrayBufferToArray(iv), saltSize); + msg.set(arrayBufferToArray(sig), saltSize+ivSize); + msg.set(arrayBufferToArray(arr), saltSize+ivSize+(sigSize/8)); + + return arrayToBase64(msg); +}; +async function verify(sig, arr) { + try { + for (let i = 0; i < keychain.length; i++) { + let trust = await window.crypto.subtle.verify("RSASSA-PKCS1-v1_5", keychain[i], sig, arr); + if (trust === true) { return true; } + } + + console.log("could not verify signature"); + return false; + } catch(e) { + console.log("verification failed") + return false; + } +} +async function decrypt(passkey, salt, iv, arr) { try { const key = await derive(passkey, salt); - const buf = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, msg); + const buf = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, arr); return arrayToString(arrayBufferToArray(buf)); } catch (e) { console.log("decryption failed"); - return blob; + return arrayToString(arr); } }; +async function justVerify(blob) { + const arr = base64ToArray(blob); + const sig = arr.slice(saltSize+ivSize, saltSize+ivSize+(sigSize/8)); + const msg = arr.slice(saltSize+ivSize+(sigSize/8)); + + await verify(arrayToArrayBuffer(sig), arrayToArrayBuffer(msg)); + return arrayToString(msg); +} +async function verifyAndDecrypt(blob) { + const arr = base64ToArray(blob); + const salt = arr.slice(0, saltSize); + const iv = arr.slice(saltSize, saltSize+ivSize); + const sig = arr.slice(saltSize+ivSize, saltSize+ivSize+(sigSize/8)); + const msg = arr.slice(saltSize+ivSize+(sigSize/8)); + await verify(sig, msg); + return await decrypt(passkey, salt, iv, msg); +} function escapeHTML(str) { return str.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }; @@ -69,6 +129,25 @@ function escapeHTML(str) { // initialize passkey to null var passkey; +async function initializeKeyChain(el) { + try { + keypair = await ephemeralKeyPair(); + keychain = [keypair.publicKey]; + + const pubkey = await window.crypto.subtle.exportKey("spki", keychain[0]); + el.innerHTML = arrayToBase64(arrayBufferToArray(pubkey)); + } catch(e) { + console.log("failed to create ephemeral keypair"); + el.innerHTML = "n/a"; + } +} + +// initialize keypair to null +var keypair; + +// initialize empty keychain +var keychain = []; + function connect() { socket = new WebSocket('wss://api.dominic-ricottone.com/chat'); @@ -80,9 +159,10 @@ function connect() { socket.onmessage = async (m) => { const el = document.createElement('li'); if (passkey == null) { - el.innerHTML = escapeHTML(m.data); + const msg = await justVerify(m.data); + el.innerHTML = escapeHTML(msg); } else { - const decrypted = await decrypt(m.data); + const decrypted = await verifyAndDecrypt(m.data); el.innerHTML = escapeHTML(decrypted); } document.getElementById('chat-room').appendChild(el); @@ -94,15 +174,21 @@ var socket; connect(); document.addEventListener("DOMContentLoaded", () => { + //key interface + const pubkeyShown = document.getElementById('pubkey-shown'); + initializeKeyChain(pubkeyShown); + // chat interface const chatInput = document.getElementById('chat-input'); const chatButton = document.getElementById('chat-button'); chatButton.onclick = async () => { + const msg = chatInput.value; if (passkey == null) { - socket.send(chatInput.value); + const signed = await justSign(msg); + socket.send(signed); } else { - const encrypted = await encrypt(chatInput.value); + const encrypted = await encryptAndSign(msg); socket.send(encrypted); } }; diff --git a/client/index.html b/client/index.html index 065047d..8e64ad4 100644 --- a/client/index.html +++ b/client/index.html @@ -7,6 +7,7 @@ +
-- 2.45.2