Home | Send Feedback | Share on Bluesky |

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 those ubiquitous Todo applications with Ionic. The app stores data in localStorage on the client, encrypting it before storage to ensure security.

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 is 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 issue is that Safari lacks 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 browsers support it and implement a similar set of algorithms. In this blog post, you can 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 user sees is the Password (html / ts) page. On this page, the user enters their 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 their 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 bits, 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 application 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, which 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 a 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/