~dricottone/blog

886475f15ceade7663816d9378bf72628745798f — Dominic Ricottone 2 years ago 369b714
Chat identity

The chat app now uses digital signatures for identify verification.

A new blog post discusses this implementation and what I've learned
about PGP and digital signature cryptography.

Also, minor typo fixes and adding an aspell recipe.
M Makefile => Makefile +5 -0
@@ 6,6 6,7 @@ RSYNC_OPTS=--recursive --links --compress --delete --chown=$(TARGET_USER):$(TARG
clean:
	rm -rf public resources themes
	rm -rf scripts/cv.aux scripts/cv.log scripts/cv.out scripts/cv.tex
	rm -rf content/posts/*.bak

static/files/dominic-ricottone.pdf: content/cv.md
	sed content/cv.md \


@@ 26,6 27,10 @@ dev: static/files/dominic-ricottone.pdf static/files/dominic-ricottone.html
build: clean static/files/dominic-ricottone.pdf static/files/dominic-ricottone.html
	hugo

.PHONY: check
check:
	for f in content/posts/*.md; do aspell --check $$f; done

.PHONY: sync
sync: build
	rsync $(RSYNC_OPTS) public/ $(TARGET_HOST):/var/deploy/build/blog/public/

M config.toml => config.toml +3 -0
@@ 4,3 4,6 @@ title = "Dominic Ricottone"
disableKinds = ["taxonomy", "taxonomyTerm"]
summaryLength = 20

[markup.goldmark.renderer]
unsafe=true


M content/chat.md => content/chat.md +7 -1
@@ 3,7 3,13 @@ title: Live Chat
chat: true
---

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

Your ephemeral public key is:

<div id="pubkey-shown"></div>

To set or update the key for symmetric encryption, use this text box:

{{< chat >}}


M content/posts/cheatsheet.md => content/posts/cheatsheet.md +5 -5
@@ 29,7 29,7 @@ Strong emphasis, aka bold, with **asterisks** or __underscores__.

Combined emphasis with **asterisks and _underscores_**.

Strikethrough uses two tildes. ~~Scratch this.~~
Strike-through uses two tildes. ~~Scratch this.~~





@@ 46,7 46,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~

   To have a line break without a paragraph, you will need to use two trailing spaces.
   Note that this line is separate, but within the same paragraph.
   (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
   (This is contrary to the typical GFM line break behavior, where trailing spaces are not required.)

* Unordered list can use asterisks
- Or minuses


@@ 153,14 153,14 @@ Markdown | Less | Pretty



## Blockquotes
## Block quotes

> Blockquotes are very handy in email to emulate reply text.
> Block quotes are very handy in email to emulate reply text.
> This line is part of the same quote.

Quote break.

> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a block quote.




A content/posts/identity.md => content/posts/identity.md +70 -0
@@ 0,0 1,70 @@
---
title: Identity
date: 2022-10-06T10:13:32-05:00
draft: true
---

I've been thinking about adding identity verification by way of digital
signatures to my [web chat](https://www.dominic-ricottone.com/chat/).
This has lead to a bit of a deep dive into OpenPGP infrastructure and security
strategy.

A dip into this pond quickly informs a reader that the Web of Trust is flawed
fundamentally. The arguments against it center around a few points:

 1 End users don't hesitate to throw away their PGP keys.
   *(Because who actually knows enough about GnuPG configuration to migrate
   their setup between hosts? Everyone just uses the distro defaults...)*
   And because keys are such cheap things,
   end users don't take proper steps to protect them.
   Altogether, they fail to be trustworthy in the longer term.
 2 End users blindly import a key when a gist tells them to.
 3 End users prefer to verify their identity by password authentication.
   So all commercial and/or ambitious projects eventually outgrow PGP in terms
   of features and functionality.
 4 The infrastructure itself is vulnerable, *a la*
   [certificate poisoning](https://access.redhat.com/articles/4264021).

And on further consideration, there's always a bit of noise on project mail
lists when someone's signing key needs to be revoked.
A toolchain that requires so much maintenance is not a very good toolchain.

But further consideration leads to the question: *if not PGP, then what?*

I remember not too long ago seeing a number of GitHub issues requesting use
of signify for releases.
I didn't think highly of it at the time.
*(Just another BSD trying to force it's philosophy on mere mortals.)*
But in light of the above, maybe a simpler toolchain *is* what we need.

There's currently a public discussion in the
[age community](https://words.filippo.io/dispatches/age-authentication/)
about whether authentication should be supported.
This conversation has highlighted how federated identity is very difficult.

> If you encrypt and then sign, an attacker can strip your signature, replace
> it with their own, and make it look like they encrypted the file even if
> they don't actually know the contents.
>
> If you sign and then encrypt, the recipient can decrypt the file, keep your
> signature, and encrypt it to a different recipient, making it look like you
> intended to send the file to them.

Adding to all the consternation is the fact that I *am* trying to use
signatures in a chat application that is public, i.e. multiple recipients.

----

In the end, I have begun to press ahead with implementing digital signatures.
Signing happens *last*, and does not imply ownership over the content of the
signed message.
Rather, it ensures that *this instance* of the message came from a certain
sender.

At this stage I don't think it's a good idea to put real-world private keys
into the application, so I am just using ephemeral RSA keys and the PKCS #1
scheme for signatures (specifically RSASSA-PKCS1-v1_5).

I am interested in the idea of GPG integration though, so I'm likely to start
looking at a companion client implementation for use on the terminal.


M content/posts/progress_and_the_lack_thereof.md => content/posts/progress_and_the_lack_thereof.md +2 -2
@@ 30,7 30,7 @@ as I coaxed this project into a `pyproject.toml`-based system, I realized that
Goodbye, `setup.cfg`!

I'm happy with where Python packaging landed. It's a shame it took this long.
Or maybe it didn't take *time*, just the [seccession of the packaging
Or maybe it didn't take *time*, just the [secession of the packaging
infrastructure team](https://peps.python.org/pep-0609/) from the rest of the
steering council...



@@ 39,7 39,7 @@ steering council...
Another familiar story: The biggest difference between my old and new projects
is type hints.

I used to typeguard all passed-in arguments as a debugging tool.
I used to type guard all passed-in arguments as a debugging tool.
(If `int(myint)` fails then clearly `myint` isn't what I want it to be).
This made re-using code a *pain in the ass* at the best of times.


M layouts/partials/head.html => layouts/partials/head.html +1 -1
@@ 15,7 15,7 @@
  <!--Stylesheets-->
  <link rel="stylesheet" type="text/css" href="/css/common.css" />
  <link rel="stylesheet" type="text/css" href="/css/blog.css" />
  {{ if .Params.cgit }}     <link rel="stylesheet" type="text/css" href="/css/cgit.css" /> {{ end }}
  {{ if .Params.chat }}     <link rel="stylesheet" type="text/css" href="/css/chat.css" /> {{ end }}
  {{ if .Params.lightbox }} <link rel="stylesheet" type="text/css" href="/css/gallery.css" /> {{ end }}
  {{ if .Params.lightbox }} <link rel="stylesheet" type="text/css" href="/css/lightbox-2.11.3.min.css" /> {{ end }}


D static/css/cgit.css => static/css/cgit.css +0 -418
@@ 1,418 0,0 @@
/* Style like main */
div#cgit {
  font-family: sans-serif;
  font-size: 10pt;
  margin: 0 0 0 calc(100px + 1em);
  padding: 4px;

  --diff-width: 800px;
  --diff-text: #000000;
  --diff-text-header: #000000;
  --diff-head: #f8f9fa;
  --diff-hunk: #9696f3;
  --diff-border: #000000;
  --diff-add: #96f396;
  --diff-add-darker: #0e7c0e;
  --diff-add-bolder: #51eb51;
  --diff-delete: #f39696;
  --diff-delete-darker: #7c0e0e;
  --diff-delete-bolder: #eb5151;
  --diff-change: #f3f396;
  --diff-change-darker: #7c7c0e;
  --diff-context: #ffffff;

  --button-text: #000000;
  --button-color: #f8f9fa;

  --tab-text: #000000;

  --age-newest: #008800;
  --age-newer: #004400;
  --age: #444444;
  --age-older: #888888;
  --age-oldest: #bbbbbb;

  --tag-text: #000000;
  --tag-color: #ffff88;
  --tag-color-main: #88ff88;
}
@media (max-width: 800px) {
  div#cgit {
    margin: 0;
  }
}


