stream-chat-flutter icon indicating copy to clipboard operation
stream-chat-flutter copied to clipboard

persistence incompatible with sqlcipher

Open dballance opened this issue 7 months ago • 4 comments

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

  1. Create a package that depends on sqlcipher_flutter_libs
  2. add stream_chat_persistence lib
  3. 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

dballance avatar Jun 25 '25 14:06 dballance

Few things that are required to get this to work - although also supporting an encrypted database would be preferable too.

  1. Remove the direct dependency on sqlite3_flutter_libs and direct consumers to pull in their own libs (either sqlite3_flutter_libs or sqlciper_flutter_libs.
  2. Instruct consumers to properly setup overrides for openers. SEE: https://github.com/simolus3/drift/blob/9c28a060206ffb991e30009a8e4fb9137d572051/examples/encryption/lib/main.dart#L11-L16)
  3. 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.

dballance avatar Jun 25 '25 16:06 dballance

I successfully migrated getsream to use sqlcipher in my mobile project without fork. Here are steps you need to do:

  1. Create override for sqlite3_flutter_libs package. Create new file overrides/sqlite3_override/pubspec.yaml file 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
  1. In your regular pubspec.yaml add:
dependency_overrides:
  sqlite3_flutter_libs:
    path: ./overrides/sqlite3_override
  1. 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
  1. 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'),
    );
  }
}
  1. Next your openDatabaseConnection should be something like this (_setupSqlCipher includes 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:

  1. Example above does not take into account web/desktop. This is mobile only setup
  2. Setup is for background connection mode

shorbenko avatar Jun 26 '25 06:06 shorbenko

Hey @dballance , can you try the above steps mentioned by @shorbenko ?

xsahil03x avatar Jun 27 '25 09:06 xsahil03x

@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.

dballance avatar Jun 29 '25 02:06 dballance