diff --git a/lib/shared/device-list-utils.js b/lib/shared/device-list-utils.js --- a/lib/shared/device-list-utils.js +++ b/lib/shared/device-list-utils.js @@ -1,11 +1,14 @@ // @flow +import invariant from 'invariant'; + import type { IdentityServiceClient, RawDeviceList, SignedDeviceList, } from '../types/identity-service-types.js'; import { getConfig } from '../utils/config.js'; +import { getContentSigningKey } from '../utils/crypto-utils.js'; import { composeRawDeviceList, rawDeviceListFromSignedList, @@ -134,4 +137,67 @@ }; } -export { verifyAndGetDeviceList, createAndSignInitialDeviceList }; +async function signDeviceListUpdate( + deviceListPayload: RawDeviceList, +): Promise { + const deviceID = await getContentSigningKey(); + const rawDeviceList = JSON.stringify(deviceListPayload); + + // don't sign device list if current device is not a primary one + if (deviceListPayload.devices[0] !== deviceID) { + return { + rawDeviceList, + }; + } + + const { olmAPI } = getConfig(); + const curPrimarySignature = await olmAPI.signMessage(rawDeviceList); + return { + rawDeviceList, + curPrimarySignature, + }; +} + +async function fetchLatestDeviceList( + identityClient: IdentityServiceClient, + userID: string, +): Promise { + const deviceLists = await identityClient.getDeviceListHistoryForUser(userID); + if (deviceLists.length < 1) { + throw new Error('received empty device list history'); + } + + const lastSignedDeviceList = deviceLists[deviceLists.length - 1]; + return rawDeviceListFromSignedList(lastSignedDeviceList); +} + +async function addDeviceToDeviceList( + identityClient: IdentityServiceClient, + userID: string, + newDeviceID: string, +) { + const { updateDeviceList } = identityClient; + invariant( + updateDeviceList, + 'updateDeviceList() should be defined on native. ' + + 'Are you calling it on a non-primary device?', + ); + + const { devices } = await fetchLatestDeviceList(identityClient, userID); + if (devices.includes(newDeviceID)) { + // the device was already on the device list + return; + } + + const newDeviceList = composeRawDeviceList([...devices, newDeviceID]); + const signedDeviceList = await signDeviceListUpdate(newDeviceList); + await updateDeviceList(signedDeviceList); +} + +export { + verifyAndGetDeviceList, + createAndSignInitialDeviceList, + fetchLatestDeviceList, + addDeviceToDeviceList, + signDeviceListUpdate, +}; diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js --- a/native/profile/secondary-device-qr-code-scanner.react.js +++ b/native/profile/secondary-device-qr-code-scanner.react.js @@ -7,6 +7,7 @@ import { View } from 'react-native'; import { parseDataFromDeepLink } from 'lib/facts/links.js'; +import { addDeviceToDeviceList } from 'lib/shared/device-list-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { @@ -23,10 +24,7 @@ type PeerToPeerMessage, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { qrCodeAuthMessageTypes } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; -import { - composeRawDeviceList, - rawDeviceListFromSignedList, -} from 'lib/utils/device-list-utils.js'; +import { rawDeviceListFromSignedList } from 'lib/utils/device-list-utils.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; @@ -36,7 +34,6 @@ import { composeTunnelbrokerQRAuthMessage, parseTunnelbrokerQRAuthMessage, - signDeviceListUpdate, } from '../qr-code/qr-code-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; @@ -90,40 +87,6 @@ await Promise.all(promises); }, [identityContext, tunnelbrokerContext]); - const addDeviceToList = React.useCallback( - async (newDeviceID: string) => { - const { getDeviceListHistoryForUser, updateDeviceList } = - identityContext.identityClient; - invariant( - updateDeviceList, - 'updateDeviceList() should be defined for primary device', - ); - - const authMetadata = await identityContext.getAuthMetadata(); - if (!authMetadata?.userID) { - throw new Error('missing auth metadata'); - } - - const deviceLists = await getDeviceListHistoryForUser( - authMetadata.userID, - ); - invariant(deviceLists.length > 0, 'received empty device list history'); - - const lastSignedDeviceList = deviceLists[deviceLists.length - 1]; - const deviceList = rawDeviceListFromSignedList(lastSignedDeviceList); - - const { devices } = deviceList; - if (devices.includes(newDeviceID)) { - return; - } - - const newDeviceList = composeRawDeviceList([...devices, newDeviceID]); - const signedDeviceList = await signDeviceListUpdate(newDeviceList); - await updateDeviceList(signedDeviceList); - }, - [identityContext], - ); - const tunnelbrokerMessageListener = React.useCallback( async (message: TunnelbrokerMessage) => { const encryptionKey = aes256Key.current; @@ -239,7 +202,11 @@ if (!primaryDeviceID || !userID) { throw new Error('missing auth metadata'); } - await addDeviceToList(ed25519); + await addDeviceToDeviceList( + identityContext.identityClient, + userID, + ed25519, + ); const message = await composeTunnelbrokerQRAuthMessage(aes256, { type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS, userID, @@ -259,7 +226,7 @@ navigation.goBack(); } }, - [tunnelbrokerContext, addDeviceToList, identityContext, navigation], + [tunnelbrokerContext, identityContext, navigation], ); const onCancelScan = React.useCallback(() => setScanned(false), []); diff --git a/native/qr-code/qr-code-utils.js b/native/qr-code/qr-code-utils.js --- a/native/qr-code/qr-code-utils.js +++ b/native/qr-code/qr-code-utils.js @@ -1,10 +1,6 @@ // @flow import { hexToUintArray } from 'lib/media/data-utils.js'; -import type { - RawDeviceList, - SignedDeviceList, -} from 'lib/types/identity-service-types.js'; import { peerToPeerMessageTypes, type QRCodeAuthMessage, @@ -13,8 +9,6 @@ qrCodeAuthMessagePayloadValidator, type QRCodeAuthMessagePayload, } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; -import { getConfig } from 'lib/utils/config.js'; -import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { convertBytesToObj, @@ -58,29 +52,4 @@ return Promise.resolve(payload); } -async function signDeviceListUpdate( - deviceListPayload: RawDeviceList, -): Promise { - const deviceID = await getContentSigningKey(); - const rawDeviceList = JSON.stringify(deviceListPayload); - - // don't sign device list if current device is not a primary one - if (deviceListPayload.devices[0] !== deviceID) { - return { - rawDeviceList, - }; - } - - const { olmAPI } = getConfig(); - const curPrimarySignature = await olmAPI.signMessage(rawDeviceList); - return { - rawDeviceList, - curPrimarySignature, - }; -} - -export { - composeTunnelbrokerQRAuthMessage, - parseTunnelbrokerQRAuthMessage, - signDeviceListUpdate, -}; +export { composeTunnelbrokerQRAuthMessage, parseTunnelbrokerQRAuthMessage };