/* Tables */
th {
  margin-right: 1em;
  text-align: left;
  border: solid 0px transparent;
  background-clip: padding-box;
}

td {
  margin-right: 1em;
  border: solid 0px transparent;
  background-clip: padding-box;
}

tr > td + td:not(.add, .rem, .upd, .none) {
  border-left-width: 1em;
}

tr > th + th {
  border-left-width: 1em;
}

/* Style like an h2 */
td.reposection {
  margin: 0 0 1em 0;
  font-size: 1.5em;
  line-height: 2em;
}

td a.button {
  margin: 0 1em 0 0;
  padding: 0px 5px;
  color: var(--button-text);
  background-color: var(--button-color);
  text-decoration: none;
  border-radius: 5px;
}

td a.button::before {
  content: "[";
}

td a.button::after {
  content: "]";
}


/* Tabs bar */
table.tabs td:not(.form) a {
  margin-right: 1em;
  color: var(--tab-text);
  text-decoration: none;
}

table.tabs td:not(.form) a::before {
  content: "[";
}

table.tabs td:not(.form) a::after {
  content: "]";
}

table.tabs td.form {
  padding-left: 3em;
}


/* Repository age decorations */
.age-mins {
  color: var(--age-newest);
  font-weight: bold;
}

.age-hours {
  color: var(--age-newest);
}

