From 2a045db60784d83dfad6afdb62eb2a1bd0e084ab Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 15:54:55 +0200 Subject: [PATCH 1/4] Restore SQLCipher support --- .github/workflows/check.yml | 10 +++- packages/powersync/CHANGELOG.md | 4 ++ .../lib/src/database/encryption_options.dart | 47 +++++++++++++++++-- .../native/native_open_factory.dart | 17 +++++-- packages/powersync/lib/src/version.dart | 2 +- packages/powersync/pubspec.yaml | 4 +- pubspec.lock | 4 +- tool/enable_encryption.dart | 6 ++- 8 files changed, 77 insertions(+), 17 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5b641892..f3c6f7e0 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -83,8 +83,14 @@ jobs: working-directory: packages/powersync run: dart test -p chrome,vm - - name: Enable encryption - run: dart tool/enable_encryption.dart + - name: Enable SQLite3 Multiple Ciphers + run: dart tool/enable_encryption.dart sqlite3mc + - name: Encryption tests + working-directory: packages/powersync + run: dart test -p vm -P encryption + + - name: Enable SQLCipher + run: dart tool/enable_encryption.dart sqlcipher - name: Encryption tests working-directory: packages/powersync diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index df4e9646..16125a36 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.1 (unreleased) + +- Restore SQLCipher support on native platforms. + ## 2.3.0 - Add `SyncOptions.httpClient` to `SyncOptions`. It can be set to make PowerSync use a custom HTTP client when connecting to the PowerSync Service. diff --git a/packages/powersync/lib/src/database/encryption_options.dart b/packages/powersync/lib/src/database/encryption_options.dart index 378af7b5..64cdbfdf 100644 --- a/packages/powersync/lib/src/database/encryption_options.dart +++ b/packages/powersync/lib/src/database/encryption_options.dart @@ -16,14 +16,15 @@ const _isWeb = _isCompilingToJavaScript || _isDart2Wasm; /// /// On native platforms, the `sqlite3` package provides a copy of SQLite with /// your app. To use encryption, we need to replace SQLite with -/// [SQLite3MultipleCiphers](https://utelle.github.io/SQLite3MultipleCiphers/). +/// [SQLite3MultipleCiphers](https://utelle.github.io/SQLite3MultipleCiphers/) +/// or [SQLCipher](https://www.zetetic.net/sqlcipher/). /// To enable that, add this to your `pubspec.yaml`: /// /// ```yaml /// hooks: /// user_defines: /// sqlite3: -/// source: sqlite3mc +/// source: sqlite3mc # or sqlcipher /// ``` /// /// If you're using pub workspaces, this needs to be added to the `pubspec.yaml` @@ -38,6 +39,8 @@ const _isWeb = _isCompilingToJavaScript || _isDart2Wasm; /// To use encryption, download `sqlite3mc.wasm` as `web/sqlite3.wasm`. If you /// use the `powersync:setup_web` tool to download that file, pass the /// `--encryption` option. +/// +/// Note that SQLCipher is not available on the web. final class EncryptionOptions { /// The key used to encrypt the database file. /// @@ -59,8 +62,12 @@ final class EncryptionOptions { this.sqlcipherCompatibility = !_isWeb, }); - Iterable? pragmaStatements() sync* { - if (sqlcipherCompatibility) { + Iterable pragmaStatements({ + EncryptedSqliteVariant variant = + EncryptedSqliteVariant.sqlite3MultipleCiphers, + }) sync* { + if (sqlcipherCompatibility && + variant == EncryptedSqliteVariant.sqlite3MultipleCiphers) { yield "PRAGMA cipher = 'sqlcipher'"; yield 'PRAGMA legacy = 4'; } @@ -71,6 +78,7 @@ final class EncryptionOptions { /// Throws if the `cipher` pragma doesn't exist, as that indicates that /// SQLite3MultipleCiphers is not available. + @Deprecated('Unused in PowerSync SDK') static void checkHasCipherPragma(CommonDatabase database) { if (database.select('pragma cipher').isEmpty) { throw UnsupportedError( @@ -80,3 +88,34 @@ final class EncryptionOptions { } } } + +/// A fork of SQLite with encryption support. +enum EncryptedSqliteVariant { + /// [SQLCipher](https://www.zetetic.net/sqlcipher/) can encrypt databases with + /// AES. + /// + /// Encrypting databases with SQLCipher can be more performant than + /// [sqlite3MultipleCiphers] because it uses optimized system encryption + /// libraries (on Apple platform) and OpenSSL (on other platforms). + /// + /// Note that SQLCipher is not available on the web. + sqlcipher, + + /// [SQLite3 Multiple Ciphers](https://utelle.github.io/SQLite3MultipleCiphers/) + /// provides compatibility with multiple encryption schemes from a single + /// build. + /// + /// On the web, this is the only option available to encrypt databases. + sqlite3MultipleCiphers; + + /// The [EncryptedSqliteVariant] enabled on the database, or null. + static EncryptedSqliteVariant? resolveOnDatabase(CommonDatabase db) { + if (db.select('pragma cipher').isNotEmpty) { + return EncryptedSqliteVariant.sqlite3MultipleCiphers; + } else if (db.select('pragma cipher_version').isNotEmpty) { + return EncryptedSqliteVariant.sqlcipher; + } else { + return null; + } + } +} diff --git a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart index c20752fc..ba67aeb3 100644 --- a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart @@ -22,7 +22,6 @@ base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory { @override List pragmaStatements(SqliteOpenOptions options) { return [ - ...?encryptionOptions?.pragmaStatements(), ...super.pragmaStatements(options), 'PRAGMA recursive_triggers = TRUE', ]; @@ -58,7 +57,7 @@ base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory { var retryDelay = 2; while (stopwatch.elapsedMilliseconds < 500) { try { - return super.openNativeConnection(options); + return openConnectionAttempt(options); } catch (e) { if (e is SqliteException && e.resultCode == 5) { sleep(Duration(milliseconds: retryDelay)); @@ -77,8 +76,18 @@ base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory { @override void configureConnection(Database database, SqliteOpenOptions options) { - if (encryptionOptions != null) { - EncryptionOptions.checkHasCipherPragma(database); + if (encryptionOptions case final encryption?) { + final resolved = EncryptedSqliteVariant.resolveOnDatabase(database); + if (resolved == null) { + throw UnsupportedError( + 'Tried to use encryption, but SQLite3MultipleCiphers is not available. ' + 'Consult the documentation on EncryptionOptions on how to resolve this.', + ); + } + + for (final pragma in encryption.pragmaStatements(variant: resolved)) { + database.execute(pragma); + } } super.configureConnection(database, options); diff --git a/packages/powersync/lib/src/version.dart b/packages/powersync/lib/src/version.dart index 1017f3a9..5f2e1c7d 100644 --- a/packages/powersync/lib/src/version.dart +++ b/packages/powersync/lib/src/version.dart @@ -1 +1 @@ -const String libraryVersion = '2.3.0'; +const String libraryVersion = '2.3.1'; diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index a636e714..2a599746 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,5 +1,5 @@ name: powersync -version: 2.3.0 +version: 2.3.1 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Dart and Flutter SDK. Sync Postgres, MongoDB, MySQL or SQL Server with SQLite in your app @@ -10,7 +10,7 @@ environment: dependencies: sqlite_async: ^0.14.3 sqlite3_web: ^0.9.0 - sqlite3: ^3.2.0 + sqlite3: ^3.3.3 meta: ^1.0.0 http: ^1.6.0 uuid: ^4.2.0 diff --git a/pubspec.lock b/pubspec.lock index c688b494..7f9221ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1497,10 +1497,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189" + sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.3.3" sqlite3_connection_pool: dependency: transitive description: diff --git a/tool/enable_encryption.dart b/tool/enable_encryption.dart index 6a442b93..f9f70f45 100644 --- a/tool/enable_encryption.dart +++ b/tool/enable_encryption.dart @@ -4,10 +4,12 @@ import 'dart:io'; /// encryption tests. /// /// Must be run from the root of the repository. -void main() { +void main(List args) { + final encryption = args.isEmpty ? 'sqlite3mc' : 'sqlcipher'; + final file = File('pubspec.yaml'); final updated = file .readAsStringSync() - .replaceFirst('source: sqlite3', 'source: sqlite3mc'); + .replaceFirst('source: sqlite3', 'source: $encryption'); file.writeAsStringSync(updated); } From 8a60ef417874149bfb926ec2170286922351ac9d Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 16:46:13 +0200 Subject: [PATCH 2/4] Fix encryption enable tool --- .github/workflows/check.yml | 5 ++--- tool/enable_encryption.dart | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f3c6f7e0..e1a11a8f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -85,13 +85,12 @@ jobs: - name: Enable SQLite3 Multiple Ciphers run: dart tool/enable_encryption.dart sqlite3mc - - name: Encryption tests + - name: Encryption tests with sqlite3mc working-directory: packages/powersync run: dart test -p vm -P encryption - name: Enable SQLCipher run: dart tool/enable_encryption.dart sqlcipher - - - name: Encryption tests + - name: Encryption tests with sqlcipher working-directory: packages/powersync run: dart test -p vm -P encryption diff --git a/tool/enable_encryption.dart b/tool/enable_encryption.dart index f9f70f45..ecc419d2 100644 --- a/tool/enable_encryption.dart +++ b/tool/enable_encryption.dart @@ -5,7 +5,7 @@ import 'dart:io'; /// /// Must be run from the root of the repository. void main(List args) { - final encryption = args.isEmpty ? 'sqlite3mc' : 'sqlcipher'; + final encryption = args.isEmpty ? 'sqlite3mc' : args.single; final file = File('pubspec.yaml'); final updated = file From d013fa1379d043c1f49c49be85087c5d7c6dc6f7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 16:57:01 +0200 Subject: [PATCH 3/4] Improve error message and docs --- .../lib/src/database/encryption_options.dart | 11 ++++++----- .../src/open_factory/native/native_open_factory.dart | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/powersync/lib/src/database/encryption_options.dart b/packages/powersync/lib/src/database/encryption_options.dart index 64cdbfdf..6596cbe1 100644 --- a/packages/powersync/lib/src/database/encryption_options.dart +++ b/packages/powersync/lib/src/database/encryption_options.dart @@ -50,11 +50,11 @@ final class EncryptionOptions { final String key; /// Whether to use an encryption scheme that is compatible with SQLCipher- - /// based databases. + /// based databases when SQLite3MultipleCiphers is enabled. /// - /// For backwards-compatibility with the `powersync_sqlcipher` package, this - /// is enabled by default on native platforms. If you've never used that - /// package, this can be disabled. + /// For backwards-compatibility with SQLCipher and the `powersync_sqlcipher` + /// package, this is enabled by default on native platforms. If you've never + /// used that package or SQLCipher, this can be disabled. final bool sqlcipherCompatibility; const EncryptionOptions({ @@ -78,7 +78,8 @@ final class EncryptionOptions { /// Throws if the `cipher` pragma doesn't exist, as that indicates that /// SQLite3MultipleCiphers is not available. - @Deprecated('Unused in PowerSync SDK') + @Deprecated('Unused in PowerSync SDK, check ' + 'EncryptedSqliteVariant.resolveOnDatabase instead') static void checkHasCipherPragma(CommonDatabase database) { if (database.select('pragma cipher').isEmpty) { throw UnsupportedError( diff --git a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart index ba67aeb3..459e6ca3 100644 --- a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart @@ -80,8 +80,9 @@ base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory { final resolved = EncryptedSqliteVariant.resolveOnDatabase(database); if (resolved == null) { throw UnsupportedError( - 'Tried to use encryption, but SQLite3MultipleCiphers is not available. ' - 'Consult the documentation on EncryptionOptions on how to resolve this.', + 'Tried to use encryption, but neither SQLCipher or ' + 'SQLite3MultipleCiphers is available. Consult the documentation on ' + 'EncryptionOptions on how to resolve this.', ); } From a7e1077398050052f9a6ccc57940d1753a1640b7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 17:02:17 +0200 Subject: [PATCH 4/4] Actually test with sqlcipher --- pubspec.yaml | 2 +- tool/enable_encryption.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9b7565b5..7c285250 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: hooks: user_defines: sqlite3: - # To run PowerSync encryption tests, replace this with sqlite3mc and run + # To run PowerSync encryption tests, replace this with sqlite3mc or sqlcipher and run # dart test -P encryption source: sqlite3 diff --git a/tool/enable_encryption.dart b/tool/enable_encryption.dart index ecc419d2..9ef5687a 100644 --- a/tool/enable_encryption.dart +++ b/tool/enable_encryption.dart @@ -8,8 +8,9 @@ void main(List args) { final encryption = args.isEmpty ? 'sqlite3mc' : args.single; final file = File('pubspec.yaml'); + final updated = file .readAsStringSync() - .replaceFirst('source: sqlite3', 'source: $encryption'); + .replaceFirst(RegExp(r'source: \w+'), 'source: $encryption'); file.writeAsStringSync(updated); }