Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509562
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/web/database/database-module-provider.js b/web/database/database-module-provider.js
index b879812ac..f479c23ee 100644
--- a/web/database/database-module-provider.js
+++ b/web/database/database-module-provider.js
@@ -1,101 +1,119 @@
// @flow
import { DATABASE_WORKER_PATH, SQLJS_FILE_PATH } from './utils/constants.js';
import { isSQLiteSupported } from './utils/db-utils.js';
import WorkerConnectionProxy from './utils/WorkerConnectionProxy.js';
import type { AppState } from '../redux/redux-setup.js';
import {
workerRequestMessageTypes,
type WorkerRequestMessage,
type WorkerResponseMessage,
} from '../types/worker-types.js';
declare var sqljsFilename: string;
declare var preloadedState: AppState;
const databaseStatuses = Object.freeze({
notSupported: 'NOT_SUPPORTED',
initSuccess: 'INIT_SUCCESS',
initInProgress: 'INIT_IN_PROGRESS',
initError: 'INIT_ERROR',
});
type DatabaseStatus = $Values<typeof databaseStatuses>;
class DatabaseModule {
worker: SharedWorker;
workerProxy: WorkerConnectionProxy;
initPromise: Promise<void>;
status: DatabaseStatus;
constructor() {
const currentLoggedInUserID = preloadedState.currentUserInfo?.anonymous
? undefined
: preloadedState.currentUserInfo?.id;
const isSupported = isSQLiteSupported(currentLoggedInUserID);
if (!isSupported) {
this.status = databaseStatuses.notSupported;
} else {
this.init();
}
}
init() {
this.status = databaseStatuses.initInProgress;
this.worker = new SharedWorker(DATABASE_WORKER_PATH);
this.worker.onerror = console.error;
this.workerProxy = new WorkerConnectionProxy(
this.worker.port,
console.error,
);
const origin = window.location.origin;
this.initPromise = (async () => {
try {
await this.workerProxy.scheduleOnWorker({
type: workerRequestMessageTypes.INIT,
sqljsFilePath: `${origin}${SQLJS_FILE_PATH}`,
sqljsFilename,
});
this.status = databaseStatuses.initSuccess;
console.info('Database initialization success');
} catch (error) {
this.status = databaseStatuses.initError;
console.error(`Database initialization failure`, error);
}
})();
}
- userLoggedIn(currentLoggedInUserID: ?string) {
+ initDBForLoggedInUser(currentLoggedInUserID: ?string) {
+ if (this.status === databaseStatuses.initSuccess) {
+ return;
+ }
+
if (
this.status === databaseStatuses.notSupported &&
isSQLiteSupported(currentLoggedInUserID)
) {
this.init();
}
}
+ async clearSensitiveData(): Promise<void> {
+ this.status = databaseStatuses.notSupported;
+ await this.workerProxy.scheduleOnWorker({
+ type: workerRequestMessageTypes.CLEAR_SENSITIVE_DATA,
+ });
+ }
+
+ async isDatabaseSupported(): Promise<boolean> {
+ if (this.status === databaseStatuses.initInProgress) {
+ await this.initPromise;
+ }
+ return this.status === databaseStatuses.initSuccess;
+ }
+
async schedule(
payload: WorkerRequestMessage,
): Promise<?WorkerResponseMessage> {
if (this.status === databaseStatuses.notSupported) {
throw new Error('Database not supported');
}
if (this.status === databaseStatuses.initInProgress) {
await this.initPromise;
}
if (this.status === databaseStatuses.initError) {
throw new Error('Database could not be initialized');
}
return this.workerProxy.scheduleOnWorker(payload);
}
}
const databaseModule: DatabaseModule = new DatabaseModule();
export { databaseModule };
diff --git a/web/database/sqlite-data-handler.js b/web/database/sqlite-data-handler.js
new file mode 100644
index 000000000..6b8274f9a
--- /dev/null
+++ b/web/database/sqlite-data-handler.js
@@ -0,0 +1,62 @@
+// @flow
+
+import * as React from 'react';
+
+import { databaseModule } from './database-module-provider.js';
+import { useSelector } from '../redux/redux-utils.js';
+import { workerRequestMessageTypes } from '../types/worker-types.js';
+
+function SQLiteDataHandler(): React.Node {
+ const rehydrateConcluded = useSelector(
+ state => !!(state._persist && state._persist.rehydrated),
+ );
+ const currentLoggedInUserID = useSelector(state =>
+ state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id,
+ );
+
+ const handleSensitiveData = React.useCallback(async () => {
+ try {
+ const currentUserData = await databaseModule.schedule({
+ type: workerRequestMessageTypes.GET_CURRENT_USER_ID,
+ });
+ const currentDBUserID = currentUserData?.userID;
+
+ if (currentDBUserID && currentDBUserID !== currentLoggedInUserID) {
+ await databaseModule.clearSensitiveData();
+ }
+ if (
+ currentLoggedInUserID &&
+ (currentDBUserID || currentDBUserID !== currentLoggedInUserID)
+ ) {
+ await databaseModule.schedule({
+ type: workerRequestMessageTypes.SET_CURRENT_USER_ID,
+ userID: currentLoggedInUserID,
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+ }, [currentLoggedInUserID]);
+
+ React.useEffect(() => {
+ (async () => {
+ if (currentLoggedInUserID) {
+ await databaseModule.initDBForLoggedInUser(currentLoggedInUserID);
+ }
+ if (!rehydrateConcluded) {
+ return;
+ }
+
+ const isSupported = await databaseModule.isDatabaseSupported();
+ if (!isSupported) {
+ return;
+ }
+ await handleSensitiveData();
+ })();
+ }, [currentLoggedInUserID, handleSensitiveData, rehydrateConcluded]);
+
+ return null;
+}
+
+export { SQLiteDataHandler };
diff --git a/web/database/worker/db-worker.js b/web/database/worker/db-worker.js
index a77ae0ff5..702af9a17 100644
--- a/web/database/worker/db-worker.js
+++ b/web/database/worker/db-worker.js
@@ -1,260 +1,267 @@
// @flow
import localforage from 'localforage';
import _throttle from 'lodash/throttle.js';
import initSqlJs, { type SqliteDatabase } from 'sql.js';
import type {
ClientDBDraftStoreOperation,
DraftStoreOperation,
} from 'lib/types/draft-types.js';
import type { ClientDBStore } from 'lib/types/store-ops-types.js';
import {
type SharedWorkerMessageEvent,
type WorkerRequestMessage,
type WorkerResponseMessage,
workerRequestMessageTypes,
workerResponseMessageTypes,
type WorkerRequestProxyMessage,
workerWriteRequests,
} from '../../types/worker-types.js';
import { getSQLiteDBVersion, setupSQLiteDB } from '../queries/db-queries.js';
import {
getAllDrafts,
moveDraft,
removeAllDrafts,
updateDraft,
} from '../queries/draft-queries.js';
import { getMetadata, setMetadata } from '../queries/metadata-queries.js';
import {
getPersistStorageItem,
removePersistStorageItem,
setPersistStorageItem,
} from '../queries/storage-engine-queries.js';
import {
CURRENT_USER_ID_KEY,
DB_PERSIST_THROTTLE_WAIT_MS,
SQLITE_CONTENT,
SQLITE_ENCRYPTION_KEY,
} from '../utils/constants.js';
import {
decryptDatabaseFile,
encryptDatabaseFile,
generateDatabaseCryptoKey,
} from '../utils/worker-crypto-utils.js';
const localforageConfig: PartialConfig = {
driver: localforage.INDEXEDDB,
name: 'comm',
storeName: 'commStorage',
description: 'Comm encrypted database storage',
version: '1.0',
};
localforage.config(localforageConfig);
let sqliteDb: ?SqliteDatabase = null;
let encryptionKey: ?CryptoKey = null;
async function initDatabase(sqljsFilePath: string, sqljsFilename: ?string) {
encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY);
if (!encryptionKey) {
const cryptoKey = await generateDatabaseCryptoKey();
await localforage.setItem(SQLITE_ENCRYPTION_KEY, cryptoKey);
}
const encryptedContent = await localforage.getItem(SQLITE_CONTENT);
let dbContent = null;
try {
if (encryptionKey && encryptedContent) {
dbContent = await decryptDatabaseFile(encryptedContent, encryptionKey);
}
} catch (e) {
console.error('Error while decrypting content, clearing database content');
await localforage.removeItem(SQLITE_CONTENT);
}
const locateFile = defaultFilename => {
if (sqljsFilename) {
return `${sqljsFilePath}/${sqljsFilename}`;
}
return `${sqljsFilePath}/${defaultFilename}`;
};
const SQL = await initSqlJs({
locateFile,
});
if (dbContent) {
sqliteDb = new SQL.Database(dbContent);
console.info(
'Database exists and is properly encrypted, using persisted data',
);
} else {
sqliteDb = new SQL.Database();
setupSQLiteDB(sqliteDb);
console.info('Creating fresh database');
}
const dbVersion = getSQLiteDBVersion(sqliteDb);
console.info(`Db version: ${dbVersion}`);
}
function processDraftStoreOperations(
operations: $ReadOnlyArray<ClientDBDraftStoreOperation>,
) {
if (!sqliteDb) {
throw new Error('Database not initialized');
}
for (const operation: DraftStoreOperation of operations) {
if (operation.type === 'remove_all') {
removeAllDrafts(sqliteDb);
} else if (operation.type === 'update') {
const { key, text } = operation.payload;
updateDraft(sqliteDb, key, text);
} else if (operation.type === 'move') {
const { oldKey, newKey } = operation.payload;
moveDraft(sqliteDb, oldKey, newKey);
} else {
throw new Error('Unsupported draft operation');
}
}
}
function getClientStore(): ClientDBStore {
if (!sqliteDb) {
throw new Error('Database not initialized');
}
return {
drafts: getAllDrafts(sqliteDb),
messages: [],
threads: [],
messageStoreThreads: [],
};
}
async function persist() {
if (!sqliteDb) {
throw new Error('Database not initialized');
}
if (!encryptionKey) {
encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY);
}
const dbData = sqliteDb.export();
if (!encryptionKey) {
throw new Error('Encryption key is missing');
}
const encryptedData = await encryptDatabaseFile(dbData, encryptionKey);
await localforage.setItem(SQLITE_CONTENT, encryptedData);
}
const throttledPersist = _throttle(persist, DB_PERSIST_THROTTLE_WAIT_MS);
async function processAppRequest(
message: WorkerRequestMessage,
): Promise<?WorkerResponseMessage> {
// non-database operations
if (message.type === workerRequestMessageTypes.PING) {
return {
type: workerResponseMessageTypes.PONG,
text: 'PONG',
};
} else if (
message.type === workerRequestMessageTypes.GENERATE_DATABASE_ENCRYPTION_KEY
) {
const cryptoKey = await generateDatabaseCryptoKey();
await localforage.setItem(SQLITE_ENCRYPTION_KEY, cryptoKey);
return undefined;
}
// database operations
if (message.type === workerRequestMessageTypes.INIT) {
await initDatabase(message.sqljsFilePath, message.sqljsFilename);
return undefined;
+ } else if (message.type === workerRequestMessageTypes.CLEAR_SENSITIVE_DATA) {
+ encryptionKey = null;
+ if (sqliteDb) {
+ sqliteDb.close();
+ }
+ await localforage.clear();
+ return undefined;
}
if (!sqliteDb) {
throw new Error('Database not initialized');
}
// read-only operations
if (message.type === workerRequestMessageTypes.GET_CLIENT_STORE) {
return {
type: workerResponseMessageTypes.CLIENT_STORE,
store: getClientStore(),
};
} else if (message.type === workerRequestMessageTypes.GET_CURRENT_USER_ID) {
return {
type: workerResponseMessageTypes.GET_CURRENT_USER_ID,
userID: getMetadata(sqliteDb, CURRENT_USER_ID_KEY),
};
} else if (
message.type === workerRequestMessageTypes.GET_PERSIST_STORAGE_ITEM
) {
return {
type: workerResponseMessageTypes.GET_PERSIST_STORAGE_ITEM,
item: getPersistStorageItem(sqliteDb, message.key),
};
}
// write operations
if (!workerWriteRequests.includes(message.type)) {
throw new Error('Request type not supported');
}
if (message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS) {
const { draftStoreOperations } = message.storeOperations;
if (draftStoreOperations) {
processDraftStoreOperations(draftStoreOperations);
}
} else if (message.type === workerRequestMessageTypes.SET_CURRENT_USER_ID) {
setMetadata(sqliteDb, CURRENT_USER_ID_KEY, message.userID);
} else if (
message.type === workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM
) {
setPersistStorageItem(sqliteDb, message.key, message.item);
} else if (
message.type === workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM
) {
removePersistStorageItem(sqliteDb, message.key);
}
throttledPersist();
return undefined;
}
function connectHandler(event: SharedWorkerMessageEvent) {
if (!event.ports.length) {
return;
}
const port: MessagePort = event.ports[0];
console.log('Web database worker alive!');
port.onmessage = async function (messageEvent: MessageEvent) {
const data: WorkerRequestProxyMessage = (messageEvent.data: any);
const { id, message } = data;
if (!id) {
port.postMessage({
error: new Error('Request without identifier'),
});
}
try {
const result = await processAppRequest(message);
port.postMessage({
id,
message: result,
});
} catch (e) {
port.postMessage({
id,
error: e,
});
}
};
}
self.addEventListener('connect', connectHandler);
diff --git a/web/root.js b/web/root.js
index 5145e63a7..67e25559a 100644
--- a/web/root.js
+++ b/web/root.js
@@ -1,78 +1,80 @@
// @flow
import * as React from 'react';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router';
import { createStore, applyMiddleware, type Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js';
import { createMigrate, persistReducer, persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/es/integration/react.js';
import storage from 'redux-persist/es/storage/index.js';
import thunk from 'redux-thunk';
import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js';
import { isDev } from 'lib/utils/dev-utils.js';
import App from './app.react.js';
+import { SQLiteDataHandler } from './database/sqlite-data-handler.js';
import ErrorBoundary from './error-boundary.react.js';
import Loading from './loading.react.js';
import { reducer } from './redux/redux-setup.js';
import type { AppState, Action } from './redux/redux-setup.js';
import history from './router-history.js';
import Socket from './socket.react.js';
const migrations = {
[1]: state => {
const {
primaryIdentityPublicKey,
...stateWithoutPrimaryIdentityPublicKey
} = state;
return {
...stateWithoutPrimaryIdentityPublicKey,
cryptoStore: {
primaryAccount: null,
primaryIdentityKeys: null,
notificationAccount: null,
notificationIdentityKeys: null,
},
};
},
};
const persistConfig = {
key: 'root',
storage,
whitelist: [
'enabledApps',
'deviceID',
'draftStore',
'cryptoStore',
'notifPermissionAlertInfo',
'commServicesAccessToken',
],
migrate: (createMigrate(migrations, { debug: isDev }): any),
version: 1,
};
declare var preloadedState: AppState;
const persistedReducer = persistReducer(persistConfig, reducer);
const store: Store<AppState, Action> = createStore(
persistedReducer,
preloadedState,
composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)),
);
const persistor = persistStore(store);
const RootProvider = (): React.Node => (
<Provider store={store}>
<PersistGate persistor={persistor} loading={<Loading />}>
<ErrorBoundary>
<Router history={history.getHistoryObject()}>
<Route path="*" component={App} />
</Router>
<Socket />
+ <SQLiteDataHandler />
</ErrorBoundary>
</PersistGate>
</Provider>
);
export default RootProvider;
diff --git a/web/types/worker-types.js b/web/types/worker-types.js
index c71dd1a3d..61dd3ef3c 100644
--- a/web/types/worker-types.js
+++ b/web/types/worker-types.js
@@ -1,139 +1,145 @@
// @flow
import type {
ClientDBStore,
ClientDBStoreOperations,
} from 'lib/types/store-ops-types.js';
// The types of messages sent from app to worker
export const workerRequestMessageTypes = Object.freeze({
PING: 0,
INIT: 1,
GENERATE_DATABASE_ENCRYPTION_KEY: 2,
PROCESS_STORE_OPERATIONS: 3,
GET_CLIENT_STORE: 4,
SET_CURRENT_USER_ID: 5,
GET_CURRENT_USER_ID: 6,
GET_PERSIST_STORAGE_ITEM: 7,
SET_PERSIST_STORAGE_ITEM: 8,
REMOVE_PERSIST_STORAGE_ITEM: 9,
+ CLEAR_SENSITIVE_DATA: 10,
});
export const workerWriteRequests: $ReadOnlyArray<number> = [
workerRequestMessageTypes.PROCESS_STORE_OPERATIONS,
workerRequestMessageTypes.SET_CURRENT_USER_ID,
workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM,
workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM,
];
export type PingWorkerRequestMessage = {
+type: 0,
+text: string,
};
export type InitWorkerRequestMessage = {
+type: 1,
+sqljsFilePath: string,
+sqljsFilename: ?string,
};
export type GenerateDatabaseEncryptionKeyRequestMessage = {
+type: 2,
};
export type ProcessStoreOperationsRequestMessage = {
+type: 3,
+storeOperations: ClientDBStoreOperations,
};
export type GetClientStoreRequestMessage = {
+type: 4,
};
export type SetCurrentUserIDRequestMessage = {
+type: 5,
+userID: string,
};
export type GetCurrentUserIDRequestMessage = {
+type: 6,
};
export type GetPersistStorageItemRequestMessage = {
+type: 7,
+key: string,
};
export type SetPersistStorageItemRequestMessage = {
+type: 8,
+key: string,
+item: string,
};
export type RemovePersistStorageItemRequestMessage = {
+type: 9,
+key: string,
};
+export type ClearSensitiveDataRequestMessage = {
+ +type: 10,
+};
+
export type WorkerRequestMessage =
| PingWorkerRequestMessage
| InitWorkerRequestMessage
| GenerateDatabaseEncryptionKeyRequestMessage
| ProcessStoreOperationsRequestMessage
| GetClientStoreRequestMessage
| SetCurrentUserIDRequestMessage
| GetCurrentUserIDRequestMessage
| GetPersistStorageItemRequestMessage
| SetPersistStorageItemRequestMessage
- | RemovePersistStorageItemRequestMessage;
+ | RemovePersistStorageItemRequestMessage
+ | ClearSensitiveDataRequestMessage;
export type WorkerRequestProxyMessage = {
+id: number,
+message: WorkerRequestMessage,
};
// The types of messages sent from worker to app
export const workerResponseMessageTypes = Object.freeze({
PONG: 0,
CLIENT_STORE: 1,
GET_CURRENT_USER_ID: 2,
GET_PERSIST_STORAGE_ITEM: 3,
});
export type PongWorkerResponseMessage = {
+type: 0,
+text: string,
};
export type ClientStoreResponseMessage = {
+type: 1,
+store: ClientDBStore,
};
export type GetCurrentUserIDResponseMessage = {
+type: 2,
+userID: ?string,
};
export type GetPersistStorageItemResponseMessage = {
+type: 3,
+item: string,
};
export type WorkerResponseMessage =
| PongWorkerResponseMessage
| ClientStoreResponseMessage
| GetCurrentUserIDResponseMessage
| GetPersistStorageItemResponseMessage;
export type WorkerResponseProxyMessage = {
+id?: number,
+message?: WorkerResponseMessage,
+error?: Error,
};
// SharedWorker types
export type SharedWorkerMessageEvent = MessageEvent & {
+ports: $ReadOnlyArray<MessagePort>,
...
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 6:15 AM (1 d, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690457
Default Alt Text
(19 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment