Secure Storage

Ionic Secure Storage is a cross-platform data storage system for high performance, secure data storage on iOS and Android. Ionic Secure Storage provides full SQL query and relational data support through SQLite, as well as key/value support for simpler use cases when used with the Ionic Storage utility library. Full encryption support (using 256-bit AES) is provided out of the box for security sensitive applications.

While Ionic Secure Storage is only available on iOS and Android, support for web storage is available when used in key/value mode in tandem with the Ionic Storage utility library, which will fall back to a web-friendly storage mechanism when running in a non-native browser environment.

Encryption Key Management#

Before you use Secure Storage, you need to figure out how you will manage your encryption key. Typically, managing encryption keys on the client can be incredibly challenging to get right.

Thankfully, by using Identity Vault in tandem with Ionic Secure Storage, teams can securely manage encryption keys to support online and offline use cases using the full security features available on modern mobile devices and operating systems.

If you choose to roll your own method of key management, be aware that, just like rolling your own encryption, there are many pitfalls to client-side key management that may defeat your encryption efforts. Some of those pitfalls include lack of integration with secure enclave device hardware, incorrect management of biometric credentials resulting in users accessing sensitive values from other users, and data exposure if a device is jailbroken or lost/stolen. Because of these challenges we strongly recommend against rolling your own key management system and recommend using Identity Vault instead.

Installation#

If you have not already setup Ionic Enterprise in your app, follow the one-time setup steps.

Next, install the plugin:

npm install @ionic-enterprise/secure-storage
npx cap sync

NOTE: npm 7+ is recommended. For previous versions, install @ionic-native/core as a dependency.

Angular Usage#

In Angular, start by defining the SQLite class as provider in one of your component modules. We recommend your AppModule for usage across your app:

import { SQLite } from '@ionic-enterprise/secure-storage/ngx';
@NgModule({
// ... snip ...
providers: [SQLite],
bootstrap: [AppComponent],
})
export class AppModule {}

Next, import @ionic-enterprise/secure-storage/ngx into your file and inject it into your constructor:

import { SQLite, SQLiteObject } from '@ionic-enterprise/secure-storage/ngx';
class MyComponent {
constructor(private sqlite: SQLite) {}
}

Initializing the database can be done immediately in the constructor:

private database: SQLiteObject;
constructor(private sqlite: SQLite) {
this.initializeDatabase();
}
private async initializeDatabase() {
// Create or open a table
try {
const db = await this.sqlite.create({
name: "mydb",
location: "default",
// Key/Password used to encrypt the database
// Strongly recommended to use Identity Vault to manage this
key: "password"
});
this.database = db;
// Create our initial schema
await db.executeSql('CREATE TABLE IF NOT EXISTS software(name, company, type, version)', [])
} catch (e) {
console.error('Unable to initialize database', e);
}
}

React Usage#

Begin by importing Secure Storage into one of your component source files then create a state variable for the database:

import { SQLite, SQLiteObject, SQLiteTransaction } from '@ionic-enterprise/secure-storage';
import { useEffect, useState } from 'react';
const App: React.FC = () => {
const [db, setDb] = useState<SQLiteObject | null>(null);
}

Next, initialize the database immediately and create the database tables:

useEffect(() => {
async function createDb() {
try {
const newDb = await SQLite.create({
name: 'mydb',
location: 'default',
// Key/Password used to encrypt the database
// Strongly recommended to use Identity Vault to manage this
key: "password"
});
setDb(newDb);
// create tables
await newDb.executeSql('CREATE TABLE IF NOT EXISTS software(name, company, type, version)', [])
} catch (e) {
console.error('Unable to create database', e);
}
}
createDb();
}, []);

Vue Usage#

Begin by importing Secure Storage into one of your components:

import { SQLite, SQLiteObject, SQLiteTransaction } from '@ionic-enterprise/secure-storage';

Next, create a ref object for the database instance, then initialize the database immediately:

export default defineComponent({
name: 'App',
components: {
IonApp,
IonRouterOutlet
},
setup() {
const db = ref<SQLiteObject>();
async function createDb() {
try {
const newDb = await SQLite.create({
name: 'mydb',
location: 'default',
// Key/Password used to encrypt the database
// Strongly recommended to use Identity Vault to manage this
key: "password"
});
db.value = newDb;
console.log('Created database!', newDb);
// create tables
await db.executeSql('CREATE TABLE IF NOT EXISTS software(name, company, type, version)', [])
} catch (e) {
console.error('Unable to create database', e);
}
}
createDb();
}
});

Basic SQL operations#

Insert data into a database table:

this.database.transaction((tx) => {
tx.executeSql("INSERT INTO software (name, company, type, version) VALUES (?,?,?,?)",
[ "secure-storage", "ionic", "native", "2.0"], (tx, result) => {
console.log("insertId: " + result.insertId); // New Id number
console.log("rowsAffected: " + result.rowsAffected); // 1
});
});

Read data from a database table:

this.database.transaction(tx => {
tx.executeSql('SELECT * from software', [], (tx, result) => {
// Rows is an array of results. Use zero-based indexing to access
// each element in the result set: item(0), item(1), etc.
for (let i = 0; i < result.rows.length; i++) {
// { name: "secure-storage", company: "ionic", type: "native", version: "2.0" }
console.log(result.rows.item(i));
// ionic
console.log(result.rows.item(i).company);
}
});
});

Update data:

this.database.transaction(tx => {
tx.executeSql(
'UPDATE software SET version = ? WHERE company = ?',
['2.2', 'ionic'],
(tx, result) => {
console.log('Rows affected: ' + result.rowsAffected); // 1
},
);
});

Delete data:

this.database.transaction(tx => {
tx.executeSql(
'DELETE FROM software WHERE company = ?',
['ionic'],
(tx, result) => {
console.log('Rows affected: ' + result.rowsAffected); // 1
},
);
});

Close the database:

await this.database.close();

Delete the database (provide the same configuration details used when creating it):

await this.sqlite.deleteDatabase({
name: 'images.db',
location: 'default',
key: 'password',
});

Transactions#

Transactions are strongly recommended for performance reasons, especially when using encryption, and are also critical for building atomic operations that only commit to the database if all statements execute correctly.

Single-statement Transactions#

Transactions can be performed one statement at a time using SQLiteTransaction.executeSql:

this.database.transaction(tx => {
tx.executeSql('CREATE TABLE IF NOT EXISTS software (name, company)');
tx.executeSql('INSERT INTO software VALUES (?,?)', ['offline', 'ionic']);
tx.executeSql('INSERT INTO software VALUES (?,?)', ['auth-connect', 'ionic']);
});

Or can be performed in a batch SQLiteObject.sqlBatch:

this.database.sqlBatch([
'CREATE TABLE IF NOT EXISTS software (name, company)',
['INSERT INTO software VALUES (?,?)', ['offline', 'ionic']],
['INSERT INTO software VALUES (?,?)', ['auth-connect', 'ionic']],
]);