Categories
Frontend

Simple AES end-to-end encryption using crypto-js

In one of my recent projects, I wanted to ensure that the end users could securely share resources between one another knowing that the service operator (in this case being me) won’t be able to access their data.

End-to-end encryption use case

As a logical consequence, I ended up with an attempt to implement end-to-end encryption in my app. In order to keep things simple, knowing that given the app’s functionality users would be likely to use a pre-shared password to access a resource, I decided to use the AES algorithm for encrypting and decrypting shared resources.

You may be familiar with this kind of approach if you have ever sent a message using Protonmail to a recipient whose email account is hosted by a different provider.

The concept itself is quite simple:

  1. User 1 creates a resource and sets a password
  2. User 1 client (in my case the web browser) uses the password to encrypt the resource using AES and sends the encrypted resource to the server
  3. Server stores the encrypted resource
  4. User 2 requests and receives the encrypted resource from the server
  5. User 2 enters the shared password and his clients decrypts the message

Client-side implementation

All right, let’s start coding! For the sake of simplicity, let’s assume that our resource is just a plain text message. Also, we will focus just on the frontend of this project since client-side is where all the magic is going to happen.

In order not to re-implement the AES algorithm, let’s use the crypto-js library. It features, among many other crypto standards, a convenient implementation of Advanced Encryption Standard (AES).

For demonstration purposes we are going to setup a simple HTML file with two forms. We are going to use the first form for encrypting and the other for decrypting the message.

<div class="row">
  <div class="col s-12 m-6">
    <h2>Encrypt message</h2>
    <form id="encrypt-form">
      <div class="form-group">
        <label for="message-encrypt">Message</label>
        <textarea class="form-control" id="message-encrypt" rows="3"></textarea>
      </div>
      <div class="form-group">
        <label for="encrypt-password">Password</label>
        <input type="text" class="form-control" id="encrypt-password" placeholder="Password">
      </div>
      <div class="form-group">
        <label for="encrypted-output">Output</label>
        <textarea readonly class="form-control" id="encrypted-output" rows="3"></textarea>
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
  <div class="col s-12 m-6">
    <h2>Decrypt message</h2>
    <form id="decrypt-form">
      <div class="form-group">
        <label for="message-decrypt">Encrypted message</label>
        <textarea class="form-control" id="message-decrypt" rows="3"></textarea>
      </div>
      <div class="form-group">
        <label for="decrypt-password">Password</label>
        <input type="text" class="form-control" id="decrypt-password" placeholder="Password">
      </div>
      <div class="form-group">
        <label for="encrypted-output">Output</label>
        <textarea disabled class="form-control" id="encrypted-output" rows="3"></textarea>
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
</div>

Let’s add JavaScript code that will handle message encrypting:

$(document).on('submit', 'form#encrypt-form', function(){
  
  var message = $('#message-encrypt').val();
  var password = $('#encrypt-password').val();
  
  var ciphertext = CryptoJS.AES.encrypt(message, password)
                   .toString();
  
  $('#encrypted-output').val(ciphertext);
  
  return false;
});
encrypting text with AES using crypto-js
Finished form – encrypting messages with AES

That wasn’t that bad, was it? It all comes down to one call to CryptoJS library. Now we are going to add a similar script for decrypting the message:

$(document).on('submit', 'form#decrypt-form', function(){
  var encrypted = $('#message-decrypt').val();
  var password = $('#decrypt-password').val();
  var decrypted = CryptoJS.AES.decrypt(encrypted,password)
                  .toString(CryptoJS.enc.Utf8);
  
  $('#decrypted-output').val(decrypted);
  
  return false;
});
javascript aes decrypting with crypto-js
Finished form – decrypting messages with AES

Possible improvements

AES Password validity check

There is one more subject that we haven’t touched on: how to determine the entered password’s validity?

It’s possible that an invalid password will produce a gibberish output. Instead of showing that random-looking string to the user, we should rather display a message informing the user that an invalid passphrase was entered. So how can you tell a valid output and an invalid one apart?

What we know for sure is that AES (and block ciphers in general) is a bijective function. It means that for every possible input value there is exactly one output value.

Adding message headers

So how do we validate a password? One approach would be to prepend the message with a predefined header. Whenever decrypting a message, we would check if it starts with out header and if it does, we would assume that it’s a valid message.

In order to implement that, we just have to update our encrypting function like so:

const header = '#this is our arbitrary header#';

var ciphertext = CryptoJS.AES.encrypt(header + message, password).toString();

and predictably update the decrypting function like so:

var decrypted = CryptoJS.AES.decrypt(encrypted,password)
                .toString(CryptoJS.enc.Utf8);
if (decrypted.startsWith(header)) {
  var message = decrypted.slice(header.length)
  //output decrypted message
} else {
  //inform the user that the message is invalid
}

Bare in mind that for a given output there can be more than one (input starting with our header, password) pair. The longer the header is, the lower the risk of collision becomes.

You can get the source code of this post on CodePen.

Leave a Reply

Your email address will not be published. Required fields are marked *