End-to-end encryption for HTML forms


written by Pranav Chakkarwar

published on 01 Sep 2021
updated on 20 Jun 2022

Inspiration

In Jul of 2021 I came across a post from Tutanota as they introduced secure connect, an end-to-end-encrypted contact form for Journalists and alike. I then forgot about it until one fine day it stuck me that I could use the OpenPGP JavaScript library to implement something similar to secure connect. Within an hour, I was able to implement a basic end-to-end-encrypted contact form. Thank my skills to write 10 lines of code and thank the contributors who wrote much more lines to implement PGP in JavaScript. Tutanota’s secure connect has more features and better privacy protections than my form, but my implementation is secure enough for most people and can be self-hosted for free. In the end, it’s up to you.

Advantages of using end-to-end encryption

Almost all form backend providers support submissions over TLS/HTTPS. That means, if someone submits a message on your website, the message is already encrypted between the sender, form’s backend provider, form’s email provider, your email provider and you, but that also adds more participants to your conversation: form and email service providers. So, if you want to keep your conversation between you and the sender only, you’ll need an end-to-end encrypted contact form that encrypts messages on the client side before transmitting them. This way, email or backend service providers would not understand the contents of a message and as a result, you can use this approach to handle sensitive communications. This solution is also resistant to person-in-the-middle attacks. But, keep in mind that metadata, like, time of submission, IP address, etc, may still be recorded. I have also not considered any security aspects of end users or browsers. Like cross-site-scripting attacks or browser extensions. I will probably work on it when I have some time.

Take a look at my contact form to get an idea of the user experience.

Building a basic contact form

Add two inputs (name and email) outside the form because we obviously don’t want to any data that can be understood by some server. Rather, we will combine all the information and encrypt it before submission. I have surrounded the inputs with a div and assigned and id=useless to hide it using JavaScript after encrypting the message.

<div id="useless">
  <input
    class="form-input"
    type="text"
    id="visitor-name"
    placeholder="Your Name"
  />

  <input
    class="form-input"
    type="email"
    id="visitor-contact"
    placeholder="Your Email"
  />
</div>

Now, we will add a basic form with a submit button and a textarea for the main message. I am also adding a paragraph tag with id=form-result that can be modified using JavaScript to indicate submission status of the form.

<form id="e2ee-form">
  <textarea
    class="form-input"
    id="visitor-message"
    name="message from visitor"
    cols="70"
    rows="10"
    placeholder="Your Message"
  ></textarea>

  <input class="form-input" type="hidden" name="apikey" value="ACCESS_KEY" />

  <p id="form-result"></p>

  <button class="btn" id="submit-button" hidden>Send encrypted message</button>
</form>

Lastly, we need a button to call the encryptUserMessage() function.

<button class="btn" id="encrypt-button" onclick="encryptUserMessage()">
  Encrypt my message
</button>

Make the form look pretty

body {
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  text-align: center;
}

input,
textarea {
  font-family: inherit;
  font-size: inherit;
  background-color: inherit;
  color: #fff;
  border: 2px solid #000;
  border-radius: 0.5em;
  outline: none;
}

.btn {
  font-family: inherit;
  font-size: inherit;
  padding: 0.5em;
  margin: 0.5em;
  background-color: #000;
  color: #fff;
  border: none;
  outline: none;
  cursor: pointer;
  border-radius: 0.5em;
}

#submit-button {
  color: #fff;
  background-color: #1e7e34;
  border-color: #1c7430;
}

Using JavaScript to make magic

Firstly, we need some external JavaScript files (openPGP.js) for encrypting the sender’s message. You can either download the openPGP.js script or let UNPKG deliver it for you. Instructions for both are available on openPGP.js github readme.

<script src="openpgp.min.js"></script>
<script src="main.js"></script>

Did you notice that I also added a main.js file? Yes, we will use it to pass our public key to openPGP.js, which will be used to encrypt the sender’s message.

OpenPGP provides a readKey function to read your armoredKey. We will pass a public key to it and store the result in a const.

const key = *PASTE YOUR PUBLIC KEY HERE*;
const publicKey = await openpgp.readKey({ armoredKey: key });

Once a user enters their contact details, name and a message, we will combine all three inputs as one whole message.

var combinedMessage =
  "Message: " +
  document.getElementById("visitor-message").value +
  "\n" +
  "Name: " +
  document.getElementById("visitor-name").value +
  "\n" +
  "Contact: " +
  document.getElementById("visitor-contact").value;

Encrypting a message is fairly simple and can be done by using an encrypt function from the OpenPGP library. You just need to pass a message and a processed public key as arguments to the function. To do that, create an async function, so you can call it using the encrypt button.

The encrypt button will also hide all unnecessary fields (email, name, encrypt message button) and assign the encrypted message as the value to the textarea, because the form will send any text that is included within the textarea. It also serves as a visual cue to the user that their message has been encrypted!

var encrypted = await openpgp.encrypt({
  message: await openpgp.createMessage({ text: combinedMessage }),
  encryptionKeys: publicKey,
});

document.getElementById("visitor-message").value = encrypted;
document.getElementById("submit-button").removeAttribute("hidden");
document.getElementById("encrypt-button").setAttribute("hidden", "");
document.getElementById("useless").setAttribute("hidden", "");

Below is the full function to encrypt a message.

async function encryptUserMessage() {
  const key = *PASTE YOUR PUBLIC KEY HERE*;
  const publicKey = await openpgp.readKey({ armoredKey: key });

  var combinedMessage =
    "Message: " +
    document.getElementById("visitor-message").value +
    "\n" +
    "Name: " +
    document.getElementById("visitor-name").value +
    "\n" +
    "Contact: " +
    document.getElementById("visitor-contact").value;

  var encrypted = await openpgp.encrypt({
    message: await openpgp.createMessage({ text: combinedMessage }),
    encryptionKeys: publicKey,
  });

  document.getElementById("visitor-message").value = encrypted;
  document.getElementById("submit-button").removeAttribute("hidden");
  document.getElementById("encrypt-button").setAttribute("hidden", "");
  document.getElementById("useless").setAttribute("hidden", "");
}

The code to submit the contact form should be self-explanatory.

const form = document.getElementById("e2ee-form");
const result = document.getElementById("form-result");

form.addEventListener("submit", function (e) {
  const formData = new FormData(form);
  e.preventDefault();
  var object = {};
  formData.forEach((value, key) => {
    object[key] = value;
  });
  var json = JSON.stringify(object);
  result.innerHTML = "Please wait...";

  fetch("https://api.form.form/submit", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: json,
  })
    .then(async (response) => {
      let json = await response.json();
      if (response.status == 200) {
        result.innerHTML = "Congrats your message is recieved!";
        document.getElementById("visitor-message").setAttribute("hidden", "");
        document.getElementById("submit-button").setAttribute("hidden", "");
      } else {
        console.log(response);
        result.innerHTML = "Something went wrong!";
      }
    })
    .catch((error) => {
      console.log(error);
      result.innerHTML = "Something went wrong!";
    });
});

I hope this will serve as a cool programming project for your weekend. Stay subscribed.

Get an E2EE contact form

In the past, I had a section where people could easily get their own contact form. It turns out that the process wasn’t all that simple for many people. I’m now working on a better solution.