diff --git a/keyserver/src/creators/one-time-keys-creator.js b/keyserver/src/creators/one-time-keys-creator.js deleted file mode 100644 index 86badc15f..000000000 --- a/keyserver/src/creators/one-time-keys-creator.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow - -import { dbQuery, SQL } from '../database/database.js'; -import type { Viewer } from '../session/viewer.js'; - -async function saveOneTimeKeys( - viewer: Viewer, - oneTimeKeys: $ReadOnlyArray, -): Promise { - if (oneTimeKeys.length === 0) { - return; - } - - const insertData = oneTimeKeys.map(oneTimeKey => [ - viewer.session, - oneTimeKey, - ]); - - const query = SQL` - INSERT INTO one_time_keys(session, one_time_key) - VALUES ${insertData} - `; - await dbQuery(query); -} - -export { saveOneTimeKeys }; diff --git a/keyserver/src/database/migration-config.js b/keyserver/src/database/migration-config.js index 65293e05d..147b89530 100644 --- a/keyserver/src/database/migration-config.js +++ b/keyserver/src/database/migration-config.js @@ -1,871 +1,872 @@ // @flow import fs from 'fs'; import bots from 'lib/facts/bots.js'; import genesis from 'lib/facts/genesis.js'; import { policyTypes } from 'lib/facts/policies.js'; import { specialRoles } from 'lib/permissions/special-roles.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { permissionsToRemoveInMigration } from 'lib/utils/migration-utils.js'; import { dbQuery, SQL } from '../database/database.js'; import { processMessagesInDBForSearch } from '../database/search-utils.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { createScriptViewer } from '../session/scripts.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; import { updateRolesAndPermissionsForAllThreads } from '../updaters/thread-permission-updaters.js'; import { updateThread } from '../updaters/thread-updaters.js'; import { ensureUserCredentials } from '../user/checks.js'; import { createPickledOlmAccount, publishPrekeysToIdentity, } from '../utils/olm-utils.js'; import { synchronizeInviteLinksWithBlobs } from '../utils/synchronize-invite-links-with-blobs.js'; const botViewer = createScriptViewer(bots.commbot.userID); const migrations: $ReadOnlyMap Promise> = new Map([ [ 0, async () => { await makeSureBaseRoutePathExists('facts/commapp_url.json'); await makeSureBaseRoutePathExists('facts/squadcal_url.json'); }, ], [ 1, async () => { try { await fs.promises.unlink('facts/url.json'); } catch {} }, ], [ 2, async () => { await fixBaseRoutePathForLocalhost('facts/commapp_url.json'); await fixBaseRoutePathForLocalhost('facts/squadcal_url.json'); }, ], [3, updateRolesAndPermissionsForAllThreads], [ 4, async () => { await dbQuery(SQL`ALTER TABLE uploads ADD INDEX container (container)`); }, ], [ 5, async () => { await dbQuery(SQL` ALTER TABLE cookies ADD device_id varchar(255) DEFAULT NULL, ADD public_key varchar(255) DEFAULT NULL, ADD social_proof varchar(255) DEFAULT NULL; `); }, ], [ 7, async () => { await dbQuery( SQL` ALTER TABLE users DROP COLUMN IF EXISTS public_key, MODIFY hash char(60) COLLATE utf8mb4_bin DEFAULT NULL; ALTER TABLE sessions DROP COLUMN IF EXISTS public_key; `, { multipleStatements: true }, ); }, ], [ 8, async () => { await dbQuery( SQL` ALTER TABLE users ADD COLUMN IF NOT EXISTS ethereum_address char(42) DEFAULT NULL; `, ); }, ], [ 9, async () => { await dbQuery( SQL` ALTER TABLE messages ADD COLUMN IF NOT EXISTS target_message bigint(20) DEFAULT NULL; ALTER TABLE messages ADD INDEX target_message (target_message); `, { multipleStatements: true }, ); }, ], [ 10, async () => { await dbQuery(SQL` CREATE TABLE IF NOT EXISTS policy_acknowledgments ( user bigint(20) NOT NULL, policy varchar(255) NOT NULL, date bigint(20) NOT NULL, confirmed tinyint(1) UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (user, policy) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); }, ], [ 11, async () => { const time = Date.now(); await dbQuery(SQL` INSERT IGNORE INTO policy_acknowledgments (policy, user, date, confirmed) SELECT ${policyTypes.tosAndPrivacyPolicy}, id, ${time}, 1 FROM users `); }, ], [ 12, async () => { await dbQuery(SQL` CREATE TABLE IF NOT EXISTS siwe_nonces ( nonce char(17) NOT NULL, creation_time bigint(20) NOT NULL, PRIMARY KEY (nonce) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); }, ], [ 13, async () => { await Promise.all([ writeSquadCalRoute('facts/squadcal_url.json'), moveToNonApacheConfig('facts/commapp_url.json', '/comm/'), moveToNonApacheConfig('facts/landing_url.json', '/commlanding/'), ]); }, ], [ 14, async () => { await dbQuery(SQL` ALTER TABLE cookies MODIFY COLUMN social_proof mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL; `); }, ], [ 15, async () => { await dbQuery( SQL` ALTER TABLE uploads ADD COLUMN IF NOT EXISTS thread bigint(20) DEFAULT NULL, ADD INDEX IF NOT EXISTS thread (thread); UPDATE uploads SET thread = ( SELECT thread FROM messages WHERE messages.id = uploads.container ); `, { multipleStatements: true }, ); }, ], [ 16, async () => { await dbQuery( SQL` ALTER TABLE cookies DROP COLUMN IF EXISTS public_key; ALTER TABLE cookies ADD COLUMN IF NOT EXISTS signed_identity_keys mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL; `, { multipleStatements: true }, ); }, ], [ 17, async () => { await dbQuery( SQL` ALTER TABLE cookies DROP INDEX device_token, DROP INDEX user_device_token; ALTER TABLE cookies MODIFY device_token mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, ADD UNIQUE KEY device_token (device_token(512)), ADD KEY user_device_token (user,device_token(512)); `, { multipleStatements: true }, ); }, ], [18, updateRolesAndPermissionsForAllThreads], [19, updateRolesAndPermissionsForAllThreads], [ 20, async () => { await dbQuery(SQL` ALTER TABLE threads ADD COLUMN IF NOT EXISTS avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL; `); }, ], [ 21, async () => { await dbQuery(SQL` ALTER TABLE reports DROP INDEX IF EXISTS user, ADD INDEX IF NOT EXISTS user_type_platform_creation_time (user, type, platform, creation_time); `); }, ], [ 22, async () => { await dbQuery( SQL` ALTER TABLE cookies MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE entries MODIFY creator varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE focused MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE memberships MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE messages MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE notifications MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE reports MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE revisions MODIFY author varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE sessions MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE threads MODIFY creator varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE updates MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE uploads MODIFY uploader varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE users MODIFY id varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE relationships_undirected MODIFY user1 varchar(255) CHARSET latin1 COLLATE latin1_bin, MODIFY user2 varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE relationships_directed MODIFY user1 varchar(255) CHARSET latin1 COLLATE latin1_bin, MODIFY user2 varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE user_messages MODIFY recipient varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE settings MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; ALTER TABLE policy_acknowledgments MODIFY user varchar(255) CHARSET latin1 COLLATE latin1_bin; `, { multipleStatements: true }, ); }, ], [23, updateRolesAndPermissionsForAllThreads], [24, updateRolesAndPermissionsForAllThreads], [ 25, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS message_search ( original_message_id bigint(20) NOT NULL, message_id bigint(20) NOT NULL, processed_content mediumtext COLLATE utf8mb4_bin ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; ALTER TABLE message_search ADD PRIMARY KEY (original_message_id), ADD FULLTEXT INDEX processed_content (processed_content); `, { multipleStatements: true }, ); }, ], [26, processMessagesInDBForSearch], [ 27, async () => { await dbQuery(SQL` ALTER TABLE messages ADD COLUMN IF NOT EXISTS pinned tinyint(1) UNSIGNED NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS pin_time bigint(20) DEFAULT NULL, ADD INDEX IF NOT EXISTS thread_pinned (thread, pinned); `); }, ], [ 28, async () => { await dbQuery(SQL` ALTER TABLE threads ADD COLUMN IF NOT EXISTS pinned_count int UNSIGNED NOT NULL DEFAULT 0; `); }, ], [29, updateRolesAndPermissionsForAllThreads], [ 30, async () => { await dbQuery(SQL`DROP TABLE versions;`); }, ], [ 31, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS invite_links ( id bigint(20) NOT NULL, name varchar(255) CHARSET latin1 NOT NULL, \`primary\` tinyint(1) UNSIGNED NOT NULL DEFAULT 0, role bigint(20) NOT NULL, community bigint(20) NOT NULL, expiration_time bigint(20), limit_of_uses int UNSIGNED, number_of_uses int UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE invite_links ADD PRIMARY KEY (id), ADD UNIQUE KEY (name), ADD INDEX community_primary (community, \`primary\`); `, { multipleStatements: true }, ); }, ], [ 32, async () => { await dbQuery(SQL` UPDATE messages SET target_message = JSON_VALUE(content, "$.sourceMessageID") WHERE type = ${messageTypes.SIDEBAR_SOURCE}; `); }, ], [ 33, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS olm_sessions ( cookie_id bigint(20) NOT NULL, is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickled_olm_session text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; ALTER TABLE olm_sessions ADD PRIMARY KEY (cookie_id, is_content); `, { multipleStatements: true }, ); }, ], [ 34, async () => { await dbQuery( SQL` CREATE TABLE IF NOT EXISTS olm_accounts ( is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickling_key text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, pickled_olm_account text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; ALTER TABLE olm_accounts ADD PRIMARY KEY (is_content); `, { multipleStatements: true }, ); }, ], [ 35, async () => { await createOlmAccounts(); }, ], [36, updateRolesAndPermissionsForAllThreads], [ 37, async () => { await dbQuery( SQL` DELETE FROM olm_accounts; DELETE FROM olm_sessions; `, { multipleStatements: true }, ); await createOlmAccounts(); }, ], [ 38, async () => { const [result] = await dbQuery(SQL` SELECT t.id FROM threads t INNER JOIN memberships m ON m.thread = t.id AND m.role > 0 INNER JOIN users u ON u.id = m.user WHERE t.type = ${threadTypes.PRIVATE} AND t.name = u.ethereum_address `); const threadIDs = result.map(({ id }) => id.toString()); while (threadIDs.length > 0) { // Batch 10 updateThread calls at a time const batch = threadIDs.splice(0, 10); await Promise.all( batch.map(threadID => updateThread( botViewer, { threadID, changes: { name: '', }, }, { silenceMessages: true, ignorePermissions: true, }, ), ), ); } }, ], [39, ensureUserCredentials], [ 40, // Tokens from identity service are 512 characters long () => dbQuery( SQL` ALTER TABLE metadata MODIFY COLUMN data VARCHAR(1023) `, ), ], [ 41, () => dbQuery( SQL` ALTER TABLE memberships DROP INDEX user, ADD KEY user_role_thread (user, role, thread) `, ), ], [ 42, async () => { await dbQuery(SQL` ALTER TABLE roles ADD UNIQUE KEY thread_name (thread, name); `); }, ], [ 43, () => dbQuery( SQL` UPDATE threads SET pinned_count = ( SELECT COUNT(*) FROM messages WHERE messages.thread = threads.id AND messages.pinned = 1 ) `, ), ], [ 44, async () => { const { SIDEBAR_SOURCE, TOGGLE_PIN } = messageTypes; const [result] = await dbQuery(SQL` SELECT m1.thread FROM messages m1 LEFT JOIN messages m2 ON m2.id = m1.target_message WHERE m1.type = ${SIDEBAR_SOURCE} AND m2.type = ${TOGGLE_PIN} `); const threadIDs = new Set(); for (const row of result) { threadIDs.add(row.thread.toString()); } await Promise.all( [...threadIDs].map(threadID => deleteThread(botViewer, { threadID }, { ignorePermissions: true }), ), ); }, ], [ 45, () => dbQuery( SQL` ALTER TABLE uploads CHARSET utf8mb4 COLLATE utf8mb4_bin, MODIFY COLUMN type varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, MODIFY COLUMN filename varchar(255) CHARSET utf8mb4 COLLATE utf8mb4_bin NOT NULL, MODIFY COLUMN mime varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, MODIFY COLUMN secret varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL; `, ), ], [ 46, async () => { try { const [content, notif] = await Promise.all([ fetchOlmAccount('content'), fetchOlmAccount('notifications'), ]); await publishPrekeysToIdentity(content.account, notif.account); } catch (e) { console.warn('Encountered error while trying to publish prekeys', e); if (process.env.NODE_ENV !== 'development') { throw e; } } }, ], [ 47, () => dbQuery(SQL`ALTER TABLE cookies MODIFY COLUMN hash char(64) NOT NULL`), ], [ 48, async () => { const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`; const query = SQL` UPDATE memberships mm LEFT JOIN ( SELECT m.thread, MAX(m.id) AS message FROM messages m WHERE m.type != ${messageTypes.CREATE_SUB_THREAD} AND m.thread = ${genesis().id} GROUP BY m.thread ) all_users_query ON mm.thread = all_users_query.thread LEFT JOIN ( SELECT m.thread, stm.user, MAX(m.id) AS message FROM messages m LEFT JOIN memberships stm ON m.type = ${ messageTypes.CREATE_SUB_THREAD } AND stm.thread = m.content WHERE JSON_EXTRACT(stm.permissions, ${visibleExtractString}) IS TRUE AND m.thread = ${genesis().id} GROUP BY m.thread, stm.user ) last_subthread_message_for_user_query ON mm.thread = last_subthread_message_for_user_query.thread AND mm.user = last_subthread_message_for_user_query.user SET mm.last_message = GREATEST(COALESCE(all_users_query.message, 0), COALESCE(last_subthread_message_for_user_query.message, 0)) WHERE mm.thread = ${genesis().id}; `; await dbQuery(query); }, ], [ 49, async () => { if (isDockerEnvironment()) { return; } const defaultCorsConfig = { domain: 'http://localhost:3000', }; await writeJSONToFile(defaultCorsConfig, 'facts/webapp_cors.json'); }, ], [ 50, async () => { await moveToNonApacheConfig('facts/webapp_url.json', '/webapp/'); await moveToNonApacheConfig('facts/keyserver_url.json', '/keyserver/'); }, ], [ 51, async () => { if (permissionsToRemoveInMigration.length === 0) { return; } const setClause = SQL`permissions = JSON_REMOVE(permissions, ${permissionsToRemoveInMigration.map( path => `$.${path}`, )})`; const updateQuery = SQL` UPDATE roles r LEFT JOIN threads t ON t.id = r.thread `; updateQuery.append(SQL`SET `.append(setClause)); updateQuery.append(SQL` WHERE r.name != 'Admins' AND (t.type = ${threadTypes.COMMUNITY_ROOT} OR t.type = ${threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT}) `); await dbQuery(updateQuery); }, ], [ 52, async () => { await dbQuery( SQL` ALTER TABLE roles ADD COLUMN IF NOT EXISTS special_role tinyint(2) UNSIGNED DEFAULT NULL `, ); await updateRolesAndPermissionsForAllThreads(); }, ], [ 53, async () => dbQuery(SQL` ALTER TABLE invite_links ADD COLUMN blob_holder char(36) CHARSET latin1 `), ], [ 54, async () => { await dbQuery( SQL` ALTER TABLE roles ADD COLUMN IF NOT EXISTS special_role tinyint(2) UNSIGNED DEFAULT NULL, DROP KEY IF EXISTS thread, ADD KEY IF NOT EXISTS thread_special_role (thread, special_role); UPDATE roles r JOIN threads t ON r.id = t.default_role SET r.special_role = ${specialRoles.DEFAULT_ROLE}; `, { multipleStatements: true }, ); }, ], [ 55, async () => { await dbQuery( SQL` ALTER TABLE threads DROP COLUMN IF EXISTS default_role `, ); }, ], [ 56, async () => { await dbQuery( SQL` UPDATE roles SET special_role = ${specialRoles.ADMIN_ROLE} WHERE name = 'Admins' `, ); }, ], [57, synchronizeInviteLinksWithBlobs], [ 58, async () => { await dbQuery( SQL` ALTER TABLE updates MODIFY \`key\` varchar(255) CHARSET latin1 COLLATE latin1_bin `, ); }, ], + [59, () => dbQuery(SQL`DROP TABLE one_time_keys`)], ]); const newDatabaseVersion: number = Math.max(...migrations.keys()); async function writeJSONToFile(data: any, filePath: string): Promise { console.warn(`updating ${filePath} to ${JSON.stringify(data)}`); const fileHandle = await fs.promises.open(filePath, 'w'); await fileHandle.writeFile(JSON.stringify(data, null, ' '), 'utf8'); await fileHandle.close(); } async function makeSureBaseRoutePathExists(filePath: string): Promise { let readFile, json; try { readFile = await fs.promises.open(filePath, 'r'); const contents = await readFile.readFile('utf8'); json = JSON.parse(contents); } catch { return; } finally { if (readFile) { await readFile.close(); } } if (json.baseRoutePath) { return; } let baseRoutePath; if (json.baseDomain === 'http://localhost') { baseRoutePath = json.basePath; } else if (filePath.endsWith('commapp_url.json')) { baseRoutePath = '/commweb/'; } else { baseRoutePath = '/'; } const newJSON = { ...json, baseRoutePath }; console.warn(`updating ${filePath} to ${JSON.stringify(newJSON)}`); await writeJSONToFile(newJSON, filePath); } async function fixBaseRoutePathForLocalhost(filePath: string): Promise { let readFile, json; try { readFile = await fs.promises.open(filePath, 'r'); const contents = await readFile.readFile('utf8'); json = JSON.parse(contents); } catch { return; } finally { if (readFile) { await readFile.close(); } } if (json.baseDomain !== 'http://localhost') { return; } const baseRoutePath = '/'; json = { ...json, baseRoutePath }; console.warn(`updating ${filePath} to ${JSON.stringify(json)}`); await writeJSONToFile(json, filePath); } async function moveToNonApacheConfig( filePath: string, routePath: string, ): Promise { if (isDockerEnvironment()) { return; } // Since the non-Apache config is so opinionated, just write expected config const newJSON = { baseDomain: 'http://localhost:3000', basePath: routePath, baseRoutePath: routePath, https: false, proxy: 'none', }; await writeJSONToFile(newJSON, filePath); } async function writeSquadCalRoute(filePath: string): Promise { if (isDockerEnvironment()) { return; } // Since the non-Apache config is so opinionated, just write expected config const newJSON = { baseDomain: 'http://localhost:3000', basePath: '/comm/', baseRoutePath: '/', https: false, proxy: 'apache', }; await writeJSONToFile(newJSON, filePath); } async function createOlmAccounts() { const [pickledContentAccount, pickledNotificationsAccount] = await Promise.all([createPickledOlmAccount(), createPickledOlmAccount()]); await dbQuery( SQL` INSERT INTO olm_accounts (is_content, version, pickling_key, pickled_olm_account) VALUES ( TRUE, 0, ${pickledContentAccount.picklingKey}, ${pickledContentAccount.pickledAccount} ), ( FALSE, 0, ${pickledNotificationsAccount.picklingKey}, ${pickledNotificationsAccount.pickledAccount} ); `, ); } function isDockerEnvironment(): boolean { return !!process.env.COMM_DATABASE_HOST; } export { migrations, newDatabaseVersion, createOlmAccounts }; diff --git a/keyserver/src/database/setup-db.js b/keyserver/src/database/setup-db.js index 6c18ef2e2..ebd2ad676 100644 --- a/keyserver/src/database/setup-db.js +++ b/keyserver/src/database/setup-db.js @@ -1,494 +1,489 @@ // @flow import bots from 'lib/facts/bots.js'; import genesis from 'lib/facts/genesis.js'; import { usernameMaxLength } from 'lib/shared/account-utils.js'; import { sortUserIDs } from 'lib/shared/relationship-utils.js'; import { undirectedStatus } from 'lib/types/relationship-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { createThread } from '../creators/thread-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { updateDBVersion } from '../database/db-version.js'; import { newDatabaseVersion, createOlmAccounts, } from '../database/migration-config.js'; import { createScriptViewer } from '../session/scripts.js'; import { ensureUserCredentials } from '../user/checks.js'; import { thisKeyserverAdmin } from '../user/identity.js'; import { verifyUserLoggedIn } from '../user/login.js'; async function setupDB() { await ensureUserCredentials(); await createTables(); await createOlmAccounts(); await verifyUserLoggedIn(); await createUsers(); await createThreads(); await setUpMetadataTable(); } async function createTables() { await dbQuery( SQL` CREATE TABLE cookies ( id bigint(20) NOT NULL, hash char(64) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin DEFAULT NULL, platform varchar(255) DEFAULT NULL, creation_time bigint(20) NOT NULL, last_used bigint(20) NOT NULL, device_token mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, versions json DEFAULT NULL, device_id varchar(255) DEFAULT NULL, signed_identity_keys mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, social_proof mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, \`primary\` TINYINT(1) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE days ( id bigint(20) NOT NULL, date date NOT NULL, thread bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE entries ( id bigint(20) NOT NULL, day bigint(20) NOT NULL, text mediumtext COLLATE utf8mb4_bin NOT NULL, creator varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, creation_time bigint(20) NOT NULL, last_update bigint(20) NOT NULL, deleted tinyint(1) UNSIGNED NOT NULL, creation varchar(255) COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE focused ( user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, session bigint(20) NOT NULL, thread bigint(20) NOT NULL, time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE ids ( id bigint(20) NOT NULL, table_name varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE memberships ( thread bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, role bigint(20) NOT NULL, permissions json DEFAULT NULL, permissions_for_children json DEFAULT NULL, creation_time bigint(20) NOT NULL, subscription json NOT NULL, last_message bigint(20) NOT NULL DEFAULT 0, last_read_message bigint(20) NOT NULL DEFAULT 0, sender tinyint(1) UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE messages ( id bigint(20) NOT NULL, thread bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, type tinyint(3) UNSIGNED NOT NULL, content mediumtext COLLATE utf8mb4_bin, time bigint(20) NOT NULL, creation varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, target_message bigint(20) DEFAULT NULL, pinned tinyint(1) UNSIGNED NOT NULL DEFAULT 0, pin_time bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE notifications ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, thread bigint(20) DEFAULT NULL, message bigint(20) DEFAULT NULL, collapse_key varchar(255) DEFAULT NULL, delivery json NOT NULL, rescinded tinyint(1) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE reports ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, type tinyint(3) UNSIGNED NOT NULL, platform varchar(255) NOT NULL, report json NOT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE revisions ( id bigint(20) NOT NULL, entry bigint(20) NOT NULL, author varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, text mediumtext COLLATE utf8mb4_bin NOT NULL, creation_time bigint(20) NOT NULL, session bigint(20) NOT NULL, last_update bigint(20) NOT NULL, deleted tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE roles ( id bigint(20) NOT NULL, thread bigint(20) NOT NULL, name varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, permissions json NOT NULL, creation_time bigint(20) NOT NULL, special_role tinyint(2) UNSIGNED DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE sessions ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, cookie bigint(20) NOT NULL, query json NOT NULL, creation_time bigint(20) NOT NULL, last_update bigint(20) NOT NULL, last_validated bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE threads ( id bigint(20) NOT NULL, type tinyint(3) NOT NULL, name varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, description mediumtext COLLATE utf8mb4_bin, parent_thread_id bigint(20) DEFAULT NULL, containing_thread_id bigint(20) DEFAULT NULL, community bigint(20) DEFAULT NULL, depth int UNSIGNED NOT NULL DEFAULT 0, creator varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, creation_time bigint(20) NOT NULL, color char(6) COLLATE utf8mb4_bin NOT NULL, source_message bigint(20) DEFAULT NULL UNIQUE, replies_count int UNSIGNED NOT NULL DEFAULT 0, avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, pinned_count int UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE updates ( id bigint(20) NOT NULL, user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, type tinyint(3) UNSIGNED NOT NULL, \`key\` varchar(255) CHARSET latin1 COLLATE latin1_bin DEFAULT NULL, updater bigint(20) DEFAULT NULL, target bigint(20) DEFAULT NULL, content mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE uploads ( id bigint(20) NOT NULL, thread bigint(20) DEFAULT NULL, uploader varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, container bigint(20) DEFAULT NULL, type varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, filename varchar(255) NOT NULL, mime varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, content longblob NOT NULL, secret varchar(255) CHARSET latin1 COLLATE latin1_swedish_ci NOT NULL, creation_time bigint(20) NOT NULL, extra json DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE users ( id varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, username varchar(${usernameMaxLength}) COLLATE utf8mb4_bin NOT NULL, hash char(60) COLLATE utf8mb4_bin DEFAULT NULL, avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, ethereum_address char(42) DEFAULT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE relationships_undirected ( user1 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, user2 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, status tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE relationships_directed ( user1 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, user2 varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, status tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; - CREATE TABLE one_time_keys ( - session bigint(20) NOT NULL, - one_time_key char(43) NOT NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - CREATE TABLE user_messages ( recipient varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, thread bigint(20) NOT NULL, message bigint(20) NOT NULL, time bigint(20) NOT NULL, data mediumtext COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE settings ( user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, name varchar(255) NOT NULL, data mediumtext COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE metadata ( name varchar(255) NOT NULL, data varchar(1023) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE policy_acknowledgments ( user varchar(255) CHARSET latin1 COLLATE latin1_bin NOT NULL, policy varchar(255) NOT NULL, date bigint(20) NOT NULL, confirmed tinyint(1) UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE siwe_nonces ( nonce char(17) NOT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE message_search ( original_message_id bigint(20) NOT NULL, message_id bigint(20) NOT NULL, processed_content mediumtext COLLATE utf8mb4_bin ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE invite_links ( id bigint(20) NOT NULL, name varchar(255) CHARSET latin1 NOT NULL, \`primary\` tinyint(1) UNSIGNED NOT NULL DEFAULT 0, role bigint(20) NOT NULL, community bigint(20) NOT NULL, expiration_time bigint(20), limit_of_uses int UNSIGNED, number_of_uses int UNSIGNED NOT NULL DEFAULT 0, blob_holder char(36) CHARSET latin1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE olm_sessions ( cookie_id bigint(20) NOT NULL, is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickled_olm_session text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; CREATE TABLE olm_accounts ( is_content tinyint(1) NOT NULL, version bigint(20) NOT NULL, pickling_key text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, pickled_olm_account text CHARACTER SET latin1 COLLATE latin1_bin NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; ALTER TABLE cookies ADD PRIMARY KEY (id), ADD UNIQUE KEY device_token (device_token(512)), ADD KEY user_device_token (user,device_token(512)); ALTER TABLE days ADD PRIMARY KEY (id), ADD UNIQUE KEY date_thread (date,thread) USING BTREE; ALTER TABLE entries ADD PRIMARY KEY (id), ADD UNIQUE KEY creator_creation (creator,creation), ADD KEY day (day); ALTER TABLE focused ADD UNIQUE KEY user_cookie_thread (user,session,thread), ADD KEY thread_user (thread,user); ALTER TABLE ids ADD PRIMARY KEY (id); ALTER TABLE memberships ADD UNIQUE KEY thread_user (thread, user) USING BTREE, ADD KEY role (role) USING BTREE, ADD KEY user_role_thread (user, role, thread); ALTER TABLE messages ADD PRIMARY KEY (id), ADD UNIQUE KEY user_creation (user,creation), ADD KEY thread (thread), ADD INDEX target_message (target_message), ADD INDEX thread_pinned (thread, pinned); ALTER TABLE notifications ADD PRIMARY KEY (id), ADD KEY rescinded_user_collapse_key (rescinded,user,collapse_key) USING BTREE, ADD KEY thread (thread), ADD KEY rescinded_user_thread_message (rescinded,user,thread,message) USING BTREE; ALTER TABLE notifications ADD INDEX user (user); ALTER TABLE reports ADD PRIMARY KEY (id), ADD INDEX user_type_platform_creation_time (user, type, platform, creation_time); ALTER TABLE revisions ADD PRIMARY KEY (id), ADD KEY entry (entry); ALTER TABLE roles ADD PRIMARY KEY (id), ADD KEY thread_special_role (thread, special_role), ADD UNIQUE KEY thread_name (thread, name); ALTER TABLE sessions ADD PRIMARY KEY (id), ADD KEY user (user); ALTER TABLE threads ADD PRIMARY KEY (id), ADD INDEX parent_thread_id (parent_thread_id), ADD INDEX containing_thread_id (containing_thread_id), ADD INDEX community (community); ALTER TABLE updates ADD PRIMARY KEY (id), ADD INDEX user_time (user,time), ADD INDEX target_time (target, time), ADD INDEX user_key_target_type_time (user, \`key\`, target, type, time), ADD INDEX user_key_type_time (user, \`key\`, type, time), ADD INDEX user_key_time (user, \`key\`, time); ALTER TABLE uploads ADD PRIMARY KEY (id), ADD INDEX container (container), ADD INDEX thread (thread); ALTER TABLE users ADD PRIMARY KEY (id), ADD UNIQUE KEY username (username); ALTER TABLE relationships_undirected ADD UNIQUE KEY user1_user2 (user1,user2), ADD UNIQUE KEY user2_user1 (user2,user1); ALTER TABLE relationships_directed ADD UNIQUE KEY user1_user2 (user1,user2), ADD UNIQUE KEY user2_user1 (user2,user1); ALTER TABLE one_time_keys ADD PRIMARY KEY (session, one_time_key); ALTER TABLE user_messages ADD INDEX recipient_time (recipient, time), ADD INDEX recipient_thread_time (recipient, thread, time), ADD INDEX thread (thread), ADD PRIMARY KEY (recipient, message); ALTER TABLE ids MODIFY id bigint(20) NOT NULL AUTO_INCREMENT; ALTER TABLE settings ADD PRIMARY KEY (user, name); ALTER TABLE metadata ADD PRIMARY KEY (name); ALTER TABLE policy_acknowledgments ADD PRIMARY KEY (user, policy); ALTER TABLE siwe_nonces ADD PRIMARY KEY (nonce); ALTER TABLE message_search ADD PRIMARY KEY (original_message_id), ADD FULLTEXT INDEX processed_content (processed_content); ALTER TABLE invite_links ADD PRIMARY KEY (id), ADD UNIQUE KEY (name), ADD INDEX community_primary (community, \`primary\`); ALTER TABLE olm_sessions ADD PRIMARY KEY (cookie_id, is_content); ALTER TABLE olm_accounts ADD PRIMARY KEY (is_content); `, { multipleStatements: true }, ); } async function createUsers() { const admin = await thisKeyserverAdmin(); const [user1, user2] = sortUserIDs(bots.commbot.userID, admin.id); const query = SQL` INSERT INTO ids (id, table_name) VALUES (${bots.commbot.userID}, 'users'); INSERT INTO users (id, username, hash, avatar, creation_time) VALUES (${bots.commbot.userID}, 'commbot', '', NULL, 1530049900980), (${admin.id}, ${admin.username}, '', NULL, 1463588881886); INSERT INTO relationships_undirected (user1, user2, status) VALUES (${user1}, ${user2}, ${undirectedStatus.KNOW_OF}); `; if (!isNaN(Number(admin.id))) { query.append(SQL` INSERT INTO ids (id, table_name) VALUES (${admin.id}, 'users'); `); } await dbQuery(query, { multipleStatements: true }); } const createThreadOptions = { forceAddMembers: true }; async function createThreads() { const insertIDsPromise = dbQuery(SQL` INSERT INTO ids (id, table_name) VALUES (${genesis().id}, 'threads'), (${bots.commbot.staffThreadID}, 'threads') `); const admin = await thisKeyserverAdmin(); const ashoatViewer = createScriptViewer(admin.id); const createGenesisPromise = createThread( ashoatViewer, { id: genesis().id, type: threadTypes.GENESIS, name: genesis().name, description: genesis().description, initialMemberIDs: [bots.commbot.userID], }, createThreadOptions, ); await Promise.all([insertIDsPromise, createGenesisPromise]); const commbotViewer = createScriptViewer(bots.commbot.userID); await createThread( commbotViewer, { id: bots.commbot.staffThreadID, type: threadTypes.COMMUNITY_SECRET_SUBTHREAD, initialMemberIDs: [admin.id], }, createThreadOptions, ); } async function setUpMetadataTable() { await updateDBVersion(newDatabaseVersion); } export { setupDB }; diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js index 1044c0ce9..cb1902ed4 100644 --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -1,591 +1,580 @@ // @flow import t from 'tcomb'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import { setThreadUnreadStatusResultValidator, updateActivityResultValidator, } from 'lib/types/activity-types.js'; import type { Endpoint } from 'lib/types/endpoints.js'; import { inviteLinkValidator } from 'lib/types/link-types.js'; import { uploadMultimediaResultValidator } from 'lib/types/media-types.js'; import { getOlmSessionInitializationDataResponseValidator } from 'lib/types/request-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { updateActivityResponder, threadSetUnreadStatusResponder, setThreadUnreadStatusValidator, updateActivityResponderInputValidator, } from './responders/activity-responders.js'; import { deviceTokenUpdateResponder, deviceTokenUpdateRequestInputValidator, } from './responders/device-responders.js'; import { entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, createEntryRequestInputValidator, saveEntryResponseValidator, deleteEntryRequestInputValidator, deleteEntryResponseValidator, entryQueryInputValidator, entryRevisionHistoryFetchInputValidator, fetchEntryInfosResponseValidator, fetchEntryRevisionInfosResultValidator, deltaEntryInfosResultValidator, newEntryQueryInputValidator, restoreEntryRequestInputValidator, restoreEntryResponseValidator, saveEntryRequestInputValidator, } from './responders/entry-responders.js'; import type { JSONResponder } from './responders/handlers.js'; import { createJSONResponder } from './responders/handlers.js'; -import { - getSessionPublicKeysResponder, - getOlmSessionInitializationDataResponder, - getSessionPublicKeysInputValidator, - getSessionPublicKeysResponseValidator, -} from './responders/keys-responders.js'; +import { getOlmSessionInitializationDataResponder } from './responders/keys-responders.js'; import { createOrUpdatePublicLinkResponder, disableInviteLinkResponder, fetchPrimaryInviteLinksResponder, inviteLinkVerificationResponder, createOrUpdatePublicLinkInputValidator, disableInviteLinkInputValidator, fetchInviteLinksResponseValidator, inviteLinkVerificationRequestInputValidator, inviteLinkVerificationResponseValidator, } from './responders/link-responders.js'; import { messageReportCreationResponder, messageReportCreationRequestInputValidator, messageReportCreationResultValidator, } from './responders/message-report-responder.js'; import { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, sendMessageResponseValidator, sendMultimediaMessageRequestInputValidator, sendReactionMessageRequestInputValidator, editMessageRequestInputValidator, sendEditMessageResponseValidator, sendTextMessageRequestInputValidator, fetchMessageInfosRequestInputValidator, fetchMessageInfosResponseValidator, fetchPinnedMessagesResponderInputValidator, fetchPinnedMessagesResultValidator, searchMessagesResponderInputValidator, searchMessagesResponseValidator, } from './responders/message-responders.js'; import { getInitialReduxStateResponder, initialReduxStateRequestValidator, initialReduxStateValidator, } from './responders/redux-state-responders.js'; import { updateRelationshipsResponder, relationshipErrorsValidator, updateRelationshipInputValidator, } from './responders/relationship-responders.js'; import { reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, reportCreationRequestInputValidator, reportCreationResponseValidator, fetchErrorReportInfosRequestInputValidator, fetchErrorReportInfosResponseValidator, reportMultiCreationRequestInputValidator, } from './responders/report-responders.js'; import { userSearchResponder, exactUserSearchResponder, exactUserSearchRequestInputValidator, exactUserSearchResultValidator, userSearchRequestInputValidator, userSearchResultValidator, } from './responders/search-responders.js'; import { siweNonceResponder, siweNonceResponseValidator, } from './responders/siwe-nonce-responders.js'; import { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadFetchMediaResponder, threadJoinResponder, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, leaveThreadResultValidator, newThreadRequestInputValidator, newThreadResponseValidator, threadDeletionRequestInputValidator, joinThreadRequestInputValidator, leaveThreadRequestInputValidator, threadFetchMediaRequestInputValidator, threadFetchMediaResultValidator, threadJoinResultValidator, changeThreadSettingsResultValidator, removeMembersRequestInputValidator, roleChangeRequestInputValidator, toggleMessagePinRequestInputValidator, toggleMessagePinResultValidator, updateThreadRequestInputValidator, roleDeletionRequestInputValidator, roleDeletionResultValidator, roleModificationRequestInputValidator, roleModificationResultValidator, } from './responders/thread-responders.js'; import { keyserverAuthRequestInputValidator, keyserverAuthResponder, userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, registerRequestInputValidator, registerResponseValidator, logOutResponseValidator, logInRequestInputValidator, logInResponseValidator, policyAcknowledgmentRequestInputValidator, accountUpdateInputValidator, resetPasswordRequestInputValidator, siweAuthRequestInputValidator, subscriptionUpdateRequestInputValidator, subscriptionUpdateResponseValidator, updatePasswordRequestInputValidator, updateUserAvatarResponderValidator, updateUserSettingsInputValidator, claimUsernameResponder, claimUsernameResponseValidator, } from './responders/user-responders.js'; import { codeVerificationResponder, codeVerificationRequestInputValidator, } from './responders/verification-responders.js'; import { versionResponder, versionResponseValidator, } from './responders/version-responders.js'; import { uploadMediaMetadataResponder, uploadDeletionResponder, UploadDeletionRequestInputValidator, uploadMediaMetadataInputValidator, } from './uploads/uploads.js'; const ignoredArgumentValidator = t.irreducible( 'Ignored argument', () => true, ); const jsonEndpoints: { [id: Endpoint]: JSONResponder } = { create_account: createJSONResponder( accountCreationResponder, registerRequestInputValidator, registerResponseValidator, [], ), create_entry: createJSONResponder( entryCreationResponder, createEntryRequestInputValidator, saveEntryResponseValidator, baseLegalPolicies, ), create_error_report: createJSONResponder( reportCreationResponder, reportCreationRequestInputValidator, reportCreationResponseValidator, [], ), create_message_report: createJSONResponder( messageReportCreationResponder, messageReportCreationRequestInputValidator, messageReportCreationResultValidator, baseLegalPolicies, ), create_multimedia_message: createJSONResponder( multimediaMessageCreationResponder, sendMultimediaMessageRequestInputValidator, sendMessageResponseValidator, baseLegalPolicies, ), create_or_update_public_link: createJSONResponder( createOrUpdatePublicLinkResponder, createOrUpdatePublicLinkInputValidator, inviteLinkValidator, baseLegalPolicies, ), create_reaction_message: createJSONResponder( reactionMessageCreationResponder, sendReactionMessageRequestInputValidator, sendMessageResponseValidator, baseLegalPolicies, ), disable_invite_link: createJSONResponder( disableInviteLinkResponder, disableInviteLinkInputValidator, t.Nil, baseLegalPolicies, ), edit_message: createJSONResponder( editMessageCreationResponder, editMessageRequestInputValidator, sendEditMessageResponseValidator, baseLegalPolicies, ), create_report: createJSONResponder( reportCreationResponder, reportCreationRequestInputValidator, reportCreationResponseValidator, [], ), create_reports: createJSONResponder( reportMultiCreationResponder, reportMultiCreationRequestInputValidator, t.Nil, [], ), create_text_message: createJSONResponder( textMessageCreationResponder, sendTextMessageRequestInputValidator, sendMessageResponseValidator, baseLegalPolicies, ), create_thread: createJSONResponder( threadCreationResponder, newThreadRequestInputValidator, newThreadResponseValidator, baseLegalPolicies, ), delete_account: createJSONResponder( accountDeletionResponder, ignoredArgumentValidator, logOutResponseValidator, [], ), delete_entry: createJSONResponder( entryDeletionResponder, deleteEntryRequestInputValidator, deleteEntryResponseValidator, baseLegalPolicies, ), delete_community_role: createJSONResponder( roleDeletionResponder, roleDeletionRequestInputValidator, roleDeletionResultValidator, baseLegalPolicies, ), delete_thread: createJSONResponder( threadDeletionResponder, threadDeletionRequestInputValidator, leaveThreadResultValidator, baseLegalPolicies, ), delete_upload: createJSONResponder( uploadDeletionResponder, UploadDeletionRequestInputValidator, t.Nil, baseLegalPolicies, ), exact_search_user: createJSONResponder( exactUserSearchResponder, exactUserSearchRequestInputValidator, exactUserSearchResultValidator, [], ), fetch_entries: createJSONResponder( entryFetchResponder, entryQueryInputValidator, fetchEntryInfosResponseValidator, baseLegalPolicies, ), fetch_entry_revisions: createJSONResponder( entryRevisionFetchResponder, entryRevisionHistoryFetchInputValidator, fetchEntryRevisionInfosResultValidator, baseLegalPolicies, ), fetch_error_report_infos: createJSONResponder( errorReportFetchInfosResponder, fetchErrorReportInfosRequestInputValidator, fetchErrorReportInfosResponseValidator, baseLegalPolicies, ), fetch_messages: createJSONResponder( messageFetchResponder, fetchMessageInfosRequestInputValidator, fetchMessageInfosResponseValidator, baseLegalPolicies, ), fetch_pinned_messages: createJSONResponder( fetchPinnedMessagesResponder, fetchPinnedMessagesResponderInputValidator, fetchPinnedMessagesResultValidator, baseLegalPolicies, ), fetch_primary_invite_links: createJSONResponder( fetchPrimaryInviteLinksResponder, ignoredArgumentValidator, fetchInviteLinksResponseValidator, baseLegalPolicies, ), fetch_thread_media: createJSONResponder( threadFetchMediaResponder, threadFetchMediaRequestInputValidator, threadFetchMediaResultValidator, baseLegalPolicies, ), get_initial_redux_state: createJSONResponder( getInitialReduxStateResponder, initialReduxStateRequestValidator, initialReduxStateValidator, [], ), - get_session_public_keys: createJSONResponder( - getSessionPublicKeysResponder, - getSessionPublicKeysInputValidator, - getSessionPublicKeysResponseValidator, - baseLegalPolicies, - ), join_thread: createJSONResponder( threadJoinResponder, joinThreadRequestInputValidator, threadJoinResultValidator, baseLegalPolicies, ), keyserver_auth: createJSONResponder( keyserverAuthResponder, keyserverAuthRequestInputValidator, logInResponseValidator, [], ), leave_thread: createJSONResponder( threadLeaveResponder, leaveThreadRequestInputValidator, leaveThreadResultValidator, baseLegalPolicies, ), log_in: createJSONResponder( logInResponder, logInRequestInputValidator, logInResponseValidator, [], ), log_out: createJSONResponder( logOutResponder, ignoredArgumentValidator, logOutResponseValidator, [], ), modify_community_role: createJSONResponder( roleModificationResponder, roleModificationRequestInputValidator, roleModificationResultValidator, baseLegalPolicies, ), policy_acknowledgment: createJSONResponder( policyAcknowledgmentResponder, policyAcknowledgmentRequestInputValidator, t.Nil, [], ), remove_members: createJSONResponder( memberRemovalResponder, removeMembersRequestInputValidator, changeThreadSettingsResultValidator, baseLegalPolicies, ), restore_entry: createJSONResponder( entryRestorationResponder, restoreEntryRequestInputValidator, restoreEntryResponseValidator, baseLegalPolicies, ), search_messages: createJSONResponder( searchMessagesResponder, searchMessagesResponderInputValidator, searchMessagesResponseValidator, baseLegalPolicies, ), search_users: createJSONResponder( userSearchResponder, userSearchRequestInputValidator, userSearchResultValidator, baseLegalPolicies, ), send_password_reset_email: createJSONResponder( sendPasswordResetEmailResponder, resetPasswordRequestInputValidator, t.Nil, [], ), send_verification_email: createJSONResponder( sendVerificationEmailResponder, ignoredArgumentValidator, t.Nil, [], ), set_thread_unread_status: createJSONResponder( threadSetUnreadStatusResponder, setThreadUnreadStatusValidator, setThreadUnreadStatusResultValidator, baseLegalPolicies, ), toggle_message_pin: createJSONResponder( toggleMessagePinResponder, toggleMessagePinRequestInputValidator, toggleMessagePinResultValidator, baseLegalPolicies, ), update_account: createJSONResponder( passwordUpdateResponder, accountUpdateInputValidator, t.Nil, baseLegalPolicies, ), update_activity: createJSONResponder( updateActivityResponder, updateActivityResponderInputValidator, updateActivityResultValidator, baseLegalPolicies, ), update_calendar_query: createJSONResponder( calendarQueryUpdateResponder, newEntryQueryInputValidator, deltaEntryInfosResultValidator, baseLegalPolicies, ), update_user_settings: createJSONResponder( updateUserSettingsResponder, updateUserSettingsInputValidator, t.Nil, baseLegalPolicies, ), update_device_token: createJSONResponder( deviceTokenUpdateResponder, deviceTokenUpdateRequestInputValidator, t.Nil, [], ), update_entry: createJSONResponder( entryUpdateResponder, saveEntryRequestInputValidator, saveEntryResponseValidator, baseLegalPolicies, ), update_password: createJSONResponder( oldPasswordUpdateResponder, updatePasswordRequestInputValidator, logInResponseValidator, baseLegalPolicies, ), update_relationships: createJSONResponder( updateRelationshipsResponder, updateRelationshipInputValidator, relationshipErrorsValidator, baseLegalPolicies, ), update_role: createJSONResponder( roleUpdateResponder, roleChangeRequestInputValidator, changeThreadSettingsResultValidator, baseLegalPolicies, ), update_thread: createJSONResponder( threadUpdateResponder, updateThreadRequestInputValidator, changeThreadSettingsResultValidator, baseLegalPolicies, ), update_user_subscription: createJSONResponder( userSubscriptionUpdateResponder, subscriptionUpdateRequestInputValidator, subscriptionUpdateResponseValidator, baseLegalPolicies, ), verify_code: createJSONResponder( codeVerificationResponder, codeVerificationRequestInputValidator, t.Nil, baseLegalPolicies, ), verify_invite_link: createJSONResponder( inviteLinkVerificationResponder, inviteLinkVerificationRequestInputValidator, inviteLinkVerificationResponseValidator, baseLegalPolicies, ), siwe_nonce: createJSONResponder( siweNonceResponder, ignoredArgumentValidator, siweNonceResponseValidator, [], ), siwe_auth: createJSONResponder( siweAuthResponder, siweAuthRequestInputValidator, logInResponseValidator, [], ), claim_username: createJSONResponder( claimUsernameResponder, ignoredArgumentValidator, claimUsernameResponseValidator, [], ), update_user_avatar: createJSONResponder( updateUserAvatarResponder, updateUserAvatarRequestValidator, updateUserAvatarResponderValidator, baseLegalPolicies, ), upload_media_metadata: createJSONResponder( uploadMediaMetadataResponder, uploadMediaMetadataInputValidator, uploadMultimediaResultValidator, baseLegalPolicies, ), get_olm_session_initialization_data: createJSONResponder( getOlmSessionInitializationDataResponder, ignoredArgumentValidator, getOlmSessionInitializationDataResponseValidator, [], ), version: createJSONResponder( versionResponder, ignoredArgumentValidator, versionResponseValidator, [], ), }; export { jsonEndpoints }; diff --git a/keyserver/src/fetchers/key-fetchers.js b/keyserver/src/fetchers/key-fetchers.js deleted file mode 100644 index 181965aaa..000000000 --- a/keyserver/src/fetchers/key-fetchers.js +++ /dev/null @@ -1,53 +0,0 @@ -// @flow - -import type { SessionPublicKeys } from 'lib/types/session-types.js'; -import { minimumOneTimeKeysRequired } from 'lib/utils/crypto-utils.js'; -import { ServerError } from 'lib/utils/errors.js'; - -import { dbQuery, SQL } from '../database/database.js'; -import { deleteOneTimeKey } from '../deleters/one-time-key-deleters.js'; - -async function checkIfSessionHasEnoughOneTimeKeys( - session: string, -): Promise { - const query = SQL` - SELECT COUNT(*) AS count - FROM one_time_keys - WHERE session = ${session} - `; - const [queryResult] = await dbQuery(query); - if (!queryResult.length || queryResult[0].count === undefined) { - throw new ServerError('internal_error'); - } - const [{ count }] = queryResult; - return count >= minimumOneTimeKeysRequired; -} - -async function fetchSessionPublicKeys( - session: string, -): Promise { - const query = SQL` - SELECT s.public_key, otk.one_time_key - FROM sessions s - LEFT JOIN one_time_keys otk ON otk.session = s.id - WHERE s.id = ${session} - LIMIT 1 - `; - const [queryResult] = await dbQuery(query); - if (!queryResult.length) { - return null; - } - const [result] = queryResult; - - if (!result.public_key) { - return null; - } - - const oneTimeKey = result.one_time_key; - const identityKey = result.public_key; - await deleteOneTimeKey(session, oneTimeKey); - - return { identityKey, oneTimeKey }; -} - -export { fetchSessionPublicKeys, checkIfSessionHasEnoughOneTimeKeys }; diff --git a/keyserver/src/responders/keys-responders.js b/keyserver/src/responders/keys-responders.js index 4cb990343..4311b52d6 100644 --- a/keyserver/src/responders/keys-responders.js +++ b/keyserver/src/responders/keys-responders.js @@ -1,130 +1,99 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; -import t, { type TUnion, type TInterface } from 'tcomb'; import type { OlmSessionInitializationInfo, GetOlmSessionInitializationDataResponse, - GetSessionPublicKeysArgs, } from 'lib/types/request-types.js'; -import { - type SessionPublicKeys, - sessionPublicKeysValidator, -} from 'lib/types/session-types.js'; import { ServerError } from 'lib/utils/errors.js'; -import { tShape, tNull } from 'lib/utils/validation-utils.js'; -import { fetchSessionPublicKeys } from '../fetchers/key-fetchers.js'; -import type { Viewer } from '../session/viewer.js'; import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; type SessionInitializationKeysSet = { +identityKeys: string, ...OlmSessionInitializationInfo, }; -export const getSessionPublicKeysInputValidator: TInterface = - tShape({ - session: t.String, - }); - -type GetSessionPublicKeysResponse = SessionPublicKeys | null; -export const getSessionPublicKeysResponseValidator: TUnion = - t.union([sessionPublicKeysValidator, tNull]); - -async function getSessionPublicKeysResponder( - viewer: Viewer, - request: GetSessionPublicKeysArgs, -): Promise { - if (!viewer.loggedIn) { - return null; - } - return await fetchSessionPublicKeys(request.session); -} - function retrieveSessionInitializationKeysSet( account: OlmAccount, ): SessionInitializationKeysSet { const identityKeys = account.identity_keys(); const prekey = account.prekey(); const prekeySignature = account.prekey_signature(); if (!prekeySignature) { throw new ServerError('invalid_prekey'); } account.generate_one_time_keys(1); const oneTimeKey = account.one_time_keys(); account.mark_keys_as_published(); return { identityKeys, oneTimeKey, prekey, prekeySignature }; } async function getOlmSessionInitializationDataResponder(): Promise { const { identityKeys: notificationsIdentityKeys, prekey: notificationsPrekey, prekeySignature: notificationsPrekeySignature, oneTimeKey: notificationsOneTimeKey, } = await fetchCallUpdateOlmAccount( 'notifications', retrieveSessionInitializationKeysSet, ); const contentAccountCallback = async (account: OlmAccount) => { const { identityKeys: contentIdentityKeys, oneTimeKey, prekey, prekeySignature, } = await retrieveSessionInitializationKeysSet(account); const identityKeysBlob = { primaryIdentityPublicKeys: JSON.parse(contentIdentityKeys), notificationIdentityPublicKeys: JSON.parse(notificationsIdentityKeys), }; const identityKeysBlobPayload = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob = { payload: identityKeysBlobPayload, signature: account.sign(identityKeysBlobPayload), }; return { signedIdentityKeysBlob, oneTimeKey, prekey, prekeySignature, }; }; const { signedIdentityKeysBlob, prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKey: contentOneTimeKey, } = await fetchCallUpdateOlmAccount('content', contentAccountCallback); const notifInitializationInfo = { prekey: notificationsPrekey, prekeySignature: notificationsPrekeySignature, oneTimeKey: notificationsOneTimeKey, }; const contentInitializationInfo = { prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKey: contentOneTimeKey, }; return { signedIdentityKeysBlob, contentInitializationInfo, notifInitializationInfo, }; } -export { - getSessionPublicKeysResponder, - getOlmSessionInitializationDataResponder, -}; +export { getOlmSessionInitializationDataResponder }; diff --git a/keyserver/src/responders/responder-validators.test.js b/keyserver/src/responders/responder-validators.test.js index 6c9a8e962..f8a8df9ad 100644 --- a/keyserver/src/responders/responder-validators.test.js +++ b/keyserver/src/responders/responder-validators.test.js @@ -1,972 +1,953 @@ // @flow import { setThreadUnreadStatusResultValidator, updateActivityResultValidator, } from 'lib/types/activity-types.js'; import { fetchEntryInfosResponseValidator, fetchEntryRevisionInfosResultValidator, saveEntryResponseValidator, deleteEntryResponseValidator, deltaEntryInfosResultValidator, restoreEntryResponseValidator, } from './entry-responders.js'; -import { getSessionPublicKeysResponseValidator } from './keys-responders.js'; import { inviteLinkVerificationResponseValidator, fetchInviteLinksResponseValidator, } from './link-responders.js'; import { messageReportCreationResultValidator } from './message-report-responder.js'; import { fetchMessageInfosResponseValidator, fetchPinnedMessagesResultValidator, sendEditMessageResponseValidator, sendMessageResponseValidator, } from './message-responders.js'; import { relationshipErrorsValidator } from './relationship-responders.js'; import { reportCreationResponseValidator } from './report-responders.js'; import { userSearchResultValidator } from './search-responders.js'; import { siweNonceResponseValidator } from './siwe-nonce-responders.js'; import { changeThreadSettingsResultValidator, leaveThreadResultValidator, newThreadResponseValidator, threadFetchMediaResultValidator, threadJoinResultValidator, toggleMessagePinResultValidator, roleChangeRequestInputValidator, } from './thread-responders.js'; import { logInResponseValidator, registerResponseValidator, logOutResponseValidator, } from './user-responders.js'; describe('user responder validators', () => { it('should validate logout response', () => { const response = { currentUserInfo: { anonymous: true } }; expect(logOutResponseValidator.is(response)).toBe(true); response.currentUserInfo.anonymous = false; expect(logOutResponseValidator.is(response)).toBe(false); }); it('should validate register response', () => { const response = { id: '93079', rawMessageInfos: [ { type: 1, threadID: '93095', creatorID: '93079', time: 1682086407469, initialThreadState: { type: 6, name: null, parentThreadID: '1', color: '648caa', memberIDs: ['256', '93079'], }, id: '93110', }, { type: 0, threadID: '93095', creatorID: '256', time: 1682086407575, text: 'welcome to Comm!', id: '93113', }, ], currentUserInfo: { id: '93079', username: 'user' }, cookieChange: { threadInfos: { '1': { id: '1', type: 12, name: 'GENESIS', description: 'desc', color: 'c85000', creationTime: 1672934346213, parentThreadID: null, members: [ { id: '256', role: '83796', permissions: { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: true, source: '1' }, edit_entries: { value: true, source: '1' }, edit_thread: { value: true, source: '1' }, edit_thread_description: { value: true, source: '1' }, edit_thread_color: { value: true, source: '1' }, delete_thread: { value: true, source: '1' }, create_subthreads: { value: true, source: '1' }, create_sidebars: { value: true, source: '1' }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: true, source: '1' }, remove_members: { value: true, source: '1' }, change_role: { value: true, source: '1' }, leave_thread: { value: false, source: null }, react_to_message: { value: true, source: '1' }, edit_message: { value: true, source: '1' }, }, isSender: false, }, ], roles: { '83795': { id: '83795', name: 'Members', permissions: { know_of: true, visible: true, descendant_open_know_of: true, descendant_open_visible: true, descendant_opentoplevel_join_thread: true, }, isDefault: true, }, }, currentUser: { role: '83795', permissions: { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, source: null }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: false, source: null }, remove_members: { value: false, source: null }, change_role: { value: false, source: null }, leave_thread: { value: false, source: null }, react_to_message: { value: false, source: null }, edit_message: { value: false, source: null }, }, subscription: { home: true, pushNotifs: true }, unread: true, }, repliesCount: 0, containingThreadID: null, community: null, }, }, userInfos: [ { id: '5', username: 'commbot' }, { id: '256', username: 'ashoat' }, { id: '93079', username: 'temp_user7' }, ], }, }; expect(registerResponseValidator.is(response)).toBe(true); const cookieChange: any = response.cookieChange; cookieChange.userInfos = undefined; expect(registerResponseValidator.is(response)).toBe(false); }); it('should validate login response', () => { const response = { currentUserInfo: { id: '93079', username: 'temp_user7' }, rawMessageInfos: [ { type: 0, id: '93115', threadID: '93094', time: 1682086407577, creatorID: '5', text: 'This is your private chat, where you can set', }, { type: 1, id: '93111', threadID: '93094', time: 1682086407467, creatorID: '93079', initialThreadState: { type: 7, name: 'temp_user7', parentThreadID: '1', color: '575757', memberIDs: ['93079'], }, }, ], truncationStatuses: { '93094': 'exhaustive', '93095': 'exhaustive' }, serverTime: 1682086579416, userInfos: [ { id: '5', username: 'commbot' }, { id: '256', username: 'ashoat' }, { id: '93079', username: 'temp_user7' }, ], cookieChange: { threadInfos: { '1': { id: '1', type: 12, name: 'GENESIS', description: 'This is the first community on Comm. In the future it will', color: 'c85000', creationTime: 1672934346213, parentThreadID: null, members: [ { id: '256', role: '83796', permissions: { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: true, source: '1' }, edit_entries: { value: true, source: '1' }, edit_thread: { value: true, source: '1' }, edit_thread_description: { value: true, source: '1' }, edit_thread_color: { value: true, source: '1' }, delete_thread: { value: true, source: '1' }, create_subthreads: { value: true, source: '1' }, create_sidebars: { value: true, source: '1' }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: true, source: '1' }, remove_members: { value: true, source: '1' }, change_role: { value: true, source: '1' }, leave_thread: { value: false, source: null }, react_to_message: { value: true, source: '1' }, edit_message: { value: true, source: '1' }, }, isSender: false, }, { id: '93079', role: '83795', permissions: { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, source: null }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: false, source: null }, remove_members: { value: false, source: null }, change_role: { value: false, source: null }, leave_thread: { value: false, source: null }, react_to_message: { value: false, source: null }, edit_message: { value: false, source: null }, }, isSender: false, }, ], roles: { '83795': { id: '83795', name: 'Members', permissions: { know_of: true, visible: true, descendant_open_know_of: true, descendant_open_visible: true, descendant_opentoplevel_join_thread: true, }, isDefault: true, }, '83796': { id: '83796', name: 'Admins', permissions: { know_of: true, visible: true, voiced: true, react_to_message: true, edit_message: true, edit_entries: true, edit_thread: true, edit_thread_color: true, edit_thread_description: true, create_subthreads: true, create_sidebars: true, add_members: true, delete_thread: true, remove_members: true, change_role: true, descendant_know_of: true, descendant_visible: true, descendant_toplevel_join_thread: true, child_join_thread: true, descendant_voiced: true, descendant_edit_entries: true, descendant_edit_thread: true, descendant_edit_thread_color: true, descendant_edit_thread_description: true, descendant_toplevel_create_subthreads: true, descendant_toplevel_create_sidebars: true, descendant_add_members: true, descendant_delete_thread: true, descendant_edit_permissions: true, descendant_remove_members: true, descendant_change_role: true, }, isDefault: false, }, }, currentUser: { role: '83795', permissions: { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, source: null }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: false, source: null }, remove_members: { value: false, source: null }, change_role: { value: false, source: null }, leave_thread: { value: false, source: null }, react_to_message: { value: false, source: null }, edit_message: { value: false, source: null }, }, subscription: { home: true, pushNotifs: true }, unread: true, }, repliesCount: 0, containingThreadID: null, community: null, }, }, userInfos: [], }, rawEntryInfos: [], }; expect(logInResponseValidator.is(response)).toBe(true); expect( logInResponseValidator.is({ ...response, currentUserInfo: undefined }), ).toBe(false); }); }); describe('search responder', () => { it('should validate search response', () => { const response: any = { userInfos: [ { id: '83817', username: 'temp_user0' }, { id: '83853', username: 'temp_user1' }, { id: '83890', username: 'temp_user2' }, { id: '83928', username: 'temp_user3' }, ], }; expect(userSearchResultValidator.is(response)).toBe(true); response.userInfos.push({ id: 123 }); expect(userSearchResultValidator.is(response)).toBe(false); }); }); describe('message report responder', () => { it('should validate message report response', () => { const response = { messageInfo: { type: 0, threadID: '101113', creatorID: '5', time: 1682429699746, text: 'text', id: '101121', }, }; expect(messageReportCreationResultValidator.is(response)).toBe(true); response.messageInfo.type = -2; expect(messageReportCreationResultValidator.is(response)).toBe(false); }); }); describe('relationship responder', () => { it('should validate relationship response', () => { const response = { invalid_user: ['83817', '83890'], already_friends: ['83890'], }; expect(relationshipErrorsValidator.is(response)).toBe(true); expect( relationshipErrorsValidator.is({ ...response, user_blocked: {} }), ).toBe(false); }); }); describe('activity responder', () => { it('should validate update activity response', () => { const response: any = { unfocusedToUnread: ['93095'] }; expect(updateActivityResultValidator.is(response)).toBe(true); response.unfocusedToUnread.push(123); expect(updateActivityResultValidator.is(response)).toBe(false); }); it('should validate set thread unread response', () => { const response = { resetToUnread: false }; expect(setThreadUnreadStatusResultValidator.is(response)).toBe(true); expect( setThreadUnreadStatusResultValidator.is({ ...response, unread: false }), ).toBe(false); }); }); -describe('keys responder', () => { - it('should validate get session public keys response', () => { - const response = { - identityKey: 'key', - oneTimeKey: 'key', - }; - - expect(getSessionPublicKeysResponseValidator.is(response)).toBe(true); - expect(getSessionPublicKeysResponseValidator.is(null)).toBe(true); - expect( - getSessionPublicKeysResponseValidator.is({ - ...response, - identityKey: undefined, - }), - ).toBe(false); - }); -}); - describe('siwe nonce responders', () => { it('should validate siwe nonce response', () => { const response = { nonce: 'nonce' }; expect(siweNonceResponseValidator.is(response)).toBe(true); expect(siweNonceResponseValidator.is({ nonce: 123 })).toBe(false); }); }); describe('entry reponders', () => { it('should validate entry fetch response', () => { const response = { rawEntryInfos: [ { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }, ], userInfos: { '123': { id: '123', username: 'username', }, }, }; expect(fetchEntryInfosResponseValidator.is(response)).toBe(true); expect( fetchEntryInfosResponseValidator.is({ ...response, userInfos: undefined, }), ).toBe(false); }); it('should validate entry revision fetch response', () => { const response = { result: [ { id: '93297', authorID: '83853', text: 'text', lastUpdate: 1682603494202, deleted: false, threadID: '83859', entryID: '93270', }, { id: '93284', authorID: '83853', text: 'text', lastUpdate: 1682603426996, deleted: true, threadID: '83859', entryID: '93270', }, ], }; expect(fetchEntryRevisionInfosResultValidator.is(response)).toBe(true); expect( fetchEntryRevisionInfosResultValidator.is({ ...response, result: {}, }), ).toBe(false); }); it('should validate entry save response', () => { const response = { entryID: '93270', newMessageInfos: [ { type: 9, threadID: '83859', creatorID: '83853', time: 1682603362817, entryID: '93270', date: '2023-04-03', text: 'text', id: '93272', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(saveEntryResponseValidator.is(response)).toBe(true); expect( saveEntryResponseValidator.is({ ...response, entryID: undefined, }), ).toBe(false); }); it('should validate entry delete response', () => { const response = { threadID: '83859', newMessageInfos: [ { type: 11, threadID: '83859', creatorID: '83853', time: 1682603427038, entryID: '93270', date: '2023-04-03', text: 'text', id: '93285', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(deleteEntryResponseValidator.is(response)).toBe(true); expect( deleteEntryResponseValidator.is({ ...response, threadID: undefined, }), ).toBe(false); }); it('should validate entry restore response', () => { const response = { newMessageInfos: [ { type: 11, threadID: '83859', creatorID: '83853', time: 1682603427038, entryID: '93270', date: '2023-04-03', text: 'text', id: '93285', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(restoreEntryResponseValidator.is(response)).toBe(true); expect( restoreEntryResponseValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate entry delta response', () => { const response = { rawEntryInfos: [ { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }, ], deletedEntryIDs: ['92860'], userInfos: [ { id: '123', username: 'username', }, ], }; expect(deltaEntryInfosResultValidator.is(response)).toBe(true); expect( deltaEntryInfosResultValidator.is({ ...response, rawEntryInfos: undefined, }), ).toBe(false); }); }); describe('thread responders', () => { it('should validate change thread settings response', () => { const response = { updatesResult: { newUpdates: [ { type: 1, id: '93601', time: 1682759546258, threadInfo: { id: '92796', type: 6, name: '', description: '', color: 'b8753d', creationTime: 1682076700918, parentThreadID: '1', members: [], roles: {}, currentUser: { role: '85172', permissions: {}, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }, }, ], }, newMessageInfos: [ { type: 4, threadID: '92796', creatorID: '83928', time: 1682759546275, field: 'color', value: 'b8753d', id: '93602', }, ], }; expect(changeThreadSettingsResultValidator.is(response)).toBe(true); expect( changeThreadSettingsResultValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate leave thread response', () => { const response = { updatesResult: { newUpdates: [ { type: 3, id: '93595', time: 1682759498811, threadID: '93561' }, ], }, }; expect(leaveThreadResultValidator.is(response)).toBe(true); expect( leaveThreadResultValidator.is({ ...response, updatedResult: undefined, }), ).toBe(false); }); it('should validate new thread response', () => { const response = { newThreadID: '93619', updatesResult: { newUpdates: [ { type: 4, id: '93621', time: 1682759805331, threadInfo: { id: '93619', type: 5, name: 'a', description: '', color: 'b8753d', creationTime: 1682759805298, parentThreadID: '92796', members: [], roles: {}, currentUser: { role: '85172', permissions: {}, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '92796', community: '1', sourceMessageID: '93614', pinnedCount: 0, }, rawMessageInfos: [], truncationStatus: 'exhaustive', rawEntryInfos: [], }, ], }, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, newMessageInfos: [], }; expect(newThreadResponseValidator.is(response)).toBe(true); expect( newThreadResponseValidator.is({ ...response, newMessageInfos: {}, }), ).toBe(false); }); it('should validate thread join response', () => { const response = { rawMessageInfos: [ { type: 8, threadID: '93619', creatorID: '83928', time: 1682759915935, id: '93640', }, ], truncationStatuses: {}, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, updatesResult: { newUpdates: [], }, }; expect(threadJoinResultValidator.is(response)).toBe(true); expect( threadJoinResultValidator.is({ ...response, updatesResult: [], }), ).toBe(false); }); it('should validate thread fetch media response', () => { const response = { media: [ { type: 'photo', id: '93642', uri: 'http://0.0.0.0:3000/comm/upload/93642/1e0d7a5262952e3b', dimensions: { width: 220, height: 220 }, }, ], }; expect(threadFetchMediaResultValidator.is(response)).toBe(true); expect( threadFetchMediaResultValidator.is({ ...response, media: undefined }), ).toBe(false); }); it('should validate toggle message pin response', () => { const response = { threadID: '123', newMessageInfos: [] }; expect(toggleMessagePinResultValidator.is(response)).toBe(true); expect( toggleMessagePinResultValidator.is({ ...response, threadID: undefined }), ).toBe(false); }); it('should validate role change request input', () => { const input = { threadID: '123', memberIDs: [], role: '1', }; expect(roleChangeRequestInputValidator.is(input)).toBe(true); expect(roleChangeRequestInputValidator.is({ ...input, role: '2|1' })).toBe( true, ); expect(roleChangeRequestInputValidator.is({ ...input, role: '-1' })).toBe( false, ); expect(roleChangeRequestInputValidator.is({ ...input, role: '2|-1' })).toBe( false, ); }); }); describe('message responders', () => { it('should validate send message response', () => { const response = { newMessageInfo: { type: 0, threadID: '93619', creatorID: '83928', time: 1682761023640, text: 'a', localID: 'local3', id: '93649', }, }; expect(sendMessageResponseValidator.is(response)).toBe(true); expect( sendMessageResponseValidator.is({ ...response, newMEssageInfos: undefined, }), ).toBe(false); }); it('should validate fetch message infos response', () => { const response = { rawMessageInfos: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], truncationStatuses: { '83938': 'exhaustive' }, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, }; expect(fetchMessageInfosResponseValidator.is(response)).toBe(true); expect( fetchMessageInfosResponseValidator.is({ ...response, userInfos: undefined, }), ).toBe(false); }); it('should validate send edit message response', () => { const response = { newMessageInfos: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], }; expect(sendEditMessageResponseValidator.is(response)).toBe(true); expect( sendEditMessageResponseValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate fetch pinned message response', () => { const response = { pinnedMessages: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], }; expect(fetchPinnedMessagesResultValidator.is(response)).toBe(true); expect( fetchPinnedMessagesResultValidator.is({ ...response, pinnedMessages: undefined, }), ).toBe(false); }); }); describe('report responders', () => { it('should validate report creation response', () => { const response = { id: '123' }; expect(reportCreationResponseValidator.is(response)).toBe(true); expect(reportCreationResponseValidator.is({})).toBe(false); }); }); describe('link responders', () => { it('should validate invite link verification response', () => { const response = { status: 'already_joined', community: { name: 'name', id: '123', }, }; expect(inviteLinkVerificationResponseValidator.is(response)).toBe(true); expect(inviteLinkVerificationResponseValidator.is({})).toBe(false); }); it('should validate invite link verification response', () => { const response = { links: [ { name: 'name', primary: true, role: '123', communityID: '123', expirationTime: 123, limitOfUses: 123, numberOfUses: 123, }, ], }; expect(fetchInviteLinksResponseValidator.is(response)).toBe(true); expect(fetchInviteLinksResponseValidator.is({ links: {} })).toBe(false); }); }); diff --git a/keyserver/src/socket/session-utils.js b/keyserver/src/socket/session-utils.js index b30facbb7..7c99be787 100644 --- a/keyserver/src/socket/session-utils.js +++ b/keyserver/src/socket/session-utils.js @@ -1,560 +1,535 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import type { TUnion } from 'tcomb'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { UpdateActivityResult, ActivityUpdate, } from 'lib/types/activity-types.js'; import type { IdentityKeysBlob } from 'lib/types/crypto-types.js'; import { isDeviceType } from 'lib/types/device-types.js'; import type { CalendarQuery, DeltaEntryInfosResponse, } from 'lib/types/entry-types.js'; import { reportTypes, type ThreadInconsistencyReportCreationRequest, type EntryInconsistencyReportCreationRequest, } from 'lib/types/report-types.js'; import { serverRequestTypes, type ThreadInconsistencyClientResponse, type EntryInconsistencyClientResponse, type ClientResponse, type ServerServerRequest, type ServerCheckStateServerRequest, } from 'lib/types/request-types.js'; import { sessionCheckFrequency } from 'lib/types/session-types.js'; import { signedIdentityKeysBlobValidator } from 'lib/utils/crypto-utils.js'; import { hash, values } from 'lib/utils/objects.js'; import { promiseAll, ignorePromiseRejections } from 'lib/utils/promises.js'; import { tShape, tPlatform, tPlatformDetails, } from 'lib/utils/validation-utils.js'; import { createAndPersistOlmSession } from '../creators/olm-session-creator.js'; -import { saveOneTimeKeys } from '../creators/one-time-keys-creator.js'; import createReport from '../creators/report-creator.js'; import { fetchEntriesForSession } from '../fetchers/entry-fetchers.js'; -import { checkIfSessionHasEnoughOneTimeKeys } from '../fetchers/key-fetchers.js'; import { activityUpdatesInputValidator } from '../responders/activity-responders.js'; import { threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, } from '../responders/report-responders.js'; import { setNewSession, setCookiePlatform, setCookiePlatformDetails, setCookieSignedIdentityKeysBlob, } from '../session/cookies.js'; import type { Viewer } from '../session/viewer.js'; import { serverStateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { activityUpdater } from '../updaters/activity-updaters.js'; import { compareNewCalendarQuery } from '../updaters/entry-updaters.js'; import type { SessionUpdate } from '../updaters/session-updaters.js'; import { getOlmUtility } from '../utils/olm-utils.js'; const clientResponseInputValidator: TUnion = t.union([ tShape({ type: t.irreducible( 'serverRequestTypes.PLATFORM', x => x === serverRequestTypes.PLATFORM, ), platform: tPlatform, }), tShape({ ...threadInconsistencyReportValidatorShape, type: t.irreducible( 'serverRequestTypes.THREAD_INCONSISTENCY', x => x === serverRequestTypes.THREAD_INCONSISTENCY, ), }), tShape({ ...entryInconsistencyReportValidatorShape, type: t.irreducible( 'serverRequestTypes.ENTRY_INCONSISTENCY', x => x === serverRequestTypes.ENTRY_INCONSISTENCY, ), }), tShape({ type: t.irreducible( 'serverRequestTypes.PLATFORM_DETAILS', x => x === serverRequestTypes.PLATFORM_DETAILS, ), platformDetails: tPlatformDetails, }), tShape({ type: t.irreducible( 'serverRequestTypes.CHECK_STATE', x => x === serverRequestTypes.CHECK_STATE, ), hashResults: t.dict(t.String, t.Boolean), }), tShape({ type: t.irreducible( 'serverRequestTypes.INITIAL_ACTIVITY_UPDATES', x => x === serverRequestTypes.INITIAL_ACTIVITY_UPDATES, ), activityUpdates: activityUpdatesInputValidator, }), - tShape({ - type: t.irreducible( - 'serverRequestTypes.MORE_ONE_TIME_KEYS', - x => x === serverRequestTypes.MORE_ONE_TIME_KEYS, - ), - keys: t.list(t.String), - }), tShape({ type: t.irreducible( 'serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB', x => x === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, ), signedIdentityKeysBlob: signedIdentityKeysBlobValidator, }), tShape({ type: t.irreducible( 'serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE', x => x === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, ), initialNotificationsEncryptedMessage: t.String, }), ]); type StateCheckStatus = | { status: 'state_validated' } | { status: 'state_invalid', invalidKeys: $ReadOnlyArray } | { status: 'state_check' }; type ProcessClientResponsesResult = { serverRequests: ServerServerRequest[], stateCheckStatus: ?StateCheckStatus, activityUpdateResult: ?UpdateActivityResult, }; async function processClientResponses( viewer: Viewer, clientResponses: $ReadOnlyArray, ): Promise { let viewerMissingPlatform = !viewer.platform; const { platformDetails } = viewer; let viewerMissingPlatformDetails = !platformDetails || (isDeviceType(viewer.platform) && (platformDetails.codeVersion === null || platformDetails.codeVersion === undefined || platformDetails.stateVersion === null || platformDetails.stateVersion === undefined)); const promises = []; let activityUpdates: Array = []; let stateCheckStatus = null; const clientSentPlatformDetails = clientResponses.some( response => response.type === serverRequestTypes.PLATFORM_DETAILS, ); for (const clientResponse of clientResponses) { if ( clientResponse.type === serverRequestTypes.PLATFORM && !clientSentPlatformDetails ) { promises.push(setCookiePlatform(viewer, clientResponse.platform)); viewerMissingPlatform = false; if (!isDeviceType(clientResponse.platform)) { viewerMissingPlatformDetails = false; } } else if ( clientResponse.type === serverRequestTypes.THREAD_INCONSISTENCY ) { promises.push(recordThreadInconsistency(viewer, clientResponse)); } else if (clientResponse.type === serverRequestTypes.ENTRY_INCONSISTENCY) { promises.push(recordEntryInconsistency(viewer, clientResponse)); } else if (clientResponse.type === serverRequestTypes.PLATFORM_DETAILS) { promises.push( setCookiePlatformDetails(viewer, clientResponse.platformDetails), ); viewerMissingPlatform = false; viewerMissingPlatformDetails = false; } else if ( clientResponse.type === serverRequestTypes.INITIAL_ACTIVITY_UPDATES ) { activityUpdates = [...activityUpdates, ...clientResponse.activityUpdates]; } else if (clientResponse.type === serverRequestTypes.CHECK_STATE) { const invalidKeys = []; for (const key in clientResponse.hashResults) { const result = clientResponse.hashResults[key]; if (!result) { invalidKeys.push(key); } } stateCheckStatus = invalidKeys.length > 0 ? { status: 'state_invalid', invalidKeys } : { status: 'state_validated' }; - } else if (clientResponse.type === serverRequestTypes.MORE_ONE_TIME_KEYS) { - invariant(clientResponse.keys, 'keys expected in client response'); - ignorePromiseRejections(saveOneTimeKeys(viewer, clientResponse.keys)); } else if ( clientResponse.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB ) { invariant( clientResponse.signedIdentityKeysBlob, 'signedIdentityKeysBlob expected in client response', ); const { signedIdentityKeysBlob } = clientResponse; const identityKeys: IdentityKeysBlob = JSON.parse( signedIdentityKeysBlob.payload, ); const olmUtil = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); ignorePromiseRejections( setCookieSignedIdentityKeysBlob( viewer.cookieID, signedIdentityKeysBlob, ), ); } catch (e) { continue; } } else if ( clientResponse.type === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE ) { invariant( t.String.is(clientResponse.initialNotificationsEncryptedMessage), 'initialNotificationsEncryptedMessage expected in client response', ); const { initialNotificationsEncryptedMessage } = clientResponse; try { await createAndPersistOlmSession( initialNotificationsEncryptedMessage, 'notifications', viewer.cookieID, ); } catch (e) { continue; } } } const activityUpdatePromise: Promise = (async () => { if (activityUpdates.length === 0) { return undefined; } return await activityUpdater(viewer, { updates: activityUpdates }); })(); const serverRequests: Array = []; - const checkOneTimeKeysPromise = (async () => { - if (!viewer.loggedIn) { - return; - } - const enoughOneTimeKeys = await checkIfSessionHasEnoughOneTimeKeys( - viewer.session, - ); - if (!enoughOneTimeKeys) { - serverRequests.push({ type: serverRequestTypes.MORE_ONE_TIME_KEYS }); - } - })(); - const { activityUpdateResult } = await promiseAll({ all: Promise.all(promises), activityUpdateResult: activityUpdatePromise, - checkOneTimeKeysPromise, }); if ( !stateCheckStatus && viewer.loggedIn && viewer.sessionLastValidated + sessionCheckFrequency < Date.now() ) { stateCheckStatus = { status: 'state_check' }; } if (viewerMissingPlatform) { serverRequests.push({ type: serverRequestTypes.PLATFORM }); } if (viewerMissingPlatformDetails) { serverRequests.push({ type: serverRequestTypes.PLATFORM_DETAILS }); } return { serverRequests, stateCheckStatus, activityUpdateResult }; } async function recordThreadInconsistency( viewer: Viewer, response: ThreadInconsistencyClientResponse, ): Promise { const { type, ...rest } = response; const reportCreationRequest = ({ ...rest, type: reportTypes.THREAD_INCONSISTENCY, }: ThreadInconsistencyReportCreationRequest); await createReport(viewer, reportCreationRequest); } async function recordEntryInconsistency( viewer: Viewer, response: EntryInconsistencyClientResponse, ): Promise { const { type, ...rest } = response; const reportCreationRequest = ({ ...rest, type: reportTypes.ENTRY_INCONSISTENCY, }: EntryInconsistencyReportCreationRequest); await createReport(viewer, reportCreationRequest); } type SessionInitializationResult = | { sessionContinued: false } | { sessionContinued: true, deltaEntryInfoResult: DeltaEntryInfosResponse, sessionUpdate: SessionUpdate, }; async function initializeSession( viewer: Viewer, calendarQuery: CalendarQuery, oldLastUpdate: number, ): Promise { if (!viewer.loggedIn) { return { sessionContinued: false }; } if (!viewer.hasSessionInfo) { // If the viewer has no session info but is logged in, that is indicative // of an expired / invalidated session and we should generate a new one await setNewSession(viewer, calendarQuery, oldLastUpdate); return { sessionContinued: false }; } if (oldLastUpdate < viewer.sessionLastUpdated) { // If the client has an older last_update than the server is tracking for // that client, then the client either had some issue persisting its store, // or the user restored the client app from a backup. Either way, we should // invalidate the existing session, since the server has assumed that the // checkpoint is further along than it is on the client, and might not still // have all of the updates necessary to do an incremental update await setNewSession(viewer, calendarQuery, oldLastUpdate); return { sessionContinued: false }; } let comparisonResult = null; try { comparisonResult = compareNewCalendarQuery(viewer, calendarQuery); } catch (e) { if (e.message !== 'unknown_error') { throw e; } } if (comparisonResult) { const { difference, oldCalendarQuery } = comparisonResult; const sessionUpdate = { ...comparisonResult.sessionUpdate, lastUpdate: oldLastUpdate, }; const deltaEntryInfoResult = await fetchEntriesForSession( viewer, difference, oldCalendarQuery, ); return { sessionContinued: true, deltaEntryInfoResult, sessionUpdate }; } else { await setNewSession(viewer, calendarQuery, oldLastUpdate); return { sessionContinued: false }; } } type StateCheckResult = { sessionUpdate?: SessionUpdate, checkStateRequest?: ServerCheckStateServerRequest, }; async function checkState( viewer: Viewer, status: StateCheckStatus, ): Promise { if (status.status === 'state_validated') { return { sessionUpdate: { lastValidated: Date.now() } }; } else if (status.status === 'state_check') { const promises = Object.fromEntries( values(serverStateSyncSpecs).map(spec => [ spec.hashKey, (async () => { if ( !hasMinCodeVersion(viewer.platformDetails, { native: 267, web: 32, }) ) { const data = await spec.fetch(viewer); return hash(data); } const infosHash = await spec.fetchServerInfosHash(viewer); return infosHash; })(), ]), ); const hashesToCheck = await promiseAll(promises); const checkStateRequest = { type: serverRequestTypes.CHECK_STATE, hashesToCheck, }; return { checkStateRequest }; } const invalidKeys = new Set(status.invalidKeys); const shouldFetchAll = Object.fromEntries( values(serverStateSyncSpecs).map(spec => [ spec.hashKey, invalidKeys.has(spec.hashKey), ]), ); const idsToFetch = Object.fromEntries( values(serverStateSyncSpecs) .filter(spec => spec.innerHashSpec?.hashKey) .map(spec => [spec.innerHashSpec?.hashKey, new Set()]), ); for (const key of invalidKeys) { const [innerHashKey, id] = key.split('|'); if (innerHashKey && id) { idsToFetch[innerHashKey]?.add(id); } } const fetchPromises: { [string]: Promise } = {}; for (const spec of values(serverStateSyncSpecs)) { if (shouldFetchAll[spec.hashKey]) { fetchPromises[spec.hashKey] = spec.fetch(viewer); } else if (idsToFetch[spec.innerHashSpec?.hashKey]?.size > 0) { fetchPromises[spec.hashKey] = spec.fetch( viewer, idsToFetch[spec.innerHashSpec?.hashKey], ); } } const fetchedData = await promiseAll(fetchPromises); const specPerHashKey = Object.fromEntries( values(serverStateSyncSpecs).map(spec => [spec.hashKey, spec]), ); const specPerInnerHashKey = Object.fromEntries( values(serverStateSyncSpecs) .filter(spec => spec.innerHashSpec?.hashKey) .map(spec => [spec.innerHashSpec?.hashKey, spec]), ); const hashesToCheck: { [string]: number } = {}, failUnmentioned: { [string]: boolean } = {}, stateChanges: { [string]: mixed } = {}; for (const key of invalidKeys) { const spec = specPerHashKey[key]; const innerHashKey = spec?.innerHashSpec?.hashKey; const isTopLevelKey = !!spec; if (isTopLevelKey && innerHashKey) { // Instead of returning all the infos, we want to narrow down and figure // out which infos don't match first const infos = fetchedData[key]; // We have a type error here because in fact the relationship between // Infos and Info is not guaranteed to be like this. In particular, // currentUserStateSyncSpec does not match this pattern. But this code // doesn't fire for it because no innerHashSpec is defined const iterableInfos: { +[string]: mixed } = (infos: any); for (const infoID in iterableInfos) { let hashValue; if ( hasMinCodeVersion(viewer.platformDetails, { native: 267, web: 32, }) ) { // We have a type error here because Flow has no way to determine that // spec and infos are matched up hashValue = await spec.getServerInfoHash( (iterableInfos[infoID]: any), ); } else { hashValue = hash(iterableInfos[infoID]); } hashesToCheck[`${innerHashKey}|${infoID}`] = hashValue; } failUnmentioned[key] = true; } else if (isTopLevelKey) { stateChanges[key] = fetchedData[key]; } else { const [keyPrefix, id] = key.split('|'); const innerSpec = specPerInnerHashKey[keyPrefix]; const innerHashSpec = innerSpec?.innerHashSpec; if (!innerHashSpec || !id) { continue; } const infos = fetchedData[innerSpec.hashKey]; // We have a type error here because in fact the relationship between // Infos and Info is not guaranteed to be like this. In particular, // currentUserStateSyncSpec does not match this pattern. But this code // doesn't fire for it because no innerHashSpec is defined const iterableInfos: { +[string]: mixed } = (infos: any); const info = iterableInfos[id]; // We have a type error here because Flow wants us to type iterableInfos // in this file, but we don't have access to the parameterization of // innerHashSpec here if (!info || innerHashSpec.additionalDeleteCondition?.((info: any))) { if (!stateChanges[innerHashSpec.deleteKey]) { stateChanges[innerHashSpec.deleteKey] = [id]; } else { // We have a type error here because in fact stateChanges values // aren't always arrays. In particular, currentUserStateSyncSpec does // not match this pattern. But this code doesn't fire for it because // no innerHashSpec is defined const curDeleteKeyChanges: Array = (stateChanges[ innerHashSpec.deleteKey ]: any); curDeleteKeyChanges.push(id); } continue; } if (!stateChanges[innerHashSpec.rawInfosKey]) { stateChanges[innerHashSpec.rawInfosKey] = [info]; } else { // We have a type error here because in fact stateChanges values aren't // always arrays. In particular, currentUserStateSyncSpec does not match // this pattern. But this code doesn't fire for it because no // innerHashSpec is defined const curRawInfosKeyChanges: Array = (stateChanges[ innerHashSpec.rawInfosKey ]: any); curRawInfosKeyChanges.push(info); } } } // We have a type error here because the keys that get set on some of these // collections aren't statically typed when they're set. Rather, they are set // as arbitrary strings const checkStateRequest: ServerCheckStateServerRequest = ({ type: serverRequestTypes.CHECK_STATE, hashesToCheck, failUnmentioned, stateChanges, }: any); if (Object.keys(hashesToCheck).length === 0) { return { checkStateRequest, sessionUpdate: { lastValidated: Date.now() } }; } else { return { checkStateRequest }; } } export { clientResponseInputValidator, processClientResponses, initializeSession, checkState, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index 56a161439..5a25577e9 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,867 +1,852 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { preRequestUserStateSelector } from '../selectors/account-selectors.js'; import { getOneTimeKeyValuesFromBlob } from '../shared/crypto-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, LogInRequest, KeyserverAuthResult, KeyserverAuthInfo, KeyserverAuthRequest, ClientLogInResponse, KeyserverLogOutResult, LogOutResult, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; import type { IdentityAuthResult } from '../types/identity-service-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; -import type { - GetSessionPublicKeysArgs, - GetOlmSessionInitializationDataResponse, -} from '../types/request-types.js'; +import type { GetOlmSessionInitializationDataResponse } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; -import type { - SessionPublicKeys, - PreRequestUserState, -} from '../types/session-types.js'; +import type { PreRequestUserState } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; import type { CurrentUserInfo, UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../utils/call-single-keyserver-endpoint.js'; import { getConfig } from '../utils/config.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; export type KeyserverLogOutInput = { +preRequestUserState: PreRequestUserState, +keyserverIDs?: $ReadOnlyArray, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState, keyserverIDs }; }; function useLogOut(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverLogOut = useKeyserverCall(logOut); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback( async (keyserverIDs?: $ReadOnlyArray) => { const { keyserverIDs: _, ...result } = await callKeyserverLogOut({ preRequestUserState, keyserverIDs, }); return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [callKeyserverLogOut, commServicesAccessToken, preRequestUserState], ); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallSingleKeyserverEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { const requests = { [authoritativeKeyserverID()]: {} }; const responses = await callKeyserverEndpoint('claim_username', requests, { ...claimUsernameCallSingleKeyserverEndpointOptions, }); const response = responses[authoritativeKeyserverID()]; return { message: response.message, signature: response.signature, }; }; function useClaimUsername(): () => Promise { return useKeyserverCall(claimUsername); } const deleteKeyserverAccountActionTypes = Object.freeze({ started: 'DELETE_KEYSERVER_ACCOUNT_STARTED', success: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', failed: 'DELETE_KEYSERVER_ACCOUNT_FAILED', }); const deleteKeyserverAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState, keyserverIDs, }; }; function useDeleteKeyserverAccount(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); return React.useCallback( (keyserverIDs?: $ReadOnlyArray) => callKeyserverDeleteAccount({ preRequestUserState, keyserverIDs }), [callKeyserverDeleteAccount, preRequestUserState], ); } const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); function useDeleteAccount(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(async () => { const identityPromise = (async () => { if (!usingCommServicesAccessToken) { return undefined; } if (!identityClient) { throw new Error('Identity service client is not initialized'); } return await identityClient.deleteUser(); })(); const [keyserverResult] = await Promise.all([ callKeyserverDeleteAccount({ preRequestUserState, }), identityPromise, ]); const { keyserverIDs: _, ...result } = keyserverResult; return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [ callKeyserverDeleteAccount, commServicesAccessToken, identityClient, preRequestUserState, ]); } const keyserverRegisterActionTypes = Object.freeze({ started: 'KEYSERVER_REGISTER_STARTED', success: 'KEYSERVER_REGISTER_SUCCESS', failed: 'KEYSERVER_REGISTER_FAILED', }); const registerCallSingleKeyserverEndpointOptions = { timeout: 60000 }; const keyserverRegister = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( registerInfo: RegisterInfo, options?: CallSingleKeyserverEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[authoritativeKeyserverID()]; const response = await callSingleKeyserverEndpoint( 'create_account', { ...registerInfo, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...registerCallSingleKeyserverEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; export type KeyserverAuthInput = $ReadOnly<{ ...KeyserverAuthInfo, +preRequestUserInfo: ?CurrentUserInfo, }>; const keyserverAuthActionTypes = Object.freeze({ started: 'KEYSERVER_AUTH_STARTED', success: 'KEYSERVER_AUTH_SUCCESS', failed: 'KEYSERVER_AUTH_FAILED', }); const keyserverAuthCallSingleKeyserverEndpointOptions = { timeout: 60000 }; const keyserverAuth = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: KeyserverAuthInput) => Promise) => async keyserverAuthInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverData, deviceTokenUpdateInput, preRequestUserInfo, ...restLogInInfo } = keyserverAuthInfo; const keyserverIDs = Object.keys(keyserverData); const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: KeyserverAuthRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID], watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, initialContentEncryptedMessage: keyserverData[keyserverID].initialContentEncryptedMessage, initialNotificationsEncryptedMessage: keyserverData[keyserverID].initialNotificationsEncryptedMessage, source: logInActionSource, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'keyserver_auth', requests, keyserverAuthCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: keyserverAuthInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()]?.currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, logInActionSource: keyserverAuthInfo.logInActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()].notAcknowledgedPolicies, preRequestUserInfo, }; }; function useKeyserverAuth(): ( input: KeyserverAuthInfo, ) => Promise { const preRequestUserInfo = useSelector(state => state.currentUserInfo); const callKeyserverAuth = useKeyserverCall(keyserverAuth); return React.useCallback( (input: KeyserverAuthInfo) => callKeyserverAuth({ preRequestUserInfo, ...input }), [callKeyserverAuth, preRequestUserInfo], ); } const identityRegisterActionTypes = Object.freeze({ started: 'IDENTITY_REGISTER_STARTED', success: 'IDENTITY_REGISTER_SUCCESS', failed: 'IDENTITY_REGISTER_FAILED', }); function useIdentityPasswordRegister(): ( username: string, password: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerPasswordUser) { throw new Error('Register password user method unimplemented'); } return identityClient.registerPasswordUser; } function useIdentityWalletRegister(): ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerWalletUser) { throw new Error('Register wallet user method unimplemented'); } return identityClient.registerWalletUser; } const identityGenerateNonceActionTypes = Object.freeze({ started: 'IDENTITY_GENERATE_NONCE_STARTED', success: 'IDENTITY_GENERATE_NONCE_SUCCESS', failed: 'IDENTITY_GENERATE_NONCE_FAILED', }); function useIdentityGenerateNonce(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.generateNonce; } function mergeUserInfos( ...userInfoArrays: Array<$ReadOnlyArray> ): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } type WritableGenericMessagesResult = { messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: string[], currentAsOf: { [keyserverID: string]: number }, }; type WritableCalendarResult = { rawEntryInfos: RawEntryInfo[], calendarQuery: CalendarQuery, }; const identityLogInActionTypes = Object.freeze({ started: 'IDENTITY_LOG_IN_STARTED', success: 'IDENTITY_LOG_IN_SUCCESS', failed: 'IDENTITY_LOG_IN_FAILED', }); function useIdentityPasswordLogIn(): ( username: string, password: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; return React.useCallback( (username, password) => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } return identityClient.logInPasswordUser(username, password); }, [identityClient], ); } function useIdentityWalletLogIn(): ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.logInWalletUser; } const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallSingleKeyserverEndpointOptions = { timeout: 60000 }; const logIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [authoritativeKeyserverID()]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: logInActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'log_in', requests, logInCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()].notAcknowledgedPolicies, }; }; function useLogIn(): (input: LogInInfo) => Promise { return useKeyserverCall(logIn); } const changeKeyserverUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', success: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', failed: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', }); const changeKeyserverUserPassword = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callSingleKeyserverEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callSingleKeyserverEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((username: string) => Promise) => async username => { const response = await callSingleKeyserverEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; function useUpdateSubscription(): ( input: SubscriptionUpdateRequest, ) => Promise { return useKeyserverCall(updateSubscription); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } -const getSessionPublicKeys = - ( - callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, - ): ((data: GetSessionPublicKeysArgs) => Promise) => - async data => { - return await callSingleKeyserverEndpoint('get_session_public_keys', data); - }; - const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( options?: ?CallSingleKeyserverEndpointOptions, ) => Promise) => async options => { const olmInitData = await callSingleKeyserverEndpoint( 'get_olm_session_initialization_data', {}, options, ); return { signedIdentityKeysBlob: olmInitData.signedIdentityKeysBlob, contentInitializationInfo: { ...olmInitData.contentInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.contentInitializationInfo.oneTimeKey, )[0], }, notifInitializationInfo: { ...olmInitData.notifInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.notifInitializationInfo.oneTimeKey, )[0], }, }; }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callSingleKeyserverEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callSingleKeyserverEndpoint('update_user_avatar', avatarDBContent); return { updates }; }; const resetUserStateActionType = 'RESET_USER_STATE'; const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, - getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn as logInRawAction, identityLogInActionTypes, useIdentityPasswordLogIn, useIdentityWalletLogIn, useLogIn, logInActionTypes, useLogOut, logOutActionTypes, keyserverRegister, keyserverRegisterActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, deleteAccountActionTypes, useDeleteAccount, keyserverAuthActionTypes, useKeyserverAuth, identityRegisterActionTypes, useIdentityPasswordRegister, useIdentityWalletRegister, identityGenerateNonceActionTypes, useIdentityGenerateNonce, }; diff --git a/lib/selectors/socket-selectors.js b/lib/selectors/socket-selectors.js index 0e7f337fa..221ad84f7 100644 --- a/lib/selectors/socket-selectors.js +++ b/lib/selectors/socket-selectors.js @@ -1,268 +1,252 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, cookieSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; import { createOpenSocketFunction } from '../shared/socket-utils.js'; import type { BoundStateSyncSpec } from '../shared/state-sync/state-sync-spec.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { SignedIdentityKeysBlob } from '../types/crypto-types.js'; import { type CalendarQuery } from '../types/entry-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, type ClientServerRequest, type ClientClientResponse, } from '../types/request-types.js'; import type { SessionState } from '../types/session-types.js'; -import type { OneTimeKeyGenerator } from '../types/socket-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; -import { minimumOneTimeKeysRequired } from '../utils/crypto-utils.js'; import { values } from '../utils/objects.js'; const baseOpenSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = keyserverID => createSelector( urlPrefixSelector(keyserverID), // We don't actually use the cookie in the socket open function, // but we do use it in the initial message, and when the cookie changes // the socket needs to be reopened. By including the cookie here, // whenever the cookie changes this function will change, // which tells the Socket component to restart the connection. cookieSelector(keyserverID), (urlPrefix: ?string) => { if (!urlPrefix) { return null; } return createOpenSocketFunction(urlPrefix); }, ); const openSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = _memoize(baseOpenSocketSelector); const queuedReports: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.reportStore.queuedReports, ( mainQueuedReports: $ReadOnlyArray, ): $ReadOnlyArray => mainQueuedReports, ); // We pass all selectors specified in stateSyncSpecs and get the resulting // BoundStateSyncSpecs in the specs array. We do it so we don't have to // modify the selector when we add a new spec. type BoundStateSyncSpecs = { +specsPerHashKey: { +[string]: BoundStateSyncSpec }, +specPerInnerHashKey: { +[string]: BoundStateSyncSpec }, }; const stateSyncSpecSelectors = values(stateSyncSpecs).map( spec => spec.selector, ); const boundStateSyncSpecsSelector: AppState => BoundStateSyncSpecs = // The FlowFixMe is needed because createSelector types require flow // to know the number of subselectors at compile time. // $FlowFixMe createSelector(stateSyncSpecSelectors, (...specs) => { const boundSpecs = (specs: BoundStateSyncSpec[]); // We create a map from `hashKey` to a given spec for easier lookup later const specsPerHashKey = Object.fromEntries( boundSpecs.map(spec => [spec.hashKey, spec]), ); // We do the same for innerHashKey const specPerInnerHashKey = Object.fromEntries( boundSpecs .filter(spec => spec.innerHashSpec?.hashKey) .map(spec => [spec.innerHashSpec?.hashKey, spec]), ); return { specsPerHashKey, specPerInnerHashKey }; }); const getClientResponsesSelector: ( state: AppState, keyserverID: string, ) => ( calendarActive: boolean, - oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: () => Promise, getInitialNotificationsEncryptedMessage: ?( keyserverID: string, ) => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( boundStateSyncSpecsSelector, currentCalendarQuery, (state: AppState, keyserverID: string) => keyserverID, ( boundStateSyncSpecs: BoundStateSyncSpecs, calendarQuery: (calendarActive: boolean) => CalendarQuery, // eslint-disable-next-line no-unused-vars keyserverID: string, ) => { return async ( calendarActive: boolean, - oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: () => Promise, getInitialNotificationsEncryptedMessage: ?( keyserverID: string, ) => Promise, serverRequests: $ReadOnlyArray, ): Promise<$ReadOnlyArray> => { const clientResponses = []; const serverRequestedPlatformDetails = serverRequests.some( request => request.type === serverRequestTypes.PLATFORM_DETAILS, ); const { specsPerHashKey, specPerInnerHashKey } = boundStateSyncSpecs; for (const serverRequest of serverRequests) { if ( serverRequest.type === serverRequestTypes.PLATFORM && !serverRequestedPlatformDetails ) { clientResponses.push({ type: serverRequestTypes.PLATFORM, platform: getConfig().platformDetails.platform, }); } else if (serverRequest.type === serverRequestTypes.PLATFORM_DETAILS) { clientResponses.push({ type: serverRequestTypes.PLATFORM_DETAILS, platformDetails: getConfig().platformDetails, }); } else if (serverRequest.type === serverRequestTypes.CHECK_STATE) { const query = calendarQuery(calendarActive); const hashResults: { [string]: boolean } = {}; for (const key in serverRequest.hashesToCheck) { const expectedHashValue = serverRequest.hashesToCheck[key]; let hashValue; const [specKey, id] = key.split('|'); if (id) { hashValue = specPerInnerHashKey[specKey]?.getInfoHash(id); } else { hashValue = specsPerHashKey[specKey]?.getAllInfosHash(query); } // If hashValue values is null then we are still calculating // the hashes in the background. In this case we return true // to skip this state check. Future state checks (after the hash // calculation complete) will be handled normally. if (!hashValue) { hashResults[key] = true; } else { hashResults[key] = expectedHashValue === hashValue; } } const { failUnmentioned } = serverRequest; for (const spec of values(specPerInnerHashKey)) { const innerHashKey = spec.innerHashSpec?.hashKey; if (!failUnmentioned?.[spec.hashKey] || !innerHashKey) { continue; } const ids = spec.getIDs(query); if (!ids) { continue; } for (const id of ids) { const key = `${innerHashKey}|${id}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } clientResponses.push({ type: serverRequestTypes.CHECK_STATE, hashResults, }); - } else if ( - serverRequest.type === serverRequestTypes.MORE_ONE_TIME_KEYS && - oneTimeKeyGenerator - ) { - const keys: string[] = []; - for (let i = 0; i < minimumOneTimeKeysRequired; ++i) { - keys.push(oneTimeKeyGenerator(i)); - } - clientResponses.push({ - type: serverRequestTypes.MORE_ONE_TIME_KEYS, - keys, - }); } else if ( serverRequest.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB ) { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); clientResponses.push({ type: serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, signedIdentityKeysBlob, }); } else if ( serverRequest.type === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE && getInitialNotificationsEncryptedMessage ) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage( authoritativeKeyserverID(), ); clientResponses.push({ type: serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, initialNotificationsEncryptedMessage, }); } } return clientResponses; }; }, ); const baseSessionStateFuncSelector: ( keyserverID: string, ) => ( state: AppState, ) => (calendarActive: boolean) => SessionState = keyserverID => createSelector( currentAsOfSelector(keyserverID), updatesCurrentAsOfSelector(keyserverID), currentCalendarQuery, ( messagesCurrentAsOf: number, updatesCurrentAsOf: number, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => (calendarActive: boolean): SessionState => ({ calendarQuery: calendarQuery(calendarActive), messagesCurrentAsOf, updatesCurrentAsOf, watchedIDs: threadWatcher.getWatchedIDs(), }), ); const sessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => (calendarActive: boolean) => SessionState = _memoize( baseSessionStateFuncSelector, ); export { openSocketSelector, queuedReports, getClientResponsesSelector, sessionStateFuncSelector, }; diff --git a/lib/types/request-types.js b/lib/types/request-types.js index 969dbf906..c0dc31b92 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,311 +1,295 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type ActivityUpdate } from './activity-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { signedIdentityKeysBlobValidator } from './crypto-types.js'; import type { Platform, PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery, rawEntryInfoValidator, } from './entry-types.js'; import type { RawThreadInfo } from './minimally-encoded-thread-permissions-types'; import type { ThreadInconsistencyReportShape, EntryInconsistencyReportShape, ClientThreadInconsistencyReportShape, ClientEntryInconsistencyReportShape, } from './report-types.js'; import type { LegacyRawThreadInfo } from './thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type AccountUserInfo, accountUserInfoValidator, } from './user-types.js'; import { mixedRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; // "Server requests" are requests for information that the server delivers to // clients. Clients then respond to those requests with a "client response". export const serverRequestTypes = Object.freeze({ PLATFORM: 0, //DEVICE_TOKEN: 1, (DEPRECATED) THREAD_INCONSISTENCY: 2, PLATFORM_DETAILS: 3, //INITIAL_ACTIVITY_UPDATE: 4, (DEPRECATED) ENTRY_INCONSISTENCY: 5, CHECK_STATE: 6, INITIAL_ACTIVITY_UPDATES: 7, - MORE_ONE_TIME_KEYS: 8, + // MORE_ONE_TIME_KEYS: 8, (DEPRECATED) SIGNED_IDENTITY_KEYS_BLOB: 9, INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE: 10, }); type ServerRequestType = $Values; export function assertServerRequestType( serverRequestType: number, ): ServerRequestType { invariant( serverRequestType === 0 || serverRequestType === 2 || serverRequestType === 3 || serverRequestType === 5 || serverRequestType === 6 || serverRequestType === 7 || - serverRequestType === 8 || serverRequestType === 9 || serverRequestType === 10, 'number is not ServerRequestType enum', ); return serverRequestType; } type PlatformServerRequest = { +type: 0, }; const platformServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM), }); type PlatformClientResponse = { +type: 0, +platform: Platform, }; export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; type PlatformDetailsServerRequest = { type: 3, }; const platformDetailsServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), }); type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; type FailUnmentioned = Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>; type StateChanges = Partial<{ +rawThreadInfos: LegacyRawThreadInfo[] | RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>; export type ServerCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: FailUnmentioned, +stateChanges?: StateChanges, }; const serverCheckStateServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashesToCheck: t.dict(t.String, t.Number), failUnmentioned: t.maybe( tShape({ threadInfos: t.maybe(t.Boolean), entryInfos: t.maybe(t.Boolean), userInfos: t.maybe(t.Boolean), }), ), stateChanges: t.maybe( tShape({ rawThreadInfos: t.maybe(t.list(mixedRawThreadInfoValidator)), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), currentUserInfo: t.maybe(currentUserInfoValidator), userInfos: t.maybe(t.list(accountUserInfoValidator)), deleteThreadIDs: t.maybe(t.list(tID)), deleteEntryIDs: t.maybe(t.list(tID)), deleteUserInfoIDs: t.maybe(t.list(t.String)), }), ), }); type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; -type MoreOneTimeKeysServerRequest = { - +type: 8, -}; -const moreOneTimeKeysServerRequestValidator = - tShape({ - type: tNumber(serverRequestTypes.MORE_ONE_TIME_KEYS), - }); - type MoreOneTimeKeysClientResponse = { +type: 8, +keys: $ReadOnlyArray, }; type SignedIdentityKeysBlobServerRequest = { +type: 9, }; const signedIdentityKeysBlobServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), }); type SignedIdentityKeysBlobClientResponse = { +type: 9, +signedIdentityKeysBlob: SignedIdentityKeysBlob, }; type InitialNotificationsEncryptedMessageServerRequest = { +type: 10, }; const initialNotificationsEncryptedMessageServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), }); type InitialNotificationsEncryptedMessageClientResponse = { +type: 10, +initialNotificationsEncryptedMessage: string, }; export type ServerServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ServerCheckStateServerRequest - | MoreOneTimeKeysServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; export const serverServerRequestValidator: TUnion = t.union([ platformServerRequestValidator, platformDetailsServerRequestValidator, serverCheckStateServerRequestValidator, - moreOneTimeKeysServerRequestValidator, signedIdentityKeysBlobServerRequestValidator, initialNotificationsEncryptedMessageServerRequestValidator, ]); export type ClientResponse = | PlatformClientResponse | ThreadInconsistencyClientResponse | PlatformDetailsClientResponse | EntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Partial<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; export type ClientServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ClientCheckStateServerRequest - | MoreOneTimeKeysServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; // This is just the client variant of ClientResponse. The server needs to handle // multiple client versions so the type supports old versions of certain client // responses, but the client variant only need to support the latest version. type ClientThreadInconsistencyClientResponse = { ...ClientThreadInconsistencyReportShape, +type: 2, }; type ClientEntryInconsistencyClientResponse = { +type: 5, ...ClientEntryInconsistencyReportShape, }; export type ClientClientResponse = | PlatformClientResponse | ClientThreadInconsistencyClientResponse | PlatformDetailsClientResponse | ClientEntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientInconsistencyResponse = | ClientThreadInconsistencyClientResponse | ClientEntryInconsistencyClientResponse; export const processServerRequestsActionType = 'PROCESS_SERVER_REQUESTS'; export type ProcessServerRequestsPayload = { +serverRequests: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type ProcessServerRequestAction = { +type: 'PROCESS_SERVER_REQUESTS', +payload: ProcessServerRequestsPayload, }; -export type GetSessionPublicKeysArgs = { - +session: string, -}; - export type OlmSessionInitializationInfo = { +prekey: string, +prekeySignature: string, +oneTimeKey: string, }; export const olmSessionInitializationInfoValidator: TInterface = tShape({ prekey: t.String, prekeySignature: t.String, oneTimeKey: t.String, }); export type GetOlmSessionInitializationDataResponse = { +signedIdentityKeysBlob: SignedIdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, }; export const getOlmSessionInitializationDataResponseValidator: TInterface = tShape({ signedIdentityKeysBlob: signedIdentityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, }); diff --git a/lib/types/session-types.js b/lib/types/session-types.js index b43019141..a8fe9b023 100644 --- a/lib/types/session-types.js +++ b/lib/types/session-types.js @@ -1,114 +1,100 @@ // @flow -import t, { type TInterface } from 'tcomb'; - import type { LogInActionSource } from './account-types.js'; import type { CalendarQuery } from './entry-types.js'; import type { MixedRawThreadInfos } from './thread-types.js'; import { type UserInfo, type CurrentUserInfo, type LoggedOutUserInfo, } from './user-types.js'; -import { tShape } from '../utils/validation-utils.js'; export const cookieLifetime = 30 * 24 * 60 * 60 * 1000; // in milliseconds // Interval the server waits after a state check before starting a new one export const sessionCheckFrequency = 3 * 60 * 1000; // in milliseconds // How long the server debounces after activity before initiating a state check export const stateCheckInactivityActivationInterval = 3 * 1000; // in milliseconds // On native, we use the cookieID as a unique session identifier. This is // because there is no way to have two instances of an app running. On the other // hand, on web it is possible to have two sessions open using the same cookie, // so we have a unique sessionID specified in the request body. export const sessionIdentifierTypes = Object.freeze({ COOKIE_ID: 0, BODY_SESSION_ID: 1, }); export type SessionIdentifierType = $Values; export const cookieTypes = Object.freeze({ USER: 'user', ANONYMOUS: 'anonymous', }); export type CookieType = $Values; export type ServerSessionChange = | { cookieInvalidated: false, threadInfos: MixedRawThreadInfos, userInfos: $ReadOnlyArray, sessionID?: null | string, cookie?: string, } | { cookieInvalidated: true, threadInfos: MixedRawThreadInfos, userInfos: $ReadOnlyArray, currentUserInfo: LoggedOutUserInfo, sessionID?: null | string, cookie?: string, }; export type ClientSessionChange = | { +cookieInvalidated: false, +currentUserInfo?: ?CurrentUserInfo, +sessionID?: null | string, +cookie?: string, } | { +cookieInvalidated: true, +currentUserInfo: LoggedOutUserInfo, +sessionID?: null | string, +cookie?: string, }; export type PreRequestUserKeyserverSessionInfo = { +cookie: ?string, +sessionID: ?string, }; export type PreRequestUserState = { +currentUserInfo: ?CurrentUserInfo, +cookiesAndSessions: { +[keyserverID: string]: PreRequestUserKeyserverSessionInfo, }, }; export type IdentityCallPreRequestUserState = $ReadOnly<{ ...PreRequestUserState, +commServicesAccessToken: ?string, }>; export type SetSessionPayload = { +sessionChange: ClientSessionChange, +preRequestUserState: ?PreRequestUserState, +error: ?string, +logInActionSource: ?LogInActionSource, +keyserverID: string, }; export type SessionState = { calendarQuery: CalendarQuery, messagesCurrentAsOf: number, updatesCurrentAsOf: number, watchedIDs: $ReadOnlyArray, }; export type SessionIdentification = Partial<{ cookie: ?string, sessionID: ?string, }>; - -export type SessionPublicKeys = { - +identityKey: string, - +oneTimeKey?: string, -}; - -export const sessionPublicKeysValidator: TInterface = - tShape({ - identityKey: t.String, - oneTimeKey: t.maybe(t.String), - }); diff --git a/lib/utils/crypto-utils.js b/lib/utils/crypto-utils.js index e519b4549..4f3b3f776 100644 --- a/lib/utils/crypto-utils.js +++ b/lib/utils/crypto-utils.js @@ -1,36 +1,30 @@ // @flow import t from 'tcomb'; import { type TInterface } from 'tcomb'; import { primaryIdentityPublicKeyRegex } from './siwe-utils.js'; import { tRegex, tShape } from './validation-utils.js'; import type { IdentityKeysBlob, OLMIdentityKeys, SignedIdentityKeysBlob, } from '../types/crypto-types'; -const minimumOneTimeKeysRequired = 10; - const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); const olmIdentityKeysValidator: TInterface = tShape({ ed25519: tRegex(primaryIdentityPublicKeyRegex), curve25519: tRegex(primaryIdentityPublicKeyRegex), }); const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); -export { - minimumOneTimeKeysRequired, - signedIdentityKeysBlobValidator, - identityKeysBlobValidator, -}; +export { signedIdentityKeysBlobValidator, identityKeysBlobValidator }; diff --git a/native/selectors/socket-selectors.js b/native/selectors/socket-selectors.js index 92ce3944e..76512a576 100644 --- a/native/selectors/socket-selectors.js +++ b/native/selectors/socket-selectors.js @@ -1,130 +1,116 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; -import type { OneTimeKeyGenerator } from 'lib/types/socket-types.js'; import { commCoreModule } from '../native-modules.js'; import { calendarActiveSelector } from '../navigation/nav-selectors.js'; import type { AppState } from '../redux/state-types.js'; import type { NavPlusRedux } from '../types/selector-types.js'; const baseSessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = keyserverID => createSelector( cookieSelector(keyserverID), (cookie: ?string): SessionIdentification => ({ cookie }), ); const sessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = _memoize( baseSessionIdentificationSelector, ); -function oneTimeKeyGenerator(inc: number): string { - // todo replace this hard code with something like - // commCoreModule.generateOneTimeKeys() - let str = Date.now().toString() + '_' + inc.toString() + '_'; - while (str.length < 43) { - str += Math.random().toString(36).substr(2, 5); - } - str = str.substr(0, 43); - return str; -} - async function getSignedIdentityKeysBlob(): Promise { await commCoreModule.initializeCryptoAccount(); const { blobPayload, signature } = await commCoreModule.getUserPublicKey(); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: blobPayload, signature, }; return signedIdentityKeysBlob; } type NativeGetClientResponsesSelectorInputType = $ReadOnly<{ ...NavPlusRedux, getInitialNotificationsEncryptedMessage: ( keyserverID: string, ) => Promise, keyserverID: string, }>; const nativeGetClientResponsesSelector: ( input: NativeGetClientResponsesSelectorInputType, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (input: NativeGetClientResponsesSelectorInputType) => getClientResponsesSelector(input.redux, input.keyserverID), (input: NativeGetClientResponsesSelectorInputType) => calendarActiveSelector(input.navContext), (input: NativeGetClientResponsesSelectorInputType) => input.getInitialNotificationsEncryptedMessage, ( getClientResponsesFunc: ( calendarActive: boolean, - oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: () => Promise, getInitialNotificationsEncryptedMessage: ?( keyserverID: string, ) => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, calendarActive: boolean, getInitialNotificationsEncryptedMessage: ( keyserverID: string, ) => Promise, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, - oneTimeKeyGenerator, getSignedIdentityKeysBlob, getInitialNotificationsEncryptedMessage, serverRequests, ), ); const baseNativeSessionStateFuncSelector: ( keyserverID: string, ) => (input: NavPlusRedux) => () => SessionState = keyserverID => createSelector( (input: NavPlusRedux) => sessionStateFuncSelector(keyserverID)(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const nativeSessionStateFuncSelector: ( keyserverID: string, ) => (input: NavPlusRedux) => () => SessionState = _memoize( baseNativeSessionStateFuncSelector, ); export { sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, }; diff --git a/web/selectors/socket-selectors.js b/web/selectors/socket-selectors.js index 4c3181d23..86ccb1594 100644 --- a/web/selectors/socket-selectors.js +++ b/web/selectors/socket-selectors.js @@ -1,117 +1,114 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { sessionIDSelector, cookieSelector, } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; -import type { OneTimeKeyGenerator } from 'lib/types/socket-types.js'; import type { AppState } from '../redux/redux-setup.js'; const baseSessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = keyserverID => createSelector( cookieSelector(keyserverID), sessionIDSelector(keyserverID), (cookie: ?string, sessionID: ?string): SessionIdentification => ({ cookie, sessionID, }), ); const sessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = _memoize( baseSessionIdentificationSelector, ); type WebGetClientResponsesSelectorInputType = { +state: AppState, +getSignedIdentityKeysBlob: () => Promise, +getInitialNotificationsEncryptedMessage: ( keyserverID: string, ) => Promise, +keyserverID: string, }; const webGetClientResponsesSelector: ( input: WebGetClientResponsesSelectorInputType, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (input: WebGetClientResponsesSelectorInputType) => getClientResponsesSelector(input.state, input.keyserverID), (input: WebGetClientResponsesSelectorInputType) => input.getSignedIdentityKeysBlob, (input: WebGetClientResponsesSelectorInputType) => input.state.navInfo.tab === 'calendar', (input: WebGetClientResponsesSelectorInputType) => input.getInitialNotificationsEncryptedMessage, ( getClientResponsesFunc: ( calendarActive: boolean, - oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: () => Promise, getInitialNotificationsEncryptedMessage: ( keyserverID: string, ) => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, getSignedIdentityKeysBlob: () => Promise, calendarActive: boolean, getInitialNotificationsEncryptedMessage: ( keyserverID: string, ) => Promise, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, - null, getSignedIdentityKeysBlob, getInitialNotificationsEncryptedMessage, serverRequests, ), ); const baseWebSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = keyserverID => createSelector( sessionStateFuncSelector(keyserverID), (state: AppState) => state.navInfo.tab === 'calendar', ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const webSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = _memoize( baseWebSessionStateFuncSelector, ); export { sessionIdentificationSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, };