persistence incompatible with sqlcipher
Which packages are you using?
stream_chat_persistance
On what platforms did you experience the issue?
iOS, Android
What version are you using?
latest
What happened?
We use SQLCipher to encrypt our local SQLite instance.
Because persistence depends on sqlite3_flutter_libs - you can't include this in any app that uses sqlcipher_flutter_libs.
I think the way to fix this is to split out a package that doesn't directly include that package, and allow consumers to provider either sqlite3_flutter_libs or sqlcipher_flutter_libs.
Steps to reproduce
Not relevant
Supporting info to reproduce
- Create a package that depends on
sqlcipher_flutter_libs - add
stream_chat_persistencelib - Attempt build
Relevant log output
Not relevant
Flutter analyze output
Not relevant
Flutter doctor output
Not relevant
Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Few things that are required to get this to work - although also supporting an encrypted database would be preferable too.
- Remove the direct dependency on
sqlite3_flutter_libsand direct consumers to pull in their own libs (eithersqlite3_flutter_libsorsqlciper_flutter_libs. - Instruct consumers to properly setup overrides for openers. SEE: https://github.com/simolus3/drift/blob/9c28a060206ffb991e30009a8e4fb9137d572051/examples/encryption/lib/main.dart#L11-L16)
- allow for injection of code into the "background" isolate - so that we can override in that isolate as well. Could be as simple as letting us provide our own top level isolate function, replacing the default: https://github.com/GetStream/stream-chat-flutter/blob/344a10d8a289ec1b49402e811330c4e07ba06bf6/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart#L66
I updated a local fork to see if I could get things working, and with the above changes I can get it working with sqlciper and both connection modes.
I successfully migrated getsream to use sqlcipher in my mobile project without fork. Here are steps you need to do:
- Create override for
sqlite3_flutter_libspackage. Create new fileoverrides/sqlite3_override/pubspec.yamlfile wtih following contents
name: sqlite3_flutter_libs
description: "Override that re-exports sqlcipher_flutter_libs instead of the normal sqlite3."
version: 1.0.0
# We depend on sqlcipher_flutter_libs here, so any package depending on
# sqlite3_flutter_libs will effectively get sqlcipher_flutter_libs.
dependencies:
sqlcipher_flutter_libs: ^0.6.6 # or whatever the latest version is
- In your regular pubspec.yaml add:
dependency_overrides:
sqlite3_flutter_libs:
path: ./overrides/sqlite3_override
- Update Podfile as per tutorial https://www.zetetic.net/sqlcipher/ios-tutorial/
post_install do |installer|
installer.aggregate_targets.each do |aggregate_target|
aggregate_target.xcconfigs.each do |config_name, xcconfig|
libraries = xcconfig.other_linker_flags[:libraries]
if libraries.include?('sqlite3')
puts "Found sqlite3 in #{aggregate_target.name}: #{config_name}"
puts " Other linker libraries: [#{libraries.map(&:inspect).join(', ')}]"
modified_libraries = libraries.subtract(['sqlite3'])
puts " Other linker libraries modified: [#{modified_libraries.map(&:inspect).join(', ')}] "
xcconfig.other_linker_flags[:libraries] = modified_libraries
xcconfig_path = aggregate_target.xcconfig_path(config_name)
xcconfig.save_as(xcconfig_path)
end
end
end
end
- Create custom persistence client by extending StreamChatPersistenceClient
// ignore: implementation_imports
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
/// Custom SQLCipher‐backed persistence client
class CustomChatPersistenceClient extends StreamChatPersistenceClient {
CustomChatPersistenceClient({
required String passphrase,
super.logLevel,
super.webUseExperimentalIndexedDb,
super.logHandlerFunction,
}) : _passphrase = passphrase,
super(connectionMode: ConnectionMode.background);
final String _passphrase;
@override
Future<void> connect(
String userId, {
DatabaseProvider? databaseProvider, // Used only for testing
}) => super.connect(
userId,
databaseProvider: (userId, mode) => _databaseProvider(userId),
);
DriftChatDatabase _databaseProvider(String userId) {
return DriftChatDatabase(
userId,
openDatabaseConnection(passphrase: _passphrase, fileName: 'db_$userId'),
);
}
}
- Next your openDatabaseConnection should be something like this (
_setupSqlCipherincludes workarounds needed for Android):
LazyDatabase openDatabaseConnection({
required String passphrase,
required String fileName,
}) {
return LazyDatabase(() async {
final docFolder = await getApplicationDocumentsDirectory();
final file = File(docFolder.uri.resolve(fileName).path);
final token = RootIsolateToken.instance;
if (token == null) {
throw StateError('Token must be obtained from the root isolate');
}
return NativeDatabase.createInBackground(
file,
isolateSetup: () async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
await _setupSqlCipher();
},
setup: (rawDb) {
assert(_debugCheckHasCipher(rawDb), 'sql cipher is not enabled');
rawDb.execute("PRAGMA key = '$passphrase';");
// Recommended option, not enabled by default on SQLCipher
rawDb.config.doubleQuotedStringLiterals = false;
// Enable foreign key constraints
rawDb.execute('PRAGMA foreign_keys = ON;');
},
);
});
}
Future<void> _setupSqlCipher() async {
await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
open.overrideFor(OperatingSystem.android, openCipherOnAndroid);
}
bool _debugCheckHasCipher(Database database) {
return database.select('PRAGMA cipher_version;').isNotEmpty;
}
A few important notes:
- Example above does not take into account web/desktop. This is mobile only setup
- Setup is for background connection mode
Hey @dballance , can you try the above steps mentioned by @shorbenko ?
@xsahil03x - While I'm certainly aware that we can override using the above, it seems incongruent with the intent of the package (drop-in).
With a few small tweaks, this could be directly supported and not involve workarounds / forks that will eventually become stale or require fixes as the package changes.
The limited subset of changes made locally meant I could run the below sample code from the docs, without changes.
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.INFO,
connectionMode: ConnectionMode.background,
);
final client = StreamChatClient(
apiKey ?? kDefaultStreamApiKey,
logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient;
This is certainly more palatable than having to override build inclusions, write a custom wrapper, etc.
I have further questions about the database here (specifically, if it's compatible with the Swift package schema, can we change the path for the database file without a custom wrapper, whether encryption will be supported first party, etc) but some of that is moot if I'm going to have to maintain a fork myself to support using the persistence client.