Skip to content

Instantly share code, notes, and snippets.

@simolus3
Last active January 15, 2021 09:08
Show Gist options
  • Save simolus3/5097bbd80ce59f9b957961fe851fd95a to your computer and use it in GitHub Desktop.
Save simolus3/5097bbd80ce59f9b957961fe851fd95a to your computer and use it in GitHub Desktop.
Template to use moor_ffi with SQLCipher
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:math';
import 'package:moor/backends.dart';
import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:moor_ffi/open_helper.dart';
/// Tells `moor_ffi` to use `sqlcipher` instead of the regular `sqlite3`.
///
/// This needs to be called before using `moor`, for instance in the `main`
/// method.
void init() {
const sharedLibraryName = 'libsqlcipher.so';
open.overrideFor(OperatingSystem.android, () {
try {
return DynamicLibrary.open(sharedLibraryName);
} catch (_) {
// On some (especially old) Android devices, we somehow can't dlopen
// libraries shipped with the apk. We need to find the full path of the
// library (/data/data/<id>/lib/libsqlite3.so) and open that one.
// For details, see https://github.com/simolus3/moor/issues/420
final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync();
// app id ends with the first \0 character in here.
final endOfAppId = max(appIdAsBytes.indexOf(0), 0);
final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId));
return DynamicLibrary.open('/data/data/$appId/lib/$sharedLibraryName');
}
});
open.overrideFor(OperatingSystem.iOS, () => DynamicLibrary.executable());
}
class VmDatabaseEncrypted extends DelegatedDatabase {
/// Creates a database that will store its result in the [file], creating it
/// if it doesn't exist.
factory VmDatabaseEncrypted(
File file, {
String password = '',
bool logStatements = false,
}) {
final vmDatabase = VmDatabase(file, logStatements: logStatements);
return VmDatabaseEncrypted._(vmDatabase, password);
}
factory VmDatabaseEncrypted.memory({
String password = '',
bool logStatements = false,
}) {
final vmDatabase = VmDatabase.memory(logStatements: logStatements);
return VmDatabaseEncrypted._(vmDatabase, password);
}
VmDatabaseEncrypted._(
VmDatabase vmDatabase,
String password,
) : super(
_VmEncryptedDelegate(vmDatabase.delegate, password),
logStatements: vmDatabase.logStatements,
isSequential: vmDatabase.isSequential,
);
}
class _VmEncryptedDelegate extends DatabaseDelegate {
final String password;
final DatabaseDelegate delegate;
_VmEncryptedDelegate(
this.delegate,
this.password,
);
@override
Future<void> open(QueryExecutorUser db) async {
await delegate.open(db);
final keyLiteral = const StringType().mapToSqlConstant(password);
await delegate.runCustom('PRAGMA KEY = $keyLiteral', const []);
return Future.value();
}
@override
FutureOr<bool> get isOpen => delegate.isOpen;
@override
Future<void> runBatched(BatchedStatements statements) {
return delegate.runBatched(statements);
}
@override
Future<void> runCustom(String statement, List args) {
return delegate.runCustom(statement, args);
}
@override
Future<int> runInsert(String statement, List args) {
return delegate.runInsert(statement, args);
}
@override
Future<QueryResult> runSelect(String statement, List args) {
return delegate.runSelect(statement, args);
}
@override
Future<int> runUpdate(String statement, List args) {
return delegate.runUpdate(statement, args);
}
@override
TransactionDelegate get transactionDelegate => delegate.transactionDelegate;
@override
DbVersionDelegate get versionDelegate => delegate.versionDelegate;
}

Call init before using moor, for instance in your main method.

You can now use an encrypted database by replacing VmDatabase(file) with EncryptedVmDatabase(key, file)

@davidmartos96
Copy link

Nice Gist Simon!
What do you think about changing the implementation of EncryptedVmDatabase to something like this? With the following there is no need to repeat all the internal logic in the delegate methods in case something changes in the future. This is what I use in a project that uses moor_ffi and SQLCipher.

class VmDatabaseEncrypted extends DelegatedDatabase {
  /// Creates a database that will store its result in the [file], creating it
  /// if it doesn't exist.
  factory VmDatabaseEncrypted(
    File file, {
    String password = '',
    bool logStatements = false,
  }) {
    final vmDatabase = VmDatabase(file, logStatements: logStatements);
    return VmDatabaseEncrypted._(vmDatabase, password);
  }

  factory VmDatabaseEncrypted.memory({
    String password = '',
    bool logStatements = false,
  }) {
    final vmDatabase = VmDatabase.memory(logStatements: logStatements);
    return VmDatabaseEncrypted._(vmDatabase, password);
  }

  VmDatabaseEncrypted._(
    VmDatabase vmDatabase,
    this.password,
  ) : super(
          _VmEncryptedDelegate(vmDatabase.delegate, password),
          logStatements: vmDatabase.logStatements,
          isSequential: vmDatabase.isSequential,
        );

  final String password;
}

class _VmEncryptedDelegate extends DatabaseDelegate {
  final String password;
  final DatabaseDelegate delegate;

  _VmEncryptedDelegate(
    this.delegate,
    this.password,
  );

  @override
  Future<void> open(QueryExecutorUser db) async {
    await delegate.open(db);
    await delegate.runCustom('PRAGMA KEY = "$password"', <dynamic>[]);
    return Future.value();
  }

  @override
  FutureOr<bool> get isOpen => delegate.isOpen;

  @override
  Future<void> runBatched(List<BatchedStatement> statements) {
    return delegate.runBatched(statements);
  }

  @override
  Future<void> runCustom(String statement, List args) {
    return delegate.runCustom(statement, args);
  }

  @override
  Future<int> runInsert(String statement, List args) {
    return delegate.runInsert(statement, args);
  }

  @override
  Future<QueryResult> runSelect(String statement, List args) {
    return delegate.runSelect(statement, args);
  }

  @override
  Future<int> runUpdate(String statement, List args) {
    return runUpdate(statement, args);
  }

  @override
  TransactionDelegate get transactionDelegate => delegate.transactionDelegate;

  @override
  DbVersionDelegate get versionDelegate => delegate.versionDelegate;
}

@simolus3
Copy link
Author

simolus3 commented Jul 5, 2020

Good point, thank you David! I've changed the class.

@davidmartos96
Copy link

Oops, I have just spotted a small bug. The runUpdate method does not call the delegate... 😓

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment