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 @@ -10,8 +10,20 @@ import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { RawDeviceList } from 'lib/types/identity-service-types.js'; +import { + tunnelbrokerMessageTypes, + type TunnelbrokerMessage, +} from 'lib/types/tunnelbroker/messages.js'; +import { + peerToPeerMessageTypes, + peerToPeerMessageValidator, + 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 { createQRAuthTunnelbrokerMessage } from 'lib/utils/qr-code-auth.js'; +import { + createQRAuthTunnelbrokerMessage, + parseQRAuthTunnelbrokerMessage, +} from 'lib/utils/qr-code-auth.js'; import type { ProfileNavigationProp } from './profile.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; @@ -36,6 +48,9 @@ const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'identity context not set'); + const aes256Key = React.useRef(null); + const secondaryDeviceID = React.useRef(null); + const addDeviceToList = React.useCallback( async (newDeviceID: string) => { const { getDeviceListHistoryForUser, updateDeviceList } = @@ -76,6 +91,55 @@ [identityContext], ); + const tunnelbrokerMessageListener = React.useCallback( + async (message: TunnelbrokerMessage) => { + const encryptionKey = aes256Key.current; + const targetDeviceID = secondaryDeviceID.current; + if (!encryptionKey || !targetDeviceID) { + return; + } + if (message.type !== tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE) { + return; + } + + let innerMessage: PeerToPeerMessage; + try { + innerMessage = JSON.parse(message.payload); + } catch { + return; + } + if ( + !peerToPeerMessageValidator.is(innerMessage) || + innerMessage.type !== peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE + ) { + return; + } + + const payload = parseQRAuthTunnelbrokerMessage( + encryptionKey, + innerMessage, + ); + if ( + payload?.type !== + qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS + ) { + return; + } + Alert.alert('Device added', 'Device registered successfully', [ + { text: 'OK' }, + ]); + }, + [], + ); + + React.useEffect(() => { + tunnelbrokerContext.addListener(tunnelbrokerMessageListener); + + return () => { + tunnelbrokerContext.removeListener(tunnelbrokerMessageListener); + }; + }, [tunnelbrokerMessageListener, tunnelbrokerContext]); + React.useEffect(() => { void (async () => { const { status } = await BarCodeScanner.requestPermissionsAsync(); @@ -110,6 +174,8 @@ const keys = JSON.parse(decodeURIComponent(keysMatch)); const { aes256, ed25519 } = keys; + aes256Key.current = aes256; + secondaryDeviceID.current = ed25519; try { const { deviceID: primaryDeviceID, userID } = diff --git a/native/qr-code/qr-code-screen.react.js b/native/qr-code/qr-code-screen.react.js --- a/native/qr-code/qr-code-screen.react.js +++ b/native/qr-code/qr-code-screen.react.js @@ -22,7 +22,10 @@ peerToPeerMessageValidator, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { qrCodeAuthMessageTypes } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; -import { parseQRAuthTunnelbrokerMessage } from 'lib/utils/qr-code-auth.js'; +import { + createQRAuthTunnelbrokerMessage, + parseQRAuthTunnelbrokerMessage, +} from 'lib/utils/qr-code-auth.js'; import type { QRCodeSignInNavigationProp } from './qr-code-sign-in-navigator.react.js'; import { commCoreModule } from '../native-modules.js'; @@ -42,11 +45,43 @@ const [qrCodeValue, setQrCodeValue] = React.useState(); const [qrData, setQRData] = React.useState(); - const { setUnauthorizedDeviceID, addListener, removeListener } = - useTunnelbroker(); + const [primaryDeviceID, setPrimaryDeviceID] = React.useState(); + const { + setUnauthorizedDeviceID, + addListener, + removeListener, + connected: tunnelbrokerConnected, + isAuthorized, + sendMessage, + } = useTunnelbroker(); const identityContext = React.useContext(IdentityClientContext); const identityClient = identityContext?.identityClient; + React.useEffect(() => { + if ( + !tunnelbrokerConnected || + !isAuthorized || + !primaryDeviceID || + !qrData + ) { + return; + } + + const message = createQRAuthTunnelbrokerMessage(qrData.aesKey, { + type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, + }); + void sendMessage({ + deviceID: primaryDeviceID, + payload: JSON.stringify(message), + }); + }, [ + tunnelbrokerConnected, + isAuthorized, + sendMessage, + primaryDeviceID, + qrData, + ]); + const tunnelbrokerMessageListener = React.useCallback( async (message: TunnelbrokerMessage) => { invariant(identityClient, 'identity context not set'); @@ -80,7 +115,9 @@ ) { return; } - const { userID } = qrCodeAuthMessage; + const { primaryDeviceID: receivedPrimaryDeviceID, userID } = + qrCodeAuthMessage; + setPrimaryDeviceID(receivedPrimaryDeviceID); try { const nonce = await identityClient.generateNonce();