Encryption
There are two ways to use drift on encrypted databases.
The encrypted_drift
package is similar to drift_sqflite
and uses a platform plugin written in
Java.
Alternatively, you can use the ffi-based implementation with the sqlcipher_flutter_libs
package.
For new apps, we recommend using sqlcipher_flutter_libs
with a NativeDatabase
from drift.
An example of a Flutter app using the new encryption package is available
here.
Encrypted version of a NativeDatabase
¶
You can also use the new drift/native
library with an encrypted executor.
This allows you to use an encrypted drift database on more platforms, which is particularly
interesting for Desktop applications.
Setup¶
To use sqlcipher
, add a dependency on sqlcipher_flutter_libs
:
If you already have a dependency on sqlite3_flutter_libs
, drop that dependency.
sqlite3_flutter_libs
and sqlcipher_flutter_libs
are not compatible
as they both provide a (different) set of sqlite3
native apis.
On Android, you also need to adapt the opening behavior of the sqlite3
package to use the encrypted library instead
of the regular libsqlite3.so
:
import 'package:sqlite3/open.dart';
import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
// call this method before using drift
Future<void> setupSqlCipher() async {
await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
open.overrideFor(OperatingSystem.android, openCipherOnAndroid);
}
When using drift on a background database, you need to call setupSqlCipher
on the background isolate
as well. With NativeDatabase.createInBackground
, which are using isolates internally, you can use
the setupIsolate
callback to do this - the examples on this page use this as well.
Since applyWorkaroundToOpenSqlCipherOnOldAndroidVersions()
invokes a platform channel, one needs
to install a BackgroundIsolateBinaryMessenger
on the isolate as well.
On iOS, macOS and Windows, no additional setup is necessary - simply depend on sqlcipher_flutter_libs
.
For Linux builds, note that OpenSSL is linked statically by default. If you want to compile your app to use
a dynamically-linked distribution of OpenSSL, see this
issue comment.
Using¶
SQLCipher implements sqlite3's C api, which means that you can continue to use the sqlite3
package
or drift/ffi
without changes. They're both fully compatible with sqlcipher_flutter_libs
.
To actually encrypt a database, you must set an encryption key before using it.
A good place to do that in drift is the setup
parameter of NativeDatabase
, which runs before drift
is using the database in any way:
final token = RootIsolateToken.instance!;
NativeDatabase.createInBackground(
myDatabaseFile,
isolateSetup: () async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
await setupSqlCipher();
},
setup: (rawDb) {
rawDb.execute("PRAGMA key = 'passphrase';");
// Recommended option, not enabled by default on SQLCipher
rawDb.config.doubleQuotedStringLiterals = false;
},
);
Disabling double-quoted string literals
In sqlite3_flutter_libs
, sqlite3 is compiled to only accept single-quoted string literals.
This is a recommended option to avoid confusion - SELECT "column" FROM tbl
is always a
column reference, SELECT 'column'
is always a string literal.
SQLCipher does not disable double-quoted string literals at compile-time. For consistency, it is recommended to manually disable them for databases used with drift.
Important notice¶
On the native side, SQLCipher
and sqlite3
stand in conflict with each other.
If your package depends on both native libraries, the one you will actually get may be undefined on some platforms.
In particular, if you depend on sqlcipher_flutter_libs
and another package you use depends on say sqflite
,
you could still be getting the regular sqlite3
library without support for encryption!
For this reason, it is recommended that you check that the cipher_version
pragma is available at runtime:
bool _debugCheckHasCipher(Database database) {
return database.select('PRAGMA cipher_version;').isNotEmpty;
}
Next, add an assert(_debugCheckHasCipher(database))
before using the database. A suitable place is the
setup
parameter to a NativeDatabase
:
NativeDatabase.createInBackground(
myDatabaseFile,
isolateSetup: () async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
await setupSqlCipher();
},
setup: (rawDb) {
assert(_debugCheckHasCipher(rawDb));
rawDb.execute("PRAGMA key = 'passphrase';");
// Recommended option, not enabled by default on SQLCipher
rawDb.config.doubleQuotedStringLiterals = false;
},
);
If this check reveals that the encrypted variant is not available, please see the documentation here for advice.
Encrypting existing databases¶
If you have an existing database which you now want to encrypt, there are a few steps to consider.
First, replace your dependencies on drift_flutter
or sqlite3_flutter_libs
with
sqlcipher_flutter_libs
as shown in the setup.
Note however that you can't just apply the pragma key =
statement on existing databases!
To migrate existing databases to encryption, SQLCipher recommends these steps:
- Opening your existing database.
- Attaching a new encrypted-variant.
- Calling the
sqcipher_export
function to copy the unencrypted database into the encrypted file. - Closing and deleting the unencrypted database.
In drift, you can run these steps in the isolateSetup
callback when opening a NativeDatabase
:
final existingDatabasePath = '/path/to/your/database.db';
final encryptedDatabasePath = '/path/to/your/encrypted.db';
const yourKey = 'passphrase';
String escapeString(String source) {
return source.replaceAll('\'', '\'\'');
}
// This database can be passed to the constructor of your database class
NativeDatabase.createInBackground(
File(encryptedDatabasePath),
isolateSetup: () async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
await setupSqlCipher();
final existing = File(existingDatabasePath);
final encrypted = File(encryptedDatabasePath);
if (await existing.exists() && !await encrypted.exists()) {
// We have an existing database to migrate.
final plaintextDb = sqlite3.open(existingDatabasePath)
..execute("ATTACH DATABASE '${escapeString(encryptedDatabasePath)}' "
"AS encrypted KEY '${escapeString(yourKey)}';")
..execute("SELECT sqlcipher_export('encrypted');");
// sqlcipher_export doesn't apply the user_version pragma used by drift
// to implement migrations. The version of the encrypted database must
// match the previous state.
final userVersion =
plaintextDb.select('PRAGMA user_version;').first.columnAt(0) as int;
plaintextDb
..execute('PRAGMA encrypted.user_version = $userVersion;')
..execute('DETACH DATABASE encrypted;')
..dispose();
// This should have created the encrypted database.
assert(await encrypted.exists());
await existing.delete();
}
},
setup: (rawDb) {
assert(_debugCheckHasCipher(rawDb));
rawDb.execute("PRAGMA key = '${escapeString(yourKey)}';");
// Recommended option, not enabled by default on SQLCipher
rawDb.config.doubleQuotedStringLiterals = false;
},
);
Using encrypted_drift
¶
In addition to the dart:ffi
-based encrypted executor based on package:sqlite3
, we also provide
a version of drift that uses the sqflite_sqlcipher library
by @davidmartos96.
To use it, you need to remove a dependency on drift_sqflite
you might have in your pubspec.yaml
and replace it with this:
dependencies:
drift: ^2.26.0
encrypted_drift:
git:
url: https://github.com/simolus3/drift.git
path: extras/encryption
Instead of importing package:drift_sqflite/drift_sqflite.dart
(or package:drift/native.dart
) in your apps,
you would then import both package:drift/drift.dart
and package:encrypted_drift/encrypted_drift.dart
.
Finally, you can replace SqfliteQueryExecutor
(or a NativeDatabase
) with an EncryptedExecutor
.
Extra setup on Android and iOS¶
Some extra steps may have to be taken in your project so that SQLCipher works correctly. For example, the ProGuard configuration on Android for apps built for release.
Read instructions (Usage and installation instructions of the package can be ignored, as that is handled internally by encrypted_drift
)