Skip to content

Commit

Permalink
Web database encryption (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
mugikhan authored Dec 17, 2024
1 parent c2d0679 commit 77543e3
Show file tree
Hide file tree
Showing 9 changed files with 18,573 additions and 23,183 deletions.
2 changes: 1 addition & 1 deletion demos/react-supabase-todolist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@powersync/web": "workspace:*",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"@journeyapps/wa-sqlite": "^1.0.0",
"@journeyapps/wa-sqlite": "^1.1.1",
"@mui/icons-material": "^5.15.12",
"@mui/material": "^5.15.12",
"@mui/x-data-grid": "^6.19.6",
Expand Down
33 changes: 33 additions & 0 deletions packages/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,39 @@ Install it in your app with:
npm install @journeyapps/wa-sqlite
```

### Encryption with Multiple Ciphers

To enable encryption you need to specify an encryption key when instantiating the PowerSync database.

> The PowerSync Web SDK uses the ChaCha20 cipher algorithm by [default](https://utelle.github.io/SQLite3MultipleCiphers/docs/ciphers/cipher_chacha20/).
```typescript
export const db = new PowerSyncDatabase({
// The schema you defined
schema: AppSchema,
database: {
// Filename for the SQLite database — it's important to only instantiate one instance per file.
dbFilename: 'example.db'
// Optional. Directory where the database file is located.'
// dbLocation: 'path/to/directory'
},
// Encryption key for the database.
encryptionKey: 'your-encryption-key'
});

// If you are using a custom WASQLiteOpenFactory or WASQLiteDBAdapter, you need specify the encryption key inside the construtor
export const db = new PowerSyncDatabase({
schema: AppSchema,
database: new WASQLiteOpenFactory({
//new WASQLiteDBAdapter
dbFilename: 'example.db',
vfs: WASQLiteVFS.OPFSCoopSyncVFS,
// Encryption key for the database.
encryptionKey: 'your-encryption-key'
})
});
```

## Webpack

See the [example Webpack config](https://github.com/powersync-ja/powersync-js/blob/main/demos/example-webpack/webpack.config.js) for details on polyfills and requirements.
Expand Down
4 changes: 2 additions & 2 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"author": "JOURNEYAPPS",
"license": "Apache-2.0",
"peerDependencies": {
"@journeyapps/wa-sqlite": "^1.0.0",
"@journeyapps/wa-sqlite": "^1.1.1",
"@powersync/common": "workspace:^1.22.0"
},
"dependencies": {
Expand All @@ -72,7 +72,7 @@
"js-logger": "^1.6.1"
},
"devDependencies": {
"@journeyapps/wa-sqlite": "^1.0.0",
"@journeyapps/wa-sqlite": "^1.1.1",
"@types/uuid": "^9.0.6",
"@vitest/browser": "^2.1.4",
"crypto-browserify": "^3.12.0",
Expand Down
33 changes: 31 additions & 2 deletions packages/web/src/db/PowerSyncDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
AbstractPowerSyncDatabase,
DBAdapter,
DEFAULT_POWERSYNC_CLOSE_OPTIONS,
isDBAdapter,
isSQLOpenFactory,
PowerSyncDatabaseOptions,
PowerSyncDatabaseOptionsWithDBAdapter,
PowerSyncDatabaseOptionsWithOpenFactory,
Expand Down Expand Up @@ -56,14 +58,24 @@ type WithWebSyncOptions<Base> = Base & {
sync?: WebSyncOptions;
};

export interface WebEncryptionOptions {
/**
* Encryption key for the database.
* If set, the database will be encrypted using Multiple Ciphers.
*/
encryptionKey?: string;
}

type WithWebEncryptionOptions<Base> = Base & WebEncryptionOptions;

export type WebPowerSyncDatabaseOptionsWithAdapter = WithWebSyncOptions<
WithWebFlags<PowerSyncDatabaseOptionsWithDBAdapter>
>;
export type WebPowerSyncDatabaseOptionsWithOpenFactory = WithWebSyncOptions<
WithWebFlags<PowerSyncDatabaseOptionsWithOpenFactory>
>;
export type WebPowerSyncDatabaseOptionsWithSettings = WithWebSyncOptions<
WithWebFlags<PowerSyncDatabaseOptionsWithSettings>
WithWebFlags<WithWebEncryptionOptions<PowerSyncDatabaseOptionsWithSettings>>
>;

export type WebPowerSyncDatabaseOptions = WithWebSyncOptions<WithWebFlags<PowerSyncDatabaseOptions>>;
Expand All @@ -81,6 +93,20 @@ export const resolveWebPowerSyncFlags = (flags?: WebPowerSyncFlags): Required<We
};
};

/**
* Asserts that the database options are valid for custom database constructors.
*/
function assertValidDatabaseOptions(options: WebPowerSyncDatabaseOptions): void {
if ('database' in options && 'encryptionKey' in options) {
const { database } = options;
if (isSQLOpenFactory(database) || isDBAdapter(database)) {
throw new Error(
`Invalid configuration: 'encryptionKey' should only be included inside the database object when using a custom ${isSQLOpenFactory(database) ? 'WASQLiteOpenFactory' : 'WASQLiteDBAdapter'} constructor.`
);
}
}
}

/**
* A PowerSync database which provides SQLite functionality
* which is automatically synced.
Expand Down Expand Up @@ -108,6 +134,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
constructor(protected options: WebPowerSyncDatabaseOptions) {
super(options);

assertValidDatabaseOptions(options);

this.resolvedFlags = resolveWebPowerSyncFlags(options.flags);

if (this.resolvedFlags.enableMultiTabs && !this.resolvedFlags.externallyUnload) {
Expand All @@ -121,7 +149,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
protected openDBAdapter(options: WebPowerSyncDatabaseOptionsWithSettings): DBAdapter {
const defaultFactory = new WASQLiteOpenFactory({
...options.database,
flags: resolveWebPowerSyncFlags(options.flags)
flags: resolveWebPowerSyncFlags(options.flags),
encryptionKey: options.encryptionKey
});
return defaultFactory.openDB();
}
Expand Down
62 changes: 57 additions & 5 deletions packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type SQLiteModule = Parameters<typeof SQLite.Factory>[0];
/**
* @internal
*/
export type WASQLiteModuleFactoryOptions = { dbFileName: string };
export type WASQLiteModuleFactoryOptions = { dbFileName: string; encryptionKey?: string };

/**
* @internal
Expand All @@ -53,6 +53,14 @@ export const AsyncWASQLiteModuleFactory = async () => {
return factory();
};

/**
* @internal
*/
export const MultiCipherAsyncWASQLiteModuleFactory = async () => {
const { default: factory } = await import('@journeyapps/wa-sqlite/dist/mc-wa-sqlite-async.mjs');
return factory();
};

/**
* @internal
*/
Expand All @@ -61,12 +69,25 @@ export const SyncWASQLiteModuleFactory = async () => {
return factory();
};

/**
* @internal
*/
export const MultiCipherSyncWASQLiteModuleFactory = async () => {
const { default: factory } = await import('@journeyapps/wa-sqlite/dist/mc-wa-sqlite.mjs');
return factory();
};

/**
* @internal
*/
export const DEFAULT_MODULE_FACTORIES = {
[WASQLiteVFS.IDBBatchAtomicVFS]: async (options: WASQLiteModuleFactoryOptions) => {
const module = await AsyncWASQLiteModuleFactory();
let module;
if (options.encryptionKey) {
module = await MultiCipherAsyncWASQLiteModuleFactory();
} else {
module = await AsyncWASQLiteModuleFactory();
}
const { IDBBatchAtomicVFS } = await import('@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js');
return {
module,
Expand All @@ -75,7 +96,12 @@ export const DEFAULT_MODULE_FACTORIES = {
};
},
[WASQLiteVFS.AccessHandlePoolVFS]: async (options: WASQLiteModuleFactoryOptions) => {
const module = await SyncWASQLiteModuleFactory();
let module;
if (options.encryptionKey) {
module = await MultiCipherSyncWASQLiteModuleFactory();
} else {
module = await SyncWASQLiteModuleFactory();
}
// @ts-expect-error The types for this static method are missing upstream
const { AccessHandlePoolVFS } = await import('@journeyapps/wa-sqlite/src/examples/AccessHandlePoolVFS.js');
return {
Expand All @@ -84,7 +110,12 @@ export const DEFAULT_MODULE_FACTORIES = {
};
},
[WASQLiteVFS.OPFSCoopSyncVFS]: async (options: WASQLiteModuleFactoryOptions) => {
const module = await SyncWASQLiteModuleFactory();
let module;
if (options.encryptionKey) {
module = await MultiCipherSyncWASQLiteModuleFactory();
} else {
module = await SyncWASQLiteModuleFactory();
}
// @ts-expect-error The types for this static method are missing upstream
const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
return {
Expand Down Expand Up @@ -146,15 +177,35 @@ export class WASqliteConnection
return this._dbP;
}

protected async executeEncryptionPragma(): Promise<void> {
if (this.options.encryptionKey) {
await this.executeSingleStatement(`PRAGMA key = "${this.options.encryptionKey}"`);
}
return;
}

protected async openSQLiteAPI(): Promise<SQLiteAPI> {
const { module, vfs } = await this._moduleFactory({ dbFileName: this.options.dbFilename });
const { module, vfs } = await this._moduleFactory({
dbFileName: this.options.dbFilename,
encryptionKey: this.options.encryptionKey
});
const sqlite3 = SQLite.Factory(module);
sqlite3.vfs_register(vfs, true);
/**
* Register the PowerSync core SQLite extension
*/
module.ccall('powersync_init_static', 'int', []);

/**
* Create the multiple cipher vfs if an encryption key is provided
*/
if (this.options.encryptionKey) {
const createResult = module.ccall('sqlite3mc_vfs_create', 'int', ['string', 'int'], [this.options.dbFilename, 1]);
if (createResult !== 0) {
throw new Error('Failed to create multiple cipher vfs, Database encryption will not work');
}
}

return sqlite3;
}

Expand Down Expand Up @@ -182,6 +233,7 @@ export class WASqliteConnection
await this.openDB();
this.registerBroadcastListeners();
await this.executeSingleStatement(`PRAGMA temp_store = ${this.options.temporaryStorage};`);
await this.executeEncryptionPragma();

this.sqliteAPI.update_hook(this.dbP, (updateType: number, dbName: string | null, tableName: string | null) => {
if (!tableName) {
Expand Down
10 changes: 9 additions & 1 deletion packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface WASQLiteDBAdapterOptions extends Omit<PowerSyncOpenFactoryOptio

vfs?: WASQLiteVFS;
temporaryStorage?: TemporaryStorageOption;

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

/**
Expand All @@ -46,7 +52,8 @@ export class WASQLiteDBAdapter extends LockedAsyncDatabaseAdapter {
baseConnection: await remote({
...options,
temporaryStorage: temporaryStorage ?? TemporaryStorageOption.MEMORY,
flags: resolveWebPowerSyncFlags(options.flags)
flags: resolveWebPowerSyncFlags(options.flags),
encryptionKey: options.encryptionKey
})
});
}
Expand All @@ -58,6 +65,7 @@ export class WASQLiteDBAdapter extends LockedAsyncDatabaseAdapter {
temporaryStorage,
logger: options.logger,
vfs: options.vfs,
encryptionKey: options.encryptionKey,
worker: options.worker
});
return openFactory.openConnection();
Expand Down
15 changes: 11 additions & 4 deletions packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {

async openConnection(): Promise<AsyncDatabaseConnection> {
const { enableMultiTabs, useWebWorker } = this.resolvedFlags;
const { vfs = WASQLiteVFS.IDBBatchAtomicVFS, temporaryStorage = TemporaryStorageOption.MEMORY } = this.waOptions;
const {
vfs = WASQLiteVFS.IDBBatchAtomicVFS,
temporaryStorage = TemporaryStorageOption.MEMORY,
encryptionKey
} = this.waOptions;

if (!enableMultiTabs) {
this.logger.warn('Multiple tabs are not enabled in this browser');
Expand All @@ -56,7 +60,8 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
optionsDbWorker({
...this.options,
temporaryStorage,
flags: this.resolvedFlags
flags: this.resolvedFlags,
encryptionKey
})
)
: openWorkerDatabasePort(this.options.dbFilename, enableMultiTabs, optionsDbWorker, this.waOptions.vfs);
Expand All @@ -69,7 +74,8 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
dbFilename: this.options.dbFilename,
vfs,
temporaryStorage,
flags: this.resolvedFlags
flags: this.resolvedFlags,
encryptionKey: encryptionKey
}),
identifier: this.options.dbFilename
});
Expand All @@ -81,7 +87,8 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
debugMode: this.options.debugMode,
vfs,
temporaryStorage,
flags: this.resolvedFlags
flags: this.resolvedFlags,
encryptionKey: encryptionKey
});
}
}
Expand Down
12 changes: 12 additions & 0 deletions packages/web/src/db/adapters/web-sql-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export interface ResolvedWebSQLOpenOptions extends SQLOpenOptions {
* Setting this to `FILESYSTEM` can cause issues with larger queries or datasets.
*/
temporaryStorage: TemporaryStorageOption;

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

export enum TemporaryStorageOption {
Expand Down Expand Up @@ -73,6 +79,12 @@ export interface WebSQLOpenFactoryOptions extends SQLOpenOptions {
* Setting this to `FILESYSTEM` can cause issues with larger queries or datasets.
*/
temporaryStorage?: TemporaryStorageOption;

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

export function isServerSide() {
Expand Down
Loading

0 comments on commit 77543e3

Please sign in to comment.