Skip to content

Commit 214e884

Browse files
authored
Merge pull request #354 from powersync-ja/feat/sqlcipher-op-sqlite
Feat: Encryption with SQLCipher for OPSQLite
2 parents f88a162 + 4e214bb commit 214e884

File tree

6 files changed

+16904
-20705
lines changed

6 files changed

+16904
-20705
lines changed

.changeset/strong-hotels-deliver.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/op-sqlite': patch
3+
---
4+
5+
Encryption for databases using SQLCipher.

packages/powersync-op-sqlite/README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Overview
44

5-
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).
5+
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).
66

77
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.
88

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

46+
### Encryption with SQLCipher
47+
48+
To enable SQLCipher you need to add the following configuration option to your application's `package.json`
49+
50+
```json
51+
{
52+
// your normal package.json
53+
// ...
54+
"op-sqlite": {
55+
"sqlcipher": true
56+
}
57+
}
58+
```
59+
60+
Additionally you will need to add an [encryption key](https://www.zetetic.net/sqlcipher/sqlcipher-api/#key) to the OPSQLite factory constructor
61+
62+
```typescript
63+
const factory = new OPSqliteOpenFactory({
64+
dbFilename: 'sqlite.db',
65+
sqliteOptions: {
66+
encryptionKey: 'your-encryption-key'
67+
}
68+
});
69+
```
70+
4671
## Native Projects
4772

4873
This package uses native libraries. Create native Android and iOS projects (if not created already) by running:

packages/powersync-op-sqlite/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"access": "public"
6666
},
6767
"peerDependencies": {
68-
"@op-engineering/op-sqlite": "^9.1.3",
68+
"@op-engineering/op-sqlite": "^9.2.1",
6969
"@powersync/common": "workspace:^1.20.0",
7070
"react": "*",
7171
"react-native": "*"
@@ -75,7 +75,7 @@
7575
"async-lock": "^1.4.0"
7676
},
7777
"devDependencies": {
78-
"@op-engineering/op-sqlite": "^9.1.3",
78+
"@op-engineering/op-sqlite": "^9.2.1",
7979
"@react-native/eslint-config": "^0.73.1",
8080
"@types/async-lock": "^1.4.0",
8181
"@types/react": "^18.2.44",

packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ANDROID_DATABASE_PATH, IOS_LIBRARY_PATH, open, type DB } from '@op-engi
1111
import Lock from 'async-lock';
1212
import { OPSQLiteConnection } from './OPSQLiteConnection';
1313
import { NativeModules, Platform } from 'react-native';
14-
import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
14+
import { SqliteOptions } from './SqliteOptions';
1515

1616
/**
1717
* Adapter for React Native Quick SQLite
@@ -50,15 +50,10 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
5050
}
5151

5252
protected async init() {
53-
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous } = this.options.sqliteOptions;
54-
// const { dbFilename, dbLocation } = this.options;
53+
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous, encryptionKey } = this.options.sqliteOptions;
5554
const dbFilename = this.options.name;
56-
//This is needed because an undefined dbLocation will cause the open function to fail
57-
const location = this.getDbLocation(this.options.dbLocation);
58-
const DB: DB = open({
59-
name: dbFilename,
60-
location: location
61-
});
55+
56+
this.writeConnection = await this.openConnection(dbFilename);
6257

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

85-
this.loadExtension(DB);
86-
87-
await DB.execute('SELECT powersync_init()');
80+
// Changes should only occur in the write connection
81+
this.writeConnection!.registerListener({
82+
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
83+
});
8884

8985
this.readConnections = [];
9086
for (let i = 0; i < READ_CONNECTIONS; i++) {
9187
// Workaround to create read-only connections
9288
let dbName = './'.repeat(i + 1) + dbFilename;
93-
const conn = await this.openConnection(location, dbName);
89+
const conn = await this.openConnection(dbName);
9490
await conn.execute('PRAGMA query_only = true');
9591
this.readConnections.push(conn);
9692
}
97-
98-
this.writeConnection = new OPSQLiteConnection({
99-
baseDB: DB
100-
});
101-
102-
// Changes should only occur in the write connection
103-
this.writeConnection!.registerListener({
104-
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
105-
});
10693
}
10794

108-
protected async openConnection(dbLocation: string, filenameOverride?: string): Promise<OPSQLiteConnection> {
109-
const DB: DB = open({
110-
name: filenameOverride ?? this.options.name,
111-
location: dbLocation
112-
});
95+
protected async openConnection(filenameOverride?: string): Promise<OPSQLiteConnection> {
96+
const dbFilename = filenameOverride ?? this.options.name;
97+
const DB: DB = this.openDatabase(dbFilename, this.options.sqliteOptions.encryptionKey);
11398

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

117+
private openDatabase(dbFilename: string, encryptionKey?: string): DB {
118+
//This is needed because an undefined/null dbLocation will cause the open function to fail
119+
const location = this.getDbLocation(this.options.dbLocation);
120+
//Simarlily if the encryption key is undefined/null when using SQLCipher it will cause the open function to fail
121+
if (encryptionKey) {
122+
return open({
123+
name: dbFilename,
124+
location: location,
125+
encryptionKey: encryptionKey
126+
});
127+
} else {
128+
return open({
129+
name: dbFilename,
130+
location: location
131+
});
132+
}
133+
}
134+
132135
private loadExtension(DB: DB) {
133136
if (Platform.OS === 'ios') {
134137
const bundlePath: string = NativeModules.PowerSyncOpSqlite.getBundlePath();

packages/powersync-op-sqlite/src/db/SqliteOptions.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export interface SqliteOptions {
2323
* Set to null or zero to fail immediately when the database is locked.
2424
*/
2525
lockTimeoutMs?: number;
26+
27+
/**
28+
* Encryption key for the database.
29+
* If set, the database will be encrypted using SQLCipher.
30+
*/
31+
encryptionKey?: string;
2632
}
2733

2834
// SQLite journal mode. Set on the primary connection.
@@ -36,19 +42,20 @@ enum SqliteJournalMode {
3642
truncate = 'TRUNCATE',
3743
persist = 'PERSIST',
3844
memory = 'MEMORY',
39-
off = 'OFF',
45+
off = 'OFF'
4046
}
4147

4248
// SQLite file commit mode.
4349
enum SqliteSynchronous {
4450
normal = 'NORMAL',
4551
full = 'FULL',
46-
off = 'OFF',
52+
off = 'OFF'
4753
}
4854

4955
export const DEFAULT_SQLITE_OPTIONS: Required<SqliteOptions> = {
5056
journalMode: SqliteJournalMode.wal,
5157
synchronous: SqliteSynchronous.normal,
5258
journalSizeLimit: 6 * 1024 * 1024,
5359
lockTimeoutMs: 30000,
60+
encryptionKey: null
5461
};

0 commit comments

Comments
 (0)