Skip to main content

Secure Storage - Encryption Key Management with Identity Vault

Encryption key management is critical when building secure apps, as encryption is only as good as the management of the encryption key.

Managing encryption keys client-side is an incredibly complicated and risky task, as apps will need to persist and use an encryption key client-side to enable secure offline apps.

Historically, secure client-side key management was impossible. However, thanks to advances in mobile device hardware and APIs, there is a way to securely manage sensitive values such as encryption keys on the client, and the companion Ionic Identity Vault product was built to correctly and securely implement those capabilities.

Managing Encryption Keys with Identity Vault

Ionic Identity Vault is a powerful solution that provides a cross-platform layer on top of modern mobile security hardware and biometric authentication. Sensitive values, such as encryption keys or auth tokens, can be stored securely on-device or at rest in specialty hardware developed by Apple and vendors in the Android ecosystem.

When used in tandem with Ionic Identity Vault, developers can safely store and management their encryption key on device, and enable biometric authentication to secure sensitive data against theft, loss, or jailbreaking.

In practice, once the app user authenticates, the app obtains an encryption key following one of several strategies. One strategy is to auto-generate a unique encryption key on demand, tied to the authenticated user. Another is to retrieve it from a server backend through an API call. Next, store the encryption key securely using Identity Vault. Finally, configure Secure Storage with the encryption key to encrypt all data.

Installation

To get started, follow the Identity Vault installation instructions.

Obtain an Encryption Key

There are several approaches to obtain an encryption key. It's recommended to use a Service that encapsulates all key retrieval logic.

Autogenerated

Auto-generate a unique encryption key on demand, tied to the authenticated user. Note that the key is not retrievable if it is lost.

Angular

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class KeyService {
// Generate an encryption key on the fly unique to the current app user
// Reference: https://stackoverflow.com/a/2117523/180424
get(): string {
// generate a UUID v4
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}

React, Vue, Vanilla TypeScript

With other frameworks, you may opt to create a simple utility function that you store in a key-utils.ts (or similar) module.

export const generateKey = (): string => 
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});

Retrieve from a Server

Retrieve the key from a server backend using an API call and your tooling of choice. The key may be retrievable depending on the backend configuration.

Store the Key in Identity Vault

Secure the encryption key on-device using Ionic Identity Vault. It's recommended to create a Service that encapsulates all key storage logic. For a vault that stores keys rather than authentication information, it is often most appropriate to use a SecureStorage vault. A minimal implementation contains a method to initialize the vault and a method to get the key, generating it if need be.

For complete details on creating vault services and properly integrating them into your application, please refer to the Identity Vault documentation.

Angular

import { Injectable, inject } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { KeyService } from './key.service';

@Injectable({
providedIn: 'root'
})
export class KeyVaultService {
private vault: Vault | BrowserVault;
private keyService = inject(KeyService);

constructor() {
this.vault = Capacitor.isNativePlatform() ? new Vault() : new BrowserVault();
}

async initialize(): Promise<void> {
await this.vault.initialize({
key: 'io.ionic.csdemosecurestoragekeys',
type: VaultType.SecureStorage,
deviceSecurityType: DeviceSecurityType.None,
unlockVaultOnLoad: false,
});

async getEncryptionKey(): Promise<string> {
let key = await this.vault.getValue('encryption-key');
if (!key) {
key = await this.keyService.get();
await this.valut.setValue('encryption-key', key);
}
return key;
}
}

React, Vue, Vanilla TypeScript

For React, Vue, or Vanilla TypeScript applications similar utility functions would be created and used. For example:

import { Injectable, inject } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { generateKey } from './key-utils';

const vault: Vault | BrowserVault = Capacitor.isNativePlatform() ? new Vault() : new BrowserVault();

export const initialize = async () Promise<void> => {
await vault.initialize({
key: 'io.ionic.csdemosecurestoragekeys',
type: VaultType.SecureStorage,
deviceSecurityType: DeviceSecurityType.None,
unlockVaultOnLoad: false,
});
}

export const getEncryptionKey = async (): Promise<string> => {
let key = await vault.getValue('encryption-key');
if (!key) {
key = generateKey();
await valut.setValue('encryption-key', key);
}
return key;
}

Configure Ionic Secure Storage with the Encryption Key

Configure Secure Storage with the newly generated encryption key. It's recommended to create a Service that encapsulates all storage logic. Begin by injecting the KeyVaultService created above then call an initialization function in the constructor. Within the init function, obtain the encryption key from the KeyVaultService, then set it to the key property in a call to sqlite.create().

Angular

import { Injectable } from '@angular/core';
import { SQLite, SQLiteObject } from '@ionic-enterprise/secure-storage/ngx';
import { KeyVaultService } from './key-vault.service';

@Injectable({
providedIn: 'root'
})
export class StorageService {
private _database: SQLiteObject | null = null;

constructor(private sqlite: SQLite, private keyVaultService: KeyVaultService) {
this.init();
}

async init() {
const key = await this.keyVaultService.getEncryptionKey();

try {
this._database = await this.sqlite.create({
name: "my_database.db",
location: "default",
key
});

// Create database tables, other setup tasks, etc.
} catch (e) {
console.error('Unable to initialize database', e);
}
}
}

React, Vue, Vanilla JavaScript

It is more natural to use functions rather than classes in these environments. For example:

import { getEncryptionKey } from './key-vault';
import { SQLite, SQLiteObject } from '@ionic-enterprise/secure-storage';
import { Capacitor } from '@capacitor/core';

let handle: SQLiteObject | null = null;

const openDatabase = async (): Promise<SQLiteObject | null> => {
if (Capacitor.isNativePlatform()) {
const key = await getEncryptionKey();
if (key) {
return SQLite.create({
name: "my_database.db",
location: 'default',
key,
});
}
}
return null;
};

export const getHandle = async (): Promise<SQLiteObject | null> => {
if (!handle) {
handle = await openDatabase();
}
return handle;
};

Now that Secure Storage has been configured with an encryption key, all data will be encrypted at-rest.