~dricottone/blog

4edfb14d72c852860c800f7f27cdd48bc8fe630b — Dominic Ricottone 2 years ago 044b763
Adding live chat page
4 files changed, 138 insertions(+), 2 deletions(-)

M content/_index.md
A content/chat.md
A layouts/shortcodes/chat.html
A static/js/chat.js
M content/_index.md => content/_index.md +1 -2
@@ 19,6 19,5 @@ for bi-annual research reports. Prior to joining Fors Marsh Group, he has
worked for the Harris Poll in a similar role, and for Chris Harris & Associate
as a political campaign finance intern.

You can find my CV, code repositories, and other personal work here. Below are
my most recent blog posts.
You can find my CV, code repositories, and other personal work here.


A content/chat.md => content/chat.md +9 -0
@@ 0,0 1,9 @@
---
title: Live Chat
chat: true
---

Here's a live chat app, with optional symmetric key encryption.

{{< chat >}}


A layouts/shortcodes/chat.html => layouts/shortcodes/chat.html +6 -0
@@ 0,0 1,6 @@
<input id="passwd-input" type="text">
<button id="passwd-button">Update Key</button>
<ul id="chat-room"></ul>
<input id="chat-input" type="text">
<button id="chat-button">Send</button>


A static/js/chat.js => static/js/chat.js +122 -0
@@ 0,0 1,122 @@
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

// conversion functions
function arrayBufferToArray(buf) {
  return new Uint8Array(buf);
}
function arrayToBase64(arr) {
  return btoa(String.fromCharCode.apply(null, arr));
};
function base64ToArray(b64) {
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));
};
function stringToArray(str) {
  return new TextEncoder().encode(str);
};
function arrayToString(arr) {
  return new TextDecoder().decode(arr);
};

// cryptography
async function password(passwd) {
  return window.crypto.subtle.importKey("raw", stringToArray(passwd), "PBKDF2", false, ["deriveKey"]);
};
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 encrypt(str) {
  const salt = window.crypto.getRandomValues(new Uint8Array(saltSize));
  const iv = window.crypto.getRandomValues(new Uint8Array(ivSize));

  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));
  } catch (e) {
    console.log("encryption failed");
    return str;
  }
};
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);

  try {
    const key = await derive(passkey, salt);
    const buf = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, msg);
    return arrayToString(arrayBufferToArray(buf));
  } catch (e) {
    console.log("decryption failed");
    return blob;
  }
};

// initialize passkey to null
var passkey;

function connect() {
  socket = new WebSocket('wss://api.dominic-ricottone.com/chat');

  // On close, reconnect after 1s (=1000ms)
  socket.onclose = () => {
    setTimeout(connect, 1000);
  };

  socket.onmessage = async (m) => {
    const el = document.createElement('li');
    if (passkey == null) {
      el.innerHTML = m.data;
    } else {
      const decrypted = await decrypt(m.data);
      el.innerHTML = decrypted;
    }
    document.getElementById('chat-room').appendChild(el);
  };
};

// try to initialize socket to a connection
var socket;
connect();

document.addEventListener("DOMContentLoaded", () => {
  // chat interface
  const chatInput = document.getElementById('chat-input');
  const chatButton = document.getElementById('chat-button');

  chatButton.onclick = async () => {
    if (passkey == null) {
      socket.send(chatInput.value);
    } else {
      const encrypted = await encrypt(chatInput.value);
      socket.send(encrypted);
    }
  };

  chatInput.addEventListener('keyup', (event) => {
    if (event.keyCode === 13) {
      event.preventDefault();
      chatButton.click();
    }
  });

  // password interface
  const passwdInput = document.getElementById('passwd-input');
  const passwdButton = document.getElementById('passwd-button');

  passwdButton.onclick = async () => {
    const key = await password(passwdInput.value);
    passkey = key
  };
});