~dricottone/simple-chat

3fd5e03203d8708fcb3b2da80bc8b1fae0cbf338 — Dominic Ricottone 2 years ago 11a6d55
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.
2 files changed, 107 insertions(+), 20 deletions(-)

M client/chat.js
M client/index.html
M client/chat.js => client/chat.js +106 -20
@@ 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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;');
};


@@ 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);
    }
  };

M client/index.html => client/index.html +1 -0
@@ 7,6 7,7 @@
  <script src="chat.js"></script>
</head>
<body>
  <div id="pubkey-shown"></div>
  <input id="passwd-input" type="text">
  <button id="passwd-button">Update Key</button>
  <ul id="chat-room"></ul>