diff --git a/lib/shared/device-list-utils.js b/lib/shared/device-list-utils.js new file mode 100644 --- /dev/null +++ b/lib/shared/device-list-utils.js @@ -0,0 +1,121 @@ +// @flow + +import type { + IdentityServiceClient, + RawDeviceList, + SignedDeviceList, +} from '../types/identity-service-types.js'; +import { getConfig } from '../utils/config.js'; +import { rawDeviceListFromSignedList } from '../utils/device-list-utils.js'; + +export type DeviceListVerificationResult = + | { +valid: true, +deviceList: RawDeviceList } + | DeviceListVerificationFailure; + +type DeviceListVerificationFailure = + | { +valid: false, +reason: 'empty_device_list_history' } + | { +valid: false, +reason: 'empty_device_list_update', +timestamp: number } + | { +valid: false, +reason: 'invalid_timestamp_order', +timestamp: number } + | { + +valid: false, + +reason: 'invalid_cur_primary_signature', + +timestamp: number, + } + | { + +valid: false, + +reason: 'invalid_last_primary_signature', + +timestamp: number, + }; + +// Verifies all device list updates for given `userID` since +// last known (and valid) device list. The updates are fetched +// from Identity Service. If `lastKnownDeviceList` is not provided, +// the whole device list history will be verified. +// Returns latest device list from Identity Service. +async function verifyAndGetDeviceList( + identityClient: IdentityServiceClient, + userID: string, + lastKnownDeviceList: ?SignedDeviceList, +): Promise { + let since; + if (lastKnownDeviceList) { + const rawList = rawDeviceListFromSignedList(lastKnownDeviceList); + since = rawList.timestamp; + } + + const history = await identityClient.getDeviceListHistoryForUser( + userID, + since, + ); + if (history.length < 1) { + return { valid: false, reason: 'empty_device_list_history' }; + } + + const [firstUpdate, ...updates] = history; + const deviceListUpdates = lastKnownDeviceList ? history : updates; + let previousDeviceList = lastKnownDeviceList ?? firstUpdate; + + const { olmAPI } = getConfig(); + for (const deviceList of deviceListUpdates) { + const currentPayload = rawDeviceListFromSignedList(deviceList); + const previousPayload = rawDeviceListFromSignedList(previousDeviceList); + + // verify timestamp order + const { timestamp } = currentPayload; + if (previousPayload.timestamp >= timestamp) { + return { + valid: false, + reason: 'invalid_timestamp_order', + timestamp, + }; + } + + const currentPrimaryDeviceID = currentPayload.devices[0]; + const previousPrimaryDeviceID = previousPayload.devices[0]; + if (!currentPrimaryDeviceID || !previousPrimaryDeviceID) { + return { valid: false, reason: 'empty_device_list_update', timestamp }; + } + + // verify signatures + if (deviceList.curPrimarySignature) { + // verify signature using previous primary device signature + const signatureValid = await olmAPI.verifyMessage( + deviceList.rawDeviceList, + deviceList.curPrimarySignature, + currentPrimaryDeviceID, + ); + if (!signatureValid) { + return { + valid: false, + reason: 'invalid_cur_primary_signature', + timestamp, + }; + } + } + if ( + currentPrimaryDeviceID !== previousPrimaryDeviceID && + deviceList.lastPrimarySignature + ) { + // verify signature using previous primary device signature + const signatureValid = await olmAPI.verifyMessage( + deviceList.rawDeviceList, + deviceList.lastPrimarySignature, + previousPrimaryDeviceID, + ); + if (!signatureValid) { + return { + valid: false, + reason: 'invalid_last_primary_signature', + timestamp, + }; + } + } + + previousDeviceList = deviceList; + } + + const deviceList = rawDeviceListFromSignedList(previousDeviceList); + return { valid: true, deviceList }; +} + +export { verifyAndGetDeviceList }; diff --git a/lib/utils/device-list-utils.js b/lib/utils/device-list-utils.js --- a/lib/utils/device-list-utils.js +++ b/lib/utils/device-list-utils.js @@ -2,6 +2,8 @@ import { assertWithValidator } from './validation-utils.js'; import type { + RawDeviceList, + SignedDeviceList, UsersRawDeviceLists, UsersSignedDeviceLists, } from '../types/identity-service-types.js'; @@ -14,13 +16,22 @@ for (const userID in signedDeviceLists) { usersRawDeviceLists = { ...usersRawDeviceLists, - [userID]: assertWithValidator( - JSON.parse(signedDeviceLists[userID].rawDeviceList), - rawDeviceListValidator, - ), + [userID]: rawDeviceListFromSignedList(signedDeviceLists[userID]), }; } return usersRawDeviceLists; } -export { convertSignedDeviceListsToRawDeviceLists }; +function rawDeviceListFromSignedList( + signedDeviceList: SignedDeviceList, +): RawDeviceList { + return assertWithValidator( + JSON.parse(signedDeviceList.rawDeviceList), + rawDeviceListValidator, + ); +} + +export { + convertSignedDeviceListsToRawDeviceLists, + rawDeviceListFromSignedList, +};