diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5b641892..e1a11a8f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -83,9 +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 with sqlite3mc + working-directory: packages/powersync + run: dart test -p vm -P encryption - - name: Encryption tests + - name: Enable SQLCipher + run: dart tool/enable_encryption.dart sqlcipher + - name: Encryption tests with sqlcipher working-directory: packages/powersync run: dart test -p vm -P encryption 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..6596cbe1 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. /// @@ -47,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({ @@ -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,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, check ' + 'EncryptedSqliteVariant.resolveOnDatabase instead') static void checkHasCipherPragma(CommonDatabase database) { if (database.select('pragma cipher').isEmpty) { throw UnsupportedError( @@ -80,3 +89,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..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 @@ -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,19 @@ 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 neither SQLCipher or ' + 'SQLite3MultipleCiphers is 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/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 6a442b93..9ef5687a 100644 --- a/tool/enable_encryption.dart +++ b/tool/enable_encryption.dart @@ -4,10 +4,13 @@ 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' : args.single; + final file = File('pubspec.yaml'); + final updated = file .readAsStringSync() - .replaceFirst('source: sqlite3', 'source: sqlite3mc'); + .replaceFirst(RegExp(r'source: \w+'), 'source: $encryption'); file.writeAsStringSync(updated); }