Page MenuHomePhabricator

No OneTemporary

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

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)

Event Timeline