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
5 changes: 5 additions & 0 deletions .changeset/strong-hotels-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/op-sqlite': patch
---

Encryption for databases using SQLCipher.
27 changes: 26 additions & 1 deletion packages/powersync-op-sqlite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

This package (`packages/powersync-op-sqlite`) enables using [OP-SQLite](https://github.com/op-engineering/op-sqlite) with PowerSync alongside the [React Native SDK](https://docs.powersync.com/client-sdk-references/react-native-and-expo).
This package (`packages/powersync-op-sqlite`) enables using [OP-SQLite](https://github.com/op-engineering/op-sqlite) with PowerSync alongside the [React Native SDK](https://docs.powersync.com/client-sdk-references/react-native-and-expo).

If you are not yet familiar with PowerSync, please see the [PowerSync React Native SDK README](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native) for more information.

Expand Down Expand Up @@ -43,6 +43,31 @@ const factory = new OPSqliteOpenFactory({
this.powersync = new PowerSyncDatabase({ database: factory, schema: AppSchema });
```

### Encryption with SQLCipher

To enable SQLCipher you need to add the following configuration option to your application's `package.json`

```json
{
// your normal package.json
// ...
"op-sqlite": {
"sqlcipher": true
}
}
```

Additionally you will need to add an [encryption key](https://www.zetetic.net/sqlcipher/sqlcipher-api/#key) to the OPSQLite factory constructor

```typescript
const factory = new OPSqliteOpenFactory({
dbFilename: 'sqlite.db',
sqliteOptions: {
encryptionKey: 'your-encryption-key'
}
});
```

## Native Projects

This package uses native libraries. Create native Android and iOS projects (if not created already) by running:
Expand Down
4 changes: 2 additions & 2 deletions packages/powersync-op-sqlite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"access": "public"
},
"peerDependencies": {
"@op-engineering/op-sqlite": "^9.1.3",
"@op-engineering/op-sqlite": "^9.2.1",
"@powersync/common": "workspace:^1.20.0",
"react": "*",
"react-native": "*"
Expand All @@ -75,7 +75,7 @@
"async-lock": "^1.4.0"
},
"devDependencies": {
"@op-engineering/op-sqlite": "^9.1.3",
"@op-engineering/op-sqlite": "^9.2.1",
"@react-native/eslint-config": "^0.73.1",
"@types/async-lock": "^1.4.0",
"@types/react": "^18.2.44",
Expand Down
59 changes: 31 additions & 28 deletions packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ANDROID_DATABASE_PATH, IOS_LIBRARY_PATH, open, type DB } from '@op-engi
import Lock from 'async-lock';
import { OPSQLiteConnection } from './OPSQLiteConnection';
import { NativeModules, Platform } from 'react-native';
import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
import { SqliteOptions } from './SqliteOptions';

/**
* Adapter for React Native Quick SQLite
Expand Down Expand Up @@ -50,15 +50,10 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
}

protected async init() {
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous } = this.options.sqliteOptions;
// const { dbFilename, dbLocation } = this.options;
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous, encryptionKey } = this.options.sqliteOptions;
const dbFilename = this.options.name;
//This is needed because an undefined dbLocation will cause the open function to fail
const location = this.getDbLocation(this.options.dbLocation);
const DB: DB = open({
name: dbFilename,
location: location
});

this.writeConnection = await this.openConnection(dbFilename);

const statements: string[] = [
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
Expand All @@ -70,7 +65,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
for (const statement of statements) {
for (let tries = 0; tries < 30; tries++) {
try {
await DB.execute(statement);
await this.writeConnection!.execute(statement);
break;
} catch (e: any) {
if (e instanceof Error && e.message.includes('database is locked') && tries < 29) {
Expand All @@ -82,34 +77,24 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
}
}

this.loadExtension(DB);

await DB.execute('SELECT powersync_init()');
// Changes should only occur in the write connection
this.writeConnection!.registerListener({
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
});

this.readConnections = [];
for (let i = 0; i < READ_CONNECTIONS; i++) {
// Workaround to create read-only connections
let dbName = './'.repeat(i + 1) + dbFilename;
const conn = await this.openConnection(location, dbName);
const conn = await this.openConnection(dbName);
await conn.execute('PRAGMA query_only = true');
this.readConnections.push(conn);
}

this.writeConnection = new OPSQLiteConnection({
baseDB: DB
});

// Changes should only occur in the write connection
this.writeConnection!.registerListener({
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
});
}

protected async openConnection(dbLocation: string, filenameOverride?: string): Promise<OPSQLiteConnection> {
const DB: DB = open({
name: filenameOverride ?? this.options.name,
location: dbLocation
});
protected async openConnection(filenameOverride?: string): Promise<OPSQLiteConnection> {
const dbFilename = filenameOverride ?? this.options.name;
const DB: DB = this.openDatabase(dbFilename, this.options.sqliteOptions.encryptionKey);

//Load extension for all connections
this.loadExtension(DB);
Expand All @@ -129,6 +114,24 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
}
}

private openDatabase(dbFilename: string, encryptionKey?: string): DB {
//This is needed because an undefined/null dbLocation will cause the open function to fail
const location = this.getDbLocation(this.options.dbLocation);
//Simarlily if the encryption key is undefined/null when using SQLCipher it will cause the open function to fail
if (encryptionKey) {
return open({
name: dbFilename,
location: location,
encryptionKey: encryptionKey
});
} else {
return open({
name: dbFilename,
location: location
});
}
}

private loadExtension(DB: DB) {
if (Platform.OS === 'ios') {
const bundlePath: string = NativeModules.PowerSyncOpSqlite.getBundlePath();
Expand Down
11 changes: 9 additions & 2 deletions packages/powersync-op-sqlite/src/db/SqliteOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export interface SqliteOptions {
* Set to null or zero to fail immediately when the database is locked.
*/
lockTimeoutMs?: number;

/**
* Encryption key for the database.
* If set, the database will be encrypted using SQLCipher.
*/
encryptionKey?: string;
}

// SQLite journal mode. Set on the primary connection.
Expand All @@ -36,19 +42,20 @@ enum SqliteJournalMode {
truncate = 'TRUNCATE',
persist = 'PERSIST',
memory = 'MEMORY',
off = 'OFF',
off = 'OFF'
}

// SQLite file commit mode.
enum SqliteSynchronous {
normal = 'NORMAL',
full = 'FULL',
off = 'OFF',
off = 'OFF'
}

export const DEFAULT_SQLITE_OPTIONS: Required<SqliteOptions> = {
journalMode: SqliteJournalMode.wal,
synchronous: SqliteSynchronous.normal,
journalSizeLimit: 6 * 1024 * 1024,
lockTimeoutMs: 30000,
encryptionKey: null
};
Loading
Loading