Page MenuHomePhabricator

No OneTemporary

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

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)

Event Timeline