diff --git a/keyserver/src/responders/redux-state-responders.js b/keyserver/src/responders/redux-state-responders.js --- a/keyserver/src/responders/redux-state-responders.js +++ b/keyserver/src/responders/redux-state-responders.js @@ -27,17 +27,13 @@ } from 'lib/types/message-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; -import { - threadStoreValidator, - rawThreadInfoValidator, -} from 'lib/types/thread-types.js'; +import { threadStoreValidator } from 'lib/types/thread-types.js'; import { currentUserInfoValidator, userInfosValidator, } from 'lib/types/user-types.js'; import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; import { ServerError } from 'lib/utils/errors.js'; -import { hash } from 'lib/utils/objects.js'; import { promiseAll } from 'lib/utils/promises.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; import { tShape, ashoatKeyserverID } from 'lib/utils/validation-utils.js'; @@ -61,7 +57,7 @@ import { getWebPushConfig } from '../push/providers.js'; import { setNewSession } from '../session/cookies.js'; import { Viewer } from '../session/viewer.js'; -import { validateOutput } from '../utils/validation-utils.js'; +import { serverStateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; const initialKeyserverInfoValidator = tShape({ sessionID: t.maybe(t.String), @@ -329,13 +325,9 @@ const { threadInfos } = await threadStorePromise; const threadHashes = {}; for (const id in threadInfos) { - const threadInfo = threadInfos[id]; - const convertedThreadInfo = validateOutput( - viewer.platformDetails, - rawThreadInfoValidator, - threadInfo, + threadHashes[id] = serverStateSyncSpecs.threads.getServerInfoHash( + threadInfos[id], ); - threadHashes[id] = hash(convertedThreadInfo); } return { threadHashes }; })(); 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, @@ -18,5 +21,11 @@ fetchFullSocketSyncPayload(viewer: Viewer) { return fetchCurrentUserInfo(viewer); }, + getServerInfosHash: getHash, + getServerInfoHash: getHash, ...libSpec, }); + +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,11 +2,13 @@ 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 { hash, combineUnorderedHashes, values } from 'lib/utils/objects.js'; import type { ServerStateSyncSpec } from './state-sync-spec.js'; import { @@ -14,6 +16,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, @@ -35,5 +38,13 @@ const result = await fetchEntryInfos(viewer, query); return result.rawEntryInfos; }, + getServerInfosHash(infos: RawEntryInfos) { + return combineUnorderedHashes(values(infos).map(getServerInfoHash)); + }, + getServerInfoHash, ...libSpec, }); + +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 @@ -11,5 +11,7 @@ viewer: Viewer, calendarQuery: $ReadOnlyArray, ) => 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 @@ -4,11 +4,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, @@ -24,5 +27,13 @@ const result = await fetchThreadInfos(viewer); return result.threadInfos; }, + getServerInfosHash(infos: RawThreadInfos) { + return combineUnorderedHashes(values(infos).map(getServerInfoHash)); + }, + getServerInfoHash, ...libSpec, }); + +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 @@ -2,11 +2,13 @@ import { usersStateSyncSpec as libSpec } from 'lib/shared/state-sync/users-state-sync-spec.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, @@ -24,5 +26,13 @@ const result = await fetchKnownUserInfos(viewer); return values(result); }, + getServerInfosHash(infos: UserInfos) { + return combineUnorderedHashes(values(infos).map(getServerInfoHash)); + }, + getServerInfoHash, ...libSpec, }); + +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 { + FUTURE_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'; @@ -385,9 +389,22 @@ const promises = Object.fromEntries( values(serverStateSyncSpecs).map(spec => [ spec.hashKey, + // The FlowFixMes in the following function are needed because flow + // doesn't understand that `data` is of the generic Info type of the + // spec we are currently iterating on (async () => { + // $FlowFixMe const data = await spec.fetch(viewer); - return hash(data); + if ( + !hasMinCodeVersion(viewer.platformDetails, { + native: FUTURE_CODE_VERSION, + web: FUTURE_CODE_VERSION, + }) + ) { + return hash(data); + } + // $FlowFixMe + return spec.getServerInfosHash(data); })(), ]), ); @@ -452,7 +469,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: FUTURE_CODE_VERSION, + web: FUTURE_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,