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