SQLite.Encrypted for Android

SQLite.Encrypted for Android follows the SQLite Android API and packages the SEE native libraries for Android API 21 and newer.

Core rule: Call SQLiteDatabase.activateSEE() once for the process before opening a database with an encryption key.

Codec support: SQLite.Encrypted currently supports only aes256ofb. Omit the codec or pass null to use aes256ofb; any codec argument supplied to key() or rekey() must be aes256ofb. Keyed opens use the same aes256ofb default.

Install

The Maven coordinate is org.sqlite.encrypted:sqlite3. Add the SQLite.Encrypted repository and the dependency, replacing <version> with the release you use. Maven Central remains available for ordinary dependencies.

repositories {
    google()
    mavenCentral()
    maven {
        url = uri("https://packages.sourcegear.com/maven/releases")
    }
}

dependencies {
    implementation("org.sqlite.encrypted:sqlite3:<version>")
}

Open an encrypted database

The binding supports a keyed open, an explicit key() call, and the PRAGMA compatibility flow. Prefer the keyed open: it retains the key in the database configuration, applies it through the native key() implementation to every connection in the pool, and reports a failed key operation.

1. Open with a key

Load the JNI bridge, activate SEE, and pass the database key to the keyed openOrCreateDatabase() overload. The binding applies the key and validates the schema before returning each native connection.

import java.io.File;
import org.sqlite.database.sqlite.SQLiteDatabase;

System.loadLibrary("sqliteX");
SQLiteDatabase.activateSEE(activationKey);

File file = context.getDatabasePath("application.db");
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(
    file, null, databaseKey);

Pass a normal path to an unkeyed overload to open a plaintext database.

2. Open with API calls

Open the connection and call key() immediately, before reading the schema or running any other SQL. Reading the schema verifies that the supplied key can decrypt an existing database.

import java.io.File;
import org.sqlite.database.sqlite.SQLiteDatabase;

System.loadLibrary("sqliteX");
SQLiteDatabase.activateSEE(activationKey);

File file = context.getDatabasePath("application.db");
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(file, null);
try {
    db.key(null, databaseKey, null);
    db.rawQuery("SELECT count(*) FROM sqlite_schema", null).close();
} catch (RuntimeException error) {
    db.close();
    throw error;
}

This is most useful when the application directly owns a single connection. Use the keyed open overload when connection pooling or WAL may be enabled.

3. Open with PRAGMA

The connection-level PRAGMA flow is available for applications migrating from an existing integration. Run both statements before any schema access and verify that the key PRAGMA returns ok. Escape apostrophes in the database key before placing it in SQL text.

import java.io.File;
import android.database.Cursor;
import org.sqlite.database.sqlite.SQLiteDatabase;
import org.sqlite.database.sqlite.SQLiteException;

System.loadLibrary("sqliteX");

File file = context.getDatabasePath("application.db");
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(file, null);
try {
    String escapedDatabaseKey = databaseKey.replace("'", "''");
    db.execSQL(
        "PRAGMA activate_extensions = 'see-" + activationKey + "'"
    );
    try (Cursor result = db.rawQuery(
            "PRAGMA key = '" + escapedDatabaseKey + "'", null)) {
        if (!result.moveToFirst() || !"ok".equals(result.getString(0))) {
            throw new SQLiteException("SEE did not accept the database key");
        }
    }
    db.rawQuery("SELECT count(*) FROM sqlite_schema", null).close();
} catch (RuntimeException error) {
    db.close();
    throw error;
}

On a build without encryption support, or when SEE cannot load the key, the key PRAGMA returns no row. If the result is ignored, the application can continue with a connection whose encryption key was never applied.

Query and update

The ordinary Android SQLite API is unchanged.

db.execSQL(
    "CREATE TABLE IF NOT EXISTS notes "
        + "(id INTEGER PRIMARY KEY, body TEXT NOT NULL)"
);
db.execSQL("INSERT INTO notes(body) VALUES (?)", new Object[] {"hello"});

try (Cursor rows = db.rawQuery(
        "SELECT id, body FROM notes ORDER BY id", null)) {
    while (rows.moveToNext()) {
        long id = rows.getLong(0);
        String body = rows.getString(1);
    }
}

Apply or rotate a key on an open connection

Text and binary overloads preserve SEE text-key and exact-byte semantics. Pass null for the database name and codec to use the main database and the aes256ofb default.

db.key(null, "initial text key", null);
db.rekey(null, "replacement text key", null);

byte[] binaryKey = new byte[] {0x01, 0x00, 0x7f, (byte) 0xff};
db.rekey(null, binaryKey, null);

A null key removes encryption. Cast a Java null literal because it is otherwise ambiguous between the String and byte[] overloads.

db.rekey(null, (String) null, null);

If the codec is supplied explicitly, the only supported value is aes256ofb.

db.rekey("main", "replacement key", "aes256ofb");

Use with Room

Room does not select this driver automatically. Supply a custom SupportSQLiteOpenHelper.Factory backed by this binding. Its SQLiteOpenHelper delegate receives the key so every pooled connection calls the native key() implementation.

SQLiteDatabase.activateSEE(activationKey);

AppDatabase database = Room.databaseBuilder(
        context, AppDatabase.class, "application.db")
    .openHelperFactory(new SeeOpenHelperFactory(databaseKey))
    .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
    .build();

In this example, SeeOpenHelperFactory is the application's custom adapter. The key-applying portion passes the key to the binding's SQLiteOpenHelper constructor. The remaining adapter code forwards the Room lifecycle and database APIs to that delegate.

private SQLiteOpenHelper createDelegate(
        SupportSQLiteOpenHelper.Configuration configuration,
        String databaseKey) {
    return new SQLiteOpenHelper(
            configuration.context,
            configuration.name,
            null,
            configuration.callback.version,
            databaseKey,
            null) {
        // Forward onConfigure(), onCreate(), onUpgrade(), onDowngrade(),
        // and onOpen() to configuration.callback.
    };
}

The helper retains the key in the connection-pool configuration. Initial and later reader connections apply the key before any schema access. A failed key call or an unreadable schema aborts that connection's open.