Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509998
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/web/database/utils/worker-crypto-utils.js b/web/database/utils/worker-crypto-utils.js
index 920a50181..ae365b697 100644
--- a/web/database/utils/worker-crypto-utils.js
+++ b/web/database/utils/worker-crypto-utils.js
@@ -1,60 +1,64 @@
// @flow
const ENCRYPTION_ALGORITHM = 'AES-GCM';
type EncryptedData = {
+iv: BufferSource,
+ciphertext: Uint8Array,
};
-function generateDatabaseCryptoKey(): Promise<CryptoKey> {
+function generateDatabaseCryptoKey({
+ extractable,
+}: {
+ +extractable: boolean,
+}): Promise<CryptoKey> {
return crypto.subtle.generateKey(
{
name: ENCRYPTION_ALGORITHM,
length: 256,
},
- false,
+ extractable,
['encrypt', 'decrypt'],
);
}
function generateIV(): BufferSource {
return crypto.getRandomValues(new Uint8Array(12));
}
async function encryptDatabaseFile(
data: Uint8Array,
key: CryptoKey,
): Promise<EncryptedData> {
const iv = generateIV();
const ciphertext = await crypto.subtle.encrypt(
{
name: ENCRYPTION_ALGORITHM,
iv: iv,
},
key,
data,
);
return {
ciphertext: new Uint8Array(ciphertext),
iv,
};
}
async function decryptDatabaseFile(
encryptedData: EncryptedData,
key: CryptoKey,
): Promise<Uint8Array> {
const { ciphertext, iv } = encryptedData;
const decrypted = await crypto.subtle.decrypt(
{
name: ENCRYPTION_ALGORITHM,
iv,
},
key,
ciphertext,
);
return new Uint8Array(decrypted);
}
export { generateDatabaseCryptoKey, encryptDatabaseFile, decryptDatabaseFile };
diff --git a/web/database/utils/worker-crypto-utlis.test.js b/web/database/utils/worker-crypto-utlis.test.js
index 6a0d22a40..e7f3d6b45 100644
--- a/web/database/utils/worker-crypto-utlis.test.js
+++ b/web/database/utils/worker-crypto-utlis.test.js
@@ -1,85 +1,87 @@
// @flow
import initSqlJs, { type SqliteDatabase } from 'sql.js';
import {
decryptDatabaseFile,
encryptDatabaseFile,
generateDatabaseCryptoKey,
} from './worker-crypto-utils.js';
import { getSQLiteDBVersion } from '../queries/db-queries.js';
const TEST_DB_VERSION = 5;
const TAG_LENGTH = 16;
const IV_LENGTH = 12;
// calling export on empty schema will return empty buffer
function setUpMockDb(database: SqliteDatabase) {
database.exec(`
CREATE TABLE test_table (
key TEXT UNIQUE PRIMARY KEY NOT NULL,
value TEXT NOT NULL
);
`);
database.exec(`
INSERT INTO test_table VALUES ("key_1", "value 1");
`);
database.exec(`PRAGMA user_version=${TEST_DB_VERSION};`);
}
describe('database encryption utils', () => {
let database;
let cryptoKey;
beforeAll(async () => {
const SQL = await initSqlJs();
database = new SQL.Database();
setUpMockDb(database);
- cryptoKey = await generateDatabaseCryptoKey();
+ cryptoKey = await generateDatabaseCryptoKey({ extractable: false });
});
it('should encrypt database content', async () => {
const dbContent: Uint8Array = database.export();
const { ciphertext, iv } = await encryptDatabaseFile(dbContent, cryptoKey);
expect(iv.byteLength).toBe(IV_LENGTH);
expect(ciphertext.length).toBe(dbContent.length + TAG_LENGTH);
});
it('is decryptable', async () => {
const dbContent: Uint8Array = database.export();
const encryptedData = await encryptDatabaseFile(dbContent, cryptoKey);
const decrypted = await decryptDatabaseFile(encryptedData, cryptoKey);
expect(decrypted).toEqual(dbContent);
});
it('should fail with wrong key', async () => {
const dbContent: Uint8Array = database.export();
const encryptedData = await encryptDatabaseFile(dbContent, cryptoKey);
- const newCryptoKey = await generateDatabaseCryptoKey();
+ const newCryptoKey = await generateDatabaseCryptoKey({
+ extractable: false,
+ });
expect(decryptDatabaseFile(encryptedData, newCryptoKey)).rejects.toThrow();
});
it('should fail with wrong content', async () => {
const dbContent: Uint8Array = database.export();
const encryptedData = await encryptDatabaseFile(dbContent, cryptoKey);
const randomizedEncryptedData = {
...encryptedData,
ciphertext: encryptedData.ciphertext.map(uint => uint ^ 1),
};
expect(
decryptDatabaseFile(randomizedEncryptedData, cryptoKey),
).rejects.toThrow();
});
it('should create database with decrypted content', async () => {
const dbContent: Uint8Array = database.export();
const encryptedData = await encryptDatabaseFile(dbContent, cryptoKey);
const decrypted = await decryptDatabaseFile(encryptedData, cryptoKey);
const SQL = await initSqlJs();
const newDatabase = new SQL.Database(decrypted);
expect(getSQLiteDBVersion(newDatabase)).toBe(TEST_DB_VERSION);
});
});
diff --git a/web/database/worker/db-worker.js b/web/database/worker/db-worker.js
index f155ca48e..4cdf44baa 100644
--- a/web/database/worker/db-worker.js
+++ b/web/database/worker/db-worker.js
@@ -1,302 +1,302 @@
// @flow
import localforage from 'localforage';
import initSqlJs, { type SqliteDatabase } from 'sql.js';
import type { ClientDBReportStoreOperation } from 'lib/ops/report-store-ops.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 { migrate, 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 {
getAllReports,
removeAllReports,
removeReports,
updateReport,
} from '../queries/report-queries.js';
import {
getPersistStorageItem,
removePersistStorageItem,
setPersistStorageItem,
} from '../queries/storage-engine-queries.js';
import {
CURRENT_USER_ID_KEY,
localforageConfig,
SQLITE_CONTENT,
SQLITE_ENCRYPTION_KEY,
} from '../utils/constants.js';
import {
decryptDatabaseFile,
encryptDatabaseFile,
generateDatabaseCryptoKey,
} from '../utils/worker-crypto-utils.js';
localforage.config(localforageConfig);
let sqliteDb: ?SqliteDatabase = null;
let encryptionKey: ?CryptoKey = null;
let persistNeeded: boolean = false;
let persistInProgress: boolean = false;
async function initDatabase(sqljsFilePath: string, sqljsFilename: ?string) {
encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY);
if (!encryptionKey) {
- const cryptoKey = await generateDatabaseCryptoKey();
+ const cryptoKey = await generateDatabaseCryptoKey({ extractable: false });
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',
);
migrate(sqliteDb);
} else {
sqliteDb = new SQL.Database();
setupSQLiteDB(sqliteDb);
console.info('Creating fresh database');
}
}
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 processReportStoreOperations(
operations: $ReadOnlyArray<ClientDBReportStoreOperation>,
) {
if (!sqliteDb) {
throw new Error('Database not initialized');
}
for (const operation: ClientDBReportStoreOperation of operations) {
if (operation.type === 'remove_all_reports') {
removeAllReports(sqliteDb);
} else if (operation.type === 'remove_reports') {
const { ids } = operation.payload;
removeReports(sqliteDb, ids);
} else if (operation.type === 'replace_report') {
const { id, report } = operation.payload;
updateReport(sqliteDb, id, report);
} else {
throw new Error('Unsupported report operation');
}
}
}
function getClientStore(): ClientDBStore {
if (!sqliteDb) {
throw new Error('Database not initialized');
}
return {
drafts: getAllDrafts(sqliteDb),
messages: [],
threads: [],
messageStoreThreads: [],
reports: getAllReports(sqliteDb),
};
}
async function persist() {
persistInProgress = true;
if (!sqliteDb) {
persistInProgress = false;
throw new Error('Database not initialized');
}
if (!encryptionKey) {
encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY);
}
while (persistNeeded) {
persistNeeded = false;
const dbData = sqliteDb.export();
if (!encryptionKey) {
persistInProgress = false;
throw new Error('Encryption key is missing');
}
const encryptedData = await encryptDatabaseFile(dbData, encryptionKey);
await localforage.setItem(SQLITE_CONTENT, encryptedData);
}
persistInProgress = false;
}
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();
+ const cryptoKey = await generateDatabaseCryptoKey({ extractable: false });
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, reportStoreOperations } =
message.storeOperations;
if (draftStoreOperations) {
processDraftStoreOperations(draftStoreOperations);
}
if (reportStoreOperations) {
processReportStoreOperations(reportStoreOperations);
}
} 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);
}
persistNeeded = true;
if (!persistInProgress) {
persist();
}
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: 'Request without identifier',
});
}
try {
const result = await processAppRequest(message);
port.postMessage({
id,
message: result,
});
} catch (e) {
port.postMessage({
id,
error: e.message,
});
}
};
}
self.addEventListener('connect', connectHandler);
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 10:12 AM (17 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690762
Default Alt Text
(13 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment