diff --git a/web/database/utils/worker-crypto-utils.js b/web/database/utils/worker-crypto-utils.js index ae365b697..b2c3d1c11 100644 --- a/web/database/utils/worker-crypto-utils.js +++ b/web/database/utils/worker-crypto-utils.js @@ -1,64 +1,92 @@ // @flow const ENCRYPTION_ALGORITHM = 'AES-GCM'; +const ENCRYPTION_KEY_USAGES: $ReadOnlyArray = [ + 'encrypt', + 'decrypt', +]; type EncryptedData = { +iv: BufferSource, +ciphertext: Uint8Array, }; function generateDatabaseCryptoKey({ extractable, }: { +extractable: boolean, }): Promise { return crypto.subtle.generateKey( { name: ENCRYPTION_ALGORITHM, length: 256, }, extractable, - ['encrypt', 'decrypt'], + ENCRYPTION_KEY_USAGES, ); } function generateIV(): BufferSource { return crypto.getRandomValues(new Uint8Array(12)); } async function encryptDatabaseFile( data: Uint8Array, key: CryptoKey, ): Promise { 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 { const { ciphertext, iv } = encryptedData; const decrypted = await crypto.subtle.decrypt( { name: ENCRYPTION_ALGORITHM, iv, }, key, ciphertext, ); return new Uint8Array(decrypted); } -export { generateDatabaseCryptoKey, encryptDatabaseFile, decryptDatabaseFile }; +async function exportKeyToJWK( + key: CryptoKey, +): Promise { + return await crypto.subtle.exportKey('jwk', key); +} + +async function importJWKKey( + jwkKey: SubtleCrypto$JsonWebKey, +): Promise { + return await crypto.subtle.importKey( + 'jwk', + jwkKey, + ENCRYPTION_ALGORITHM, + true, + ENCRYPTION_KEY_USAGES, + ); +} + +export { + generateDatabaseCryptoKey, + encryptDatabaseFile, + decryptDatabaseFile, + exportKeyToJWK, + importJWKKey, +}; diff --git a/web/database/utils/worker-crypto-utlis.test.js b/web/database/utils/worker-crypto-utlis.test.js index e7f3d6b45..288d029bd 100644 --- a/web/database/utils/worker-crypto-utlis.test.js +++ b/web/database/utils/worker-crypto-utlis.test.js @@ -1,87 +1,107 @@ // @flow import initSqlJs, { type SqliteDatabase } from 'sql.js'; import { decryptDatabaseFile, encryptDatabaseFile, + exportKeyToJWK, generateDatabaseCryptoKey, + importJWKKey, } 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({ 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({ 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); }); + + it('should export and import key in JWK format', async () => { + // creating new key + const key = await generateDatabaseCryptoKey({ extractable: true }); + const dbContent: Uint8Array = database.export(); + const encryptedData = await encryptDatabaseFile(dbContent, key); + + // exporting and importing key + const exportedKey = await exportKeyToJWK(key); + const importedKey = await importJWKKey(exportedKey); + + // decrypt using re-created on import key + const decrypted = await decryptDatabaseFile(encryptedData, importedKey); + + const SQL = await initSqlJs(); + const newDatabase = new SQL.Database(decrypted); + expect(getSQLiteDBVersion(newDatabase)).toBe(TEST_DB_VERSION); + }); });