.age-days {
  color: var(--age-newer);
}

.age-weeks {
  color: var(--age);
}

.age-months {
  color: var(--age-older);
}

.age-years {
  color: var(--age-oldest);
}


/* Insertions and Deletions decorations */
.insertions {
  color: var(--diff-add-darker);
}

.deletions {
  color: var(--diff-delete-darker);
}

/* Branch and Tag decorations */
a.branch-deco,
a.tag-deco,
a.remote-deco,
a.deco {
  margin-left: 1em;
  padding: 0px 5px;
  color: var(--tag-text);
  background-color: var(--tag-color);
  border-radius: 5px;
  text-decoration: none;
}

a.branch-deco::before,
a.tag-deco::before,
a.remote-deco::before,
a.deco::before {
  content: "[";
}

a.branch-deco::after,
a.tag-deco::after,
a.remote-deco::after,
a.deco::after {
  content: "]";
}

table a.branch-deco,
table a.tag-deco,
table a.remote-deco,
table a.deco {
  float: right;
}

a.deco + a.branch-deco {
  background-color: var(--tag-color-main);
}


/* Monospace content
 *  + Log page (logsubject and logmsg cells only)
 *  + Tree page
 *    + path div
 *    + ls-mode cells, ls-dir links, and ls-blob links
 *    + blob table (entire table)
 *    + bin-blob table (entire table)
 *  + Commit page, Commit page's diff components, and Diff page
 *    + commit-info table (sha1 cells only)
 *    + commit-subject and commit-msg divs
 *    + diffstat table (mode, upd, add, and del cells)
 *    + diff table (entire table)
 *    + ssdiff table (entire table)
 */
table td.logsubject,
table td.logmsg,
div.path,
table td.ls-mode,
table td a.ls-dir,
table td a.ls-blob,
table.blob,
table.bin-blob,
table.commit-info td.sha1,
div.commit-subject,
div.commit-msg,
table.diffstat td.mode,
table.diffstat td.upd,
table.diffstat td.add,
table.diffstat td.del,
table.diff,
table.ssdiff {
  font-family: monospace;
  white-space: pre;
}



/* Diff content */

/* Style like an h2 */
div.cgit-panel b {
  margin: 0 0 1em 0;
  color: var(--diff-text-header);
  font-size: 1.5em;
  font-weight: normal;
  line-height: 2em;
}

div.cgit-panel {
  width: var(--diff-width);
  margin: 1em 0 1em 0;
  padding-bottom: 1em;
  background-color: var(--diff-head);
}

table.commit-info {
  width: var(--diff-width);
  margin: 0 0 1em 0;
  background-color: var(--diff-head);
}

/* Style like an h2 */
div.commit-subject {
  width: var(--diff-width);
  padding-top: 1em;
  color: var(--diff-text-header);
  background-color: var(--diff-head);
  font-size: 1.5em;
  line-height: 2em;
}

div.commit-msg {
  width: var(--diff-width);
  margin: 0 0 1em 0;
  padding-bottom: 1em;
  color: var(--diff-text);
  background-color: var(--diff-head);
}

div.diffstat-header {
  width: var(--diff-width);
  background-color: var(--diff-head);
}

/* Style like an h2 */
div.diffstat-header a {
  margin: 0 0 1em 0;
  font-size: 1.5em;
  line-height: 2em;
  text-decoration: none;
  color: var(--diff-text-header);
}

table.diffstat {
  width: var(--diff-width);
  color: var(--diff-text);
  background-color: var(--diff-head);
}

table.diffstat td.graph table {
  min-width: calc(var(--diff-width) * 0.5);
}

table.diffstat td.graph table td {
  height: 7pt;
}

table.diffstat td.graph table td.add {
  background-color: var(--diff-add-bolder);
}

table.diffstat td.graph table td.rem {
  background-color: var(--diff-delete-bolder);
}

div.diffstat-summary {
  width: var(--diff-width);
  margin: 0 0 1em 0;
  padding: 1em 0;
  color: var(--diff-text);
  background-color: var(--diff-head);
  font-style: italic;
}

table.diff,
table.ssdiff {
  min-width: var(--diff-width);
}

table.diff td,
table.ssdiff td {
  color: var(--diff-text);
  font-style: italic;
}

/* Diff components
 *  head          diff metadata
 *  hunk          diff context
 *  add           inserted content (on the right in ssdiff mode)
 *  add_dark      inserted content (ssdiff mode only, on the left)
 *  del           removed content (on the left in ssdiff mode)
 *  del_dark      removed content (ssdiff mode only, on the right)
 *  changed       ssdiff mode only, changed content on the right
 *  changed_dark  ssdiff mode only, changed content on the left
 *  lineno        line numbers
 */
table.diff div.add,
table.ssdiff td.add {
  background: var(--diff-add);
  font-style: normal;
}

table.ssdiff td.add_dark {
  background: var(--diff-add-darker);
  font-style: normal;
}

table.diff div.del,
table.ssdiff td.del {
  background: var(--diff-delete);
  font-style: normal;
}

table.ssdiff td.del_dark {
  background: var(--diff-delete-darker);
  font-style: normal;
}

table.ssdiff td.changed {
  background: var(--diff-change);
  font-style: normal;
}

table.ssdiff td.changed_dark {
  background: var(--diff-change-darker);
  font-style: normal;
}

table.diff div.head,
table.ssdiff td.head {
  background: var(--diff-head);
  border-bottom: solid 1px var(--diff-border);
  font-style: normal;
}

table.diff div.hunk,
table.ssdiff td.hunk {
  background: var(--diff-hunk);
  font-style: normal;
}

table.diff div.ctx,
table.ssdiff td.ctx {
  background: var(--diff-context);
  font-style: normal;
}


/* Style definition
 *  + Generated by highlight 3.9, http://www.andre-simon.de/
 *  + Theme: Kwrite Editor
 */
table.blob .num  { color:#2928ff; }
table.blob .esc  { color:#ff00ff; }
table.blob .str  { color:#ff0000; }
table.blob .dstr { color:#818100; }
table.blob .slc  { color:#838183; font-style:italic; }
table.blob .com  { color:#838183; font-style:italic; }
table.blob .dir  { color:#008200; }
table.blob .sym  { color:#000000; }
table.blob .kwa  { color:#000000; font-weight:bold; }
table.blob .kwb  { color:#830000; }
table.blob .kwc  { color:#000000; font-weight:bold; }
table.blob .kwd  { color:#010181; }
body.hl { background-color:#e0eaee; }
pre.hl  { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New',monospace;}
.hl.num { color:#b07e00; }
.hl.esc { color:#ff00ff; }
.hl.str { color:#bf0303; }
.hl.pps { color:#818100; }
.hl.slc { color:#838183; font-style:italic; }
.hl.com { color:#838183; font-style:italic; }
.hl.ppc { color:#008200; }
.hl.opt { color:#000000; }
.hl.ipl { color:#0057ae; }
.hl.lin { color:#555555; }
.hl.kwa { color:#000000; font-weight:bold; }
.hl.kwb { color:#0057ae; }
.hl.kwc { color:#000000; font-weight:bold; }
.hl.kwd { color:#010181; }


A static/css/chat.css => static/css/chat.css +10 -0
@@ 0,0 1,10 @@
#pubkey-shown {
  width: 90%;
  padding: .25em;
  margin: 0 0 1em 0;
  font-family: monospace;
  background-color: #f8f9fa;
  word-break: break-all;
  border-radius: .5em;
}


M static/js/chat.js => static/js/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);
    }
  };