Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions packages/powersync/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
56 changes: 48 additions & 8 deletions packages/powersync/lib/src/database/encryption_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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.
///
Expand All @@ -47,20 +50,24 @@ 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({
required this.key,
this.sqlcipherCompatibility = !_isWeb,
});

Iterable<String>? pragmaStatements() sync* {
if (sqlcipherCompatibility) {
Iterable<String> pragmaStatements({
EncryptedSqliteVariant variant =
EncryptedSqliteVariant.sqlite3MultipleCiphers,
}) sync* {
if (sqlcipherCompatibility &&
variant == EncryptedSqliteVariant.sqlite3MultipleCiphers) {
yield "PRAGMA cipher = 'sqlcipher'";
yield 'PRAGMA legacy = 4';
}
Expand All @@ -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(
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ base class NativePowerSyncOpenFactory extends NativeSqliteOpenFactory {
@override
List<String> pragmaStatements(SqliteOpenOptions options) {
return [
...?encryptionOptions?.pragmaStatements(),
...super.pragmaStatements(options),
'PRAGMA recursive_triggers = TRUE',
];
Expand Down Expand Up @@ -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));
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/powersync/lib/src/version.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
const String libraryVersion = '2.3.0';
const String libraryVersion = '2.3.1';
4 changes: 2 additions & 2 deletions packages/powersync/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions tool/enable_encryption.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import 'dart:io';
/// encryption tests.
///
/// Must be run from the root of the repository.
void main() {
void main(List<String> 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);
}