diff --git a/keyserver/src/shared/state-sync/current-user-state-sync-spec.js b/keyserver/src/shared/state-sync/current-user-state-sync-spec.js --- a/keyserver/src/shared/state-sync/current-user-state-sync-spec.js +++ b/keyserver/src/shared/state-sync/current-user-state-sync-spec.js @@ -2,10 +2,13 @@ import { currentUserStateSyncSpec as libSpec } from 'lib/shared/state-sync/current-user-state-sync-spec.js'; import type { CurrentUserInfo } from 'lib/types/user-types.js'; +import { currentUserInfoValidator } from 'lib/types/user-types.js'; +import { hash } from 'lib/utils/objects.js'; import type { ServerStateSyncSpec } from './state-sync-spec.js'; import { fetchCurrentUserInfo } from '../../fetchers/user-fetchers.js'; import type { Viewer } from '../../session/viewer.js'; +import { validateOutput } from '../../utils/validation-utils.js'; export const currentUserStateSyncSpec: ServerStateSyncSpec< CurrentUserInfo, @@ -13,11 +16,23 @@ CurrentUserInfo, void, > = Object.freeze({ - fetch(viewer: Viewer) { - return fetchCurrentUserInfo(viewer); - }, + fetch, fetchFullSocketSyncPayload(viewer: Viewer) { return fetchCurrentUserInfo(viewer); }, + async fetchServerInfosHash(viewer: Viewer) { + const info = await fetch(viewer); + return getHash(info); + }, + getServerInfosHash: getHash, + getServerInfoHash: getHash, ...libSpec, }); + +function fetch(viewer: Viewer) { + return fetchCurrentUserInfo(viewer); +} + +function getHash(currentUserInfo: CurrentUserInfo) { + return hash(validateOutput(null, currentUserInfoValidator, currentUserInfo)); +} diff --git a/keyserver/src/shared/state-sync/entries-state-sync-spec.js b/keyserver/src/shared/state-sync/entries-state-sync-spec.js --- a/keyserver/src/shared/state-sync/entries-state-sync-spec.js +++ b/keyserver/src/shared/state-sync/entries-state-sync-spec.js @@ -2,12 +2,14 @@ import { serverEntryInfosObject } from 'lib/shared/entry-utils.js'; import { entriesStateSyncSpec as libSpec } from 'lib/shared/state-sync/entries-state-sync-spec.js'; -import type { - CalendarQuery, - RawEntryInfo, - RawEntryInfos, +import { + type CalendarQuery, + type RawEntryInfo, + type RawEntryInfos, + rawEntryInfoValidator, } from 'lib/types/entry-types.js'; import type { ClientEntryInconsistencyReportCreationRequest } from 'lib/types/report-types.js'; +import { hash, combineUnorderedHashes, values } from 'lib/utils/objects.js'; import type { ServerStateSyncSpec } from './state-sync-spec.js'; import { @@ -15,6 +17,7 @@ fetchEntryInfosByID, } from '../../fetchers/entry-fetchers.js'; import type { Viewer } from '../../session/viewer.js'; +import { validateOutput } from '../../utils/validation-utils.js'; export const entriesStateSyncSpec: ServerStateSyncSpec< RawEntryInfos, @@ -22,14 +25,7 @@ RawEntryInfo, $ReadOnlyArray, > = Object.freeze({ - async fetch(viewer: Viewer, ids?: $ReadOnlySet) { - if (ids) { - return fetchEntryInfosByID(viewer, ids); - } - const query = [viewer.calendarQuery]; - const entriesResult = await fetchEntryInfos(viewer, query); - return serverEntryInfosObject(entriesResult.rawEntryInfos); - }, + fetch, async fetchFullSocketSyncPayload( viewer: Viewer, query: $ReadOnlyArray, @@ -37,5 +33,28 @@ const result = await fetchEntryInfos(viewer, query); return result.rawEntryInfos; }, + async fetchServerInfosHash(viewer: Viewer, ids?: $ReadOnlySet) { + const info = await fetch(viewer, ids); + return getServerInfosHash(info); + }, + getServerInfosHash, + getServerInfoHash, ...libSpec, }); + +async function fetch(viewer: Viewer, ids?: $ReadOnlySet) { + if (ids) { + return fetchEntryInfosByID(viewer, ids); + } + const query = [viewer.calendarQuery]; + const entriesResult = await fetchEntryInfos(viewer, query); + return serverEntryInfosObject(entriesResult.rawEntryInfos); +} + +function getServerInfosHash(infos: RawEntryInfos) { + return combineUnorderedHashes(values(infos).map(getServerInfoHash)); +} + +function getServerInfoHash(info: RawEntryInfo) { + return hash(validateOutput(null, rawEntryInfoValidator, info)); +} diff --git a/keyserver/src/shared/state-sync/state-sync-spec.js b/keyserver/src/shared/state-sync/state-sync-spec.js --- a/keyserver/src/shared/state-sync/state-sync-spec.js +++ b/keyserver/src/shared/state-sync/state-sync-spec.js @@ -16,5 +16,11 @@ viewer: Viewer, calendarQuery: $ReadOnlyArray, ) => Promise, + +fetchServerInfosHash: ( + viewer: Viewer, + ids?: $ReadOnlySet, + ) => Promise, + +getServerInfosHash: (infos: Infos) => number, + +getServerInfoHash: (info: Info) => number, ...StateSyncSpec, }; diff --git a/keyserver/src/shared/state-sync/threads-state-sync-spec.js b/keyserver/src/shared/state-sync/threads-state-sync-spec.js --- a/keyserver/src/shared/state-sync/threads-state-sync-spec.js +++ b/keyserver/src/shared/state-sync/threads-state-sync-spec.js @@ -5,11 +5,14 @@ import { type RawThreadInfos, type RawThreadInfo, + rawThreadInfoValidator, } from 'lib/types/thread-types.js'; +import { hash, combineUnorderedHashes, values } from 'lib/utils/objects.js'; import type { ServerStateSyncSpec } from './state-sync-spec.js'; import { fetchThreadInfos } from '../../fetchers/thread-fetchers.js'; import type { Viewer } from '../../session/viewer.js'; +import { validateOutput } from '../../utils/validation-utils.js'; export const threadsStateSyncSpec: ServerStateSyncSpec< RawThreadInfos, @@ -17,14 +20,30 @@ RawThreadInfo, $ReadOnlyArray, > = Object.freeze({ - async fetch(viewer: Viewer, ids?: $ReadOnlySet) { - const filter = ids ? { threadIDs: ids } : undefined; - const result = await fetchThreadInfos(viewer, filter); - return result.threadInfos; - }, + fetch, async fetchFullSocketSyncPayload(viewer: Viewer) { const result = await fetchThreadInfos(viewer); return result.threadInfos; }, + async fetchServerInfosHash(viewer: Viewer, ids?: $ReadOnlySet) { + const infos = await fetch(viewer, ids); + return getServerInfosHash(infos); + }, + getServerInfosHash, + getServerInfoHash, ...libSpec, }); + +async function fetch(viewer: Viewer, ids?: $ReadOnlySet) { + const filter = ids ? { threadIDs: ids } : undefined; + const result = await fetchThreadInfos(viewer, filter); + return result.threadInfos; +} + +function getServerInfosHash(infos: RawThreadInfos) { + return combineUnorderedHashes(values(infos).map(getServerInfoHash)); +} + +function getServerInfoHash(info: RawThreadInfo) { + return hash(validateOutput(null, rawThreadInfoValidator, info)); +} diff --git a/keyserver/src/shared/state-sync/users-state-sync-spec.js b/keyserver/src/shared/state-sync/users-state-sync-spec.js --- a/keyserver/src/shared/state-sync/users-state-sync-spec.js +++ b/keyserver/src/shared/state-sync/users-state-sync-spec.js @@ -3,11 +3,13 @@ import { usersStateSyncSpec as libSpec } from 'lib/shared/state-sync/users-state-sync-spec.js'; import type { UserInconsistencyReportCreationRequest } from 'lib/types/report-types.js'; import type { UserInfos, UserInfo } from 'lib/types/user-types.js'; -import { values } from 'lib/utils/objects.js'; +import { userInfoValidator } from 'lib/types/user-types.js'; +import { values, hash, combineUnorderedHashes } from 'lib/utils/objects.js'; import type { ServerStateSyncSpec } from './state-sync-spec.js'; import { fetchKnownUserInfos } from '../../fetchers/user-fetchers.js'; import type { Viewer } from '../../session/viewer.js'; +import { validateOutput } from '../../utils/validation-utils.js'; export const usersStateSyncSpec: ServerStateSyncSpec< UserInfos, @@ -15,16 +17,32 @@ UserInfo, $ReadOnlyArray, > = Object.freeze({ - fetch(viewer: Viewer, ids?: $ReadOnlySet) { - if (ids) { - return fetchKnownUserInfos(viewer, [...ids]); - } - - return fetchKnownUserInfos(viewer); - }, + fetch, async fetchFullSocketSyncPayload(viewer: Viewer) { const result = await fetchKnownUserInfos(viewer); return values(result); }, + async fetchServerInfosHash(viewer: Viewer, ids?: $ReadOnlySet) { + const infos = await fetch(viewer, ids); + return getServerInfosHash(infos); + }, + getServerInfosHash, + getServerInfoHash, ...libSpec, }); + +function fetch(viewer: Viewer, ids?: $ReadOnlySet) { + if (ids) { + return fetchKnownUserInfos(viewer, [...ids]); + } + + return fetchKnownUserInfos(viewer); +} + +function getServerInfosHash(infos: UserInfos) { + return combineUnorderedHashes(values(infos).map(getServerInfoHash)); +} + +function getServerInfoHash(info: UserInfo) { + return hash(validateOutput(null, userInfoValidator, info)); +} diff --git a/keyserver/src/socket/session-utils.js b/keyserver/src/socket/session-utils.js --- a/keyserver/src/socket/session-utils.js +++ b/keyserver/src/socket/session-utils.js @@ -4,6 +4,10 @@ import t from 'tcomb'; import type { TUnion } from 'tcomb'; +import { + NEXT_CODE_VERSION, + hasMinCodeVersion, +} from 'lib/shared/version-utils.js'; import type { UpdateActivityResult } from 'lib/types/activity-types.js'; import type { IdentityKeysBlob } from 'lib/types/crypto-types.js'; import { isDeviceType } from 'lib/types/device-types.js'; @@ -386,8 +390,17 @@ values(serverStateSyncSpecs).map(spec => [ spec.hashKey, (async () => { - const data = await spec.fetch(viewer); - return hash(data); + if ( + !hasMinCodeVersion(viewer.platformDetails, { + native: NEXT_CODE_VERSION, + web: NEXT_CODE_VERSION, + }) + ) { + const data = await spec.fetch(viewer); + return hash(data); + } + const infosHash = await spec.fetchServerInfosHash(viewer); + return infosHash; })(), ]), ); @@ -452,7 +465,18 @@ // out which infos don't match first const infos = fetchedData[key]; for (const infoID in infos) { - hashesToCheck[`${innerHashKey}|${infoID}`] = hash(infos[infoID]); + let hashValue; + if ( + hasMinCodeVersion(viewer.platformDetails, { + native: NEXT_CODE_VERSION, + web: NEXT_CODE_VERSION, + }) + ) { + hashValue = spec.getServerInfoHash(infos[infoID]); + } else { + hashValue = hash(infos[infoID]); + } + hashesToCheck[`${innerHashKey}|${infoID}`] = hashValue; } failUnmentioned[key] = true; } else if (isTopLevelKey) { diff --git a/lib/utils/objects.js b/lib/utils/objects.js --- a/lib/utils/objects.js +++ b/lib/utils/objects.js @@ -73,6 +73,14 @@ return stringHash(stableStringify(obj)); } +// This function doesn't look at the order of the hashes inside of the array +// e.g `combineUnorderedHashes([1,2,3]) === combineUnorderedHashes([3,1,2])` +// so it should only be used if the hashes include their ordering in them +// somehow (e.g. `RawThreadInfo` contains `id`) +function combineUnorderedHashes(hashes: $ReadOnlyArray): number { + return hashes.reduce((a, v) => a ^ v, 0); +} + // returns an object with properties from obj1 not included in obj2 function deepDiff( obj1: NestedObjectMap, @@ -125,6 +133,7 @@ findMaximumDepth, values, hash, + combineUnorderedHashes, assertObjectsAreEqual, deepDiff, entries,