Home | Send Feedback

Secure Todo app with Ionic

Published: 18. February 2017  •  Updated: 6. December 2018  •  ionic, 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 are going to write one of these ubiquitous Todo applications with Ionic. The app stores the data in localStorage on the client, but not in clear text; instead, it encrypts the data before storing 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 at caniuse.com, the browser support looks very promising.

Unfortunately, at the time of writing, the app did not run on Safari. 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.

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 supports 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 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


The application is based on the Ionic blank starter template, and I added the asmcrypto library to the project.

npm install asmcrypto.js

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

Derive Key

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

  setPassword(password: string): void {
    const value = localStorage.getItem('lastTodoId');
    if (value) {
      this.lastId = JSON.parse(value);
    } else {
      this.lastId = 0;
    }

    this.deriveAesKey(password);
    return this.decryptTodos();
  }

todo.service.ts

It first loads the last used id from localStorage. Every time the user creates a new todo object, the application increases this id by one and assigns it to the id field of the todo object.
Next, the method calls the deriveAesKey function that creates the AES key.

  deriveAesKey(password: string): void {
    this.aesKey = Pbkdf2HmacSha256(string_to_bytes(password), this.salt, this.iterations, 32);
  }

todo.service.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(): void {
    const binaryString = localStorage.getItem('todos');
    if (binaryString) {
      const encryptedTodos = string_to_bytes(binaryString);
      const encryptedBytes = this.decrypt(encryptedTodos);
      const decryptedString = bytes_to_string(encryptedBytes);
      this.todos = new Map(JSON.parse(decryptedString));
    } else {
      this.todos = new Map();
    }
  }

todo.service.ts

This method fetches the data from storage. When the app is started for the first time, the result is undefined, and the method assigns an empty new Map instance to the todos instance variable. If there is data stored in localStorage, the app decrypts it with a call to the decrypt method.

  decrypt(buffer: Uint8Array): Uint8Array {
    if (this.aesKey === null) {
      throw new Error('aes key not set');
    }

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

todo.service.ts

To decrypt the data, the algorithm needs the nonce, that was created during the encryption process and prepended to the encrypted data. The method separateNonceFromData splits the stored blob into the nonce and the encrypted data. The decrypt method then calls the AES_GCM.decrypt method from asmcrypto to decrypt the encrypted data. This method returns the decrypted data as an Uint8Array object. Back in the decryptTodos method, the application needs to convert this buffer into a string (bytes_to_string) and then parses it to a JavaScript object.

Encryption

Each time the user creates or updates a todo entry, the method encryptAndSaveTodos is called.

  encryptAndSaveTodos(): void {
    if (this.todos) {
      const todosString = JSON.stringify([...this.todos]);
      const encrypted = this.encrypt(string_to_bytes(todosString));
      localStorage.setItem('todos', bytes_to_string(encrypted));
    }
  }

todo.service.ts

This method first converts the todos Map into a JSON string and then encrypts it with a call to encrypt.

  encrypt(data: Uint8Array): Uint8Array {
    if (this.aesKey === null) {
      throw new Error('aes key not set');
    }

    const nonce = new Uint8Array(this.nonceLen);
    this.getRandomValues(nonce);

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

todo.service.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 plain 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 decrypting step. Back in the encryptAndSaveTodos method, the application stores the encrypted blob into localStorage.


Notice! Security is hard, and I'm not a cryptographic expert. If there is code or a description 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, take a look at 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/