Secure Todo app with Ionic 3

Published: February 18, 2017  •  Updated: March 22, 2018  •  ionic3, cryptography

June 5th 2017: Apple announced the new Safari 11 that supports the latest WebCrypto specification


September 25th 2017: See my new post about Web Cryptography API


In this article we will write one of these ubiquitous Todo applications with Ionic. The app will store the data only on the client, but as a twist, it encrypts the data before it stores it.

Web Cryptography API

First I wrote the application with the Web Cryptography API. A web standard developed by the W3C and built into modern browsers. When you look on caniuse.com the browser support looks very promising.

Unfortunately the app does not run on Safari in the current version (February 2017). Safari hides the Web Cryptography API behind a prefix. That's not a problem and easy to solve. The following snippet maps the prefixed object (crypto.webkitSubtle) to the standard (crypto.subtle).

 if (window.crypto && !window.crypto.subtle && window.crypto.webkitSubtle) {
   window.crypto.subtle = window.crypto.webkitSubtle;
 }

The problem is that Safari does not provide a password derivation algorithm implementation. The app depends on such an algorithm to derive the AES key from a password. If there is a nice polyfill or a different method to derive keys send me a note.

asmcrypto.js

Because I wanted an application that runs on all platforms I switched to asmcrypto.js, a cryptographic library that is written in JavaScript. But in my opinion the Web Cryptography API is the way to go in the future when all browser support it and implement a similar set of algorithms. In this blog post you see the performance benefit of the Web Cryptography API, which is implemented natively in the browsers, compared to cryptography libraries written in JavaScript. If you are interested, here is the source code of the Todo app written with the Web Cryptography API. It works on Chrome (also Chrome on macOS) and on Android.

App

The app we develop in this post contains three pages. The first page the users sees is the Password (html / ts) page. On this page the user enters his password, the app derives the AES key from it and uses that to encrypt and decrypt the todo entries.
Password


The second page (Home: html / ts) lists all the todo entries.
Home


And the third page (Edit: html / ts) contains a form where the user can create and update todo entries.
Edit


We start the development with ionic start and the blank starter application.

ionic start secure-todo blank

Then we install the asmcrypto.js library

cd secure-todo
npm install asmcrypto.js

Next we create the code for the two additional pages.

ionic g page Password
ionic g page Edit

I won't go into the implementation details for these pages. They are written with straightforward Ionic / Angular code and there are already a lot of tutorials how to write a todo app with Ionic (here, here, here).

Next we create the provider that is responsible for encrypting, storing, loading and decrypting the data. The application utilizes Ionic's Storage service for storing and retrieving the data. Storage stores the data in the IndexedDB database, unless the cordova sqlite plugin is installed (cordova plugin add cordova-sqlite-storage), in that case it stores the data in sqlite.

ionic g provider Todo

Derive Key

After the user enters his password and taps on the 'Show Todos' button the app calls the function setPassword.

  setPassword(password: string) {
    this.storage.get('lastTodoId').then(id => this.lastId = id);
    this.deriveAesKey(password);
    return this.decryptTodos();
  }

src/providers/todo/todo.ts

It first loads the last id from the storage. Everytime the user creates a new todo object the application increases id by one and assigns it to the id field of the todo object.
Next it calls the deriveAesKey function that creates the AES key.

  deriveAesKey(password: string) {
    this.aesKey = PBKDF2_HMAC_SHA256.bytes(password, this.salt, this.iterations, 32);
  }

src/providers/todo/todo.ts

PBKDF2 is an algorithm that can be used for deriving a key from a string. The application has to specify a salt, the number of iterations and the number of bytes the function should return. Because we want to use AES with a key length of 256 bit PBKDF2 has to generate a key with 32 bytes. In this app the salt is just a constant string, that should be okay for this single user application. iterations specifies how many times the PBKDF2 algorithm should run. Higher is better, but slower. Recommended is a value of at least 1000, the app uses 4096.

Decryption

As the last statement in the setPassword function the applications calls the decryptTodos function.

  decryptTodos() {
    return this.storage.get('todos').then(encryptedTodos => {
        if (encryptedTodos) {
          try {
            const encryptedBytes = this.decrypt(encryptedTodos);
            const decryptedString = bytes_to_string(encryptedBytes);
            this.todos = JSON.parse(decryptedString);
          } catch (err) {
            return Promise.reject(err);
          }
        }
        else {
          this.todos = [];
        }
      }
    );
  }

src/providers/todo/todo.ts

This function first fetches the data from the storage. When the app is started for the first time the result will be undefined and assigns an empty array to the todos instance variable. If there is data stored the app calls the decrypt function.

  decrypt(buffer: Uint8Array) {
    const parts = this.separateNonceFromData(buffer);
    const decrypted = AES_GCM.decrypt(parts.data, this.aesKey, parts.nonce);
    return decrypted;
  }

src/providers/todo/todo.ts

To decrypt the data the algorithm needs the nonce that was created in the encryption function and prepended to the encrypted data. The function separateNonceFromData separates the nonce and the encrypted data and then AES_GCM.decrypt is called with the encrypted data, the key and the nonce. This function returns the decrypted data as an Uint8Array object. Back in the decryptTodos function the application needs to convert this buffer into a string (bytes_to_string) and then parse it to a JavaScript object.

Encryption

Each time the user creates a new todo entry or updates an existing todo entry the function encryptAndSaveTodos is called.

  encryptAndSaveTodos() {
    const todosString = JSON.stringify(this.todos);
    const encrypted = this.encrypt(string_to_bytes(todosString));
    this.storage.set('todos', encrypted);
  }

src/providers/todo/todo.ts

Here the application first converts the todos array into a JSON string and then encrypts it with a call to the encrypt function.

  encrypt(data: Uint8Array) {
    const nonce = new Uint8Array(this.nonceLen);
    getRandomValues(nonce);

    const encrypted = AES_GCM.encrypt(data, this.aesKey, nonce);
    return this.joinNonceAndData(nonce, new Uint8Array(encrypted));
  }

src/providers/todo/todo.ts

The encrypt function first creates the nonce, an arbitrary number that may only be used once. Then it calls the AES_GCM.encrypt function with the clear text, key and the nonce as parameters. After the encryption the function merges the nonce and the encrypted data into one buffer (joinNonceAndData), because the application needs to provide the same arguments in the decrypt step. Back in the encryptAndSaveTodos function the app stores the concatenated buffer with the Storage service.

Notice! Security is hard and I'm not a cryptographic expert. If there is code or a description concerning the security that is wrong please tell me so I can correct it. Thanks.

You find the entire source code for this app on GitHub.

If you want to learn more about the Web Cryptography API, look in this GitHub repository that hosts examples for all the algorithms and operations: https://github.com/diafygi/webcrypto-examples

The repository also contains a live view where you can see the features of the Web Cryptography API your browser supports: https://diafygi.github.io/webcrypto-examples/