diff --git a/web/database/sqlite-data-handler.js b/web/database/sqlite-data-handler.js index 56b923747..9bccabb46 100644 --- a/web/database/sqlite-data-handler.js +++ b/web/database/sqlite-data-handler.js @@ -1,93 +1,121 @@ // @flow +import localforage from 'localforage'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { convertClientDBReportToClientReportCreationRequest } from 'lib/ops/report-store-ops.js'; import { databaseModule } from './database-module-provider.js'; +import { SQLITE_ENCRYPTION_KEY } from './utils/constants.js'; +import { isDesktopSafari } from './utils/db-utils.js'; +import { + exportKeyToJWK, + generateDatabaseCryptoKey, +} from './utils/worker-crypto-utils.js'; import { useSelector } from '../redux/redux-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; +const isSafari = isDesktopSafari(); + +async function getSafariEncryptionKey(): Promise { + const encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY); + if (encryptionKey) { + return await exportKeyToJWK(encryptionKey); + } + const newEncryptionKey = await generateDatabaseCryptoKey({ + extractable: true, + }); + await localforage.setItem(SQLITE_ENCRYPTION_KEY, newEncryptionKey); + return await exportKeyToJWK(newEncryptionKey); +} + function SQLiteDataHandler(): React.Node { const dispatch = useDispatch(); 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); + let databaseEncryptionKeyJWK = null; + if (isSafari) { + databaseEncryptionKeyJWK = await getSafariEncryptionKey(); + } + await databaseModule.initDBForLoggedInUser( + currentLoggedInUserID, + databaseEncryptionKeyJWK, + ); } if (!rehydrateConcluded) { return; } const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return; } await handleSensitiveData(); if (!currentLoggedInUserID) { return; } const data = await databaseModule.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); if (!data?.store?.drafts && !data?.store?.reports) { return; } const reports = convertClientDBReportToClientReportCreationRequest( data.store.reports, ); dispatch({ type: setClientDBStoreActionType, payload: { drafts: data.store.drafts, reports, }, }); })(); }, [ currentLoggedInUserID, dispatch, handleSensitiveData, rehydrateConcluded, ]); return null; } export { SQLiteDataHandler }; diff --git a/web/database/utils/db-utils.js b/web/database/utils/db-utils.js index cae88c347..3a9ed7ee9 100644 --- a/web/database/utils/db-utils.js +++ b/web/database/utils/db-utils.js @@ -1,48 +1,53 @@ // @flow import { detect as detectBrowser } from 'detect-browser'; import type { QueryExecResult } from 'sql.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { DB_SUPPORTED_BROWSERS, DB_SUPPORTED_OS } from './constants.js'; function parseSQLiteQueryResult(result: QueryExecResult): T[] { const { columns, values } = result; return values.map(rowResult => { const row: any = Object.fromEntries( columns.map((key, index) => [key, rowResult[index]]), ); return row; }); } // NOTE: sql.js has behavior that when there are multiple statements in query // e.g. "statement1; statement2; statement3;" // and statement2 will not return anything, the result will be: // [result1, result3], not [result1, undefined, result3] function parseMultiStatementSQLiteResult( rawResult: $ReadOnlyArray, ): T[][] { return rawResult.map((queryResult: QueryExecResult) => parseSQLiteQueryResult(queryResult), ); } function isSQLiteSupported(currentLoggedInUserID: ?string): boolean { if (!currentLoggedInUserID) { return false; } if (!isDev && (!currentLoggedInUserID || !isStaff(currentLoggedInUserID))) { return false; } const browser = detectBrowser(); return ( DB_SUPPORTED_OS.includes(browser.os) && DB_SUPPORTED_BROWSERS.includes(browser.name) ); } -export { parseMultiStatementSQLiteResult, isSQLiteSupported }; +function isDesktopSafari(): boolean { + const browser = detectBrowser(); + return browser.name === 'safari' && browser.os === 'Mac OS'; +} + +export { parseMultiStatementSQLiteResult, isSQLiteSupported, isDesktopSafari };