diff --git a/lib/components/qr-auth-handler.react.js b/lib/components/qr-auth-handler.react.js new file mode 100644 --- /dev/null +++ b/lib/components/qr-auth-handler.react.js @@ -0,0 +1,147 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { IdentityClientContext } from '../shared/identity-client-context.js'; +import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; +import { + tunnelbrokerMessageTypes, + type TunnelbrokerMessage, +} from '../types/tunnelbroker/messages.js'; +import { + peerToPeerMessageTypes, + peerToPeerMessageValidator, +} from '../types/tunnelbroker/peer-to-peer-message-types.js'; +import { qrCodeAuthMessageTypes } from '../types/tunnelbroker/qr-code-auth-message-types.js'; +import { + createQRAuthTunnelbrokerMessage, + parseQRAuthTunnelbrokerMessage, +} from '../utils/qr-code-auth.js'; + +type QRAuthHandlerProps = { + secondaryDeviceID: ?string, + aesKey: ?string, + performSecondaryDeviceRegistration: (userID: string) => Promise, +}; + +function QRAuthHandler(props: QRAuthHandlerProps): React.Node { + const { secondaryDeviceID, aesKey, performSecondaryDeviceRegistration } = + props; + 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 ( + !secondaryDeviceID || + !aesKey || + !tunnelbrokerConnected || + !isAuthorized || + !primaryDeviceID + ) { + return; + } + + void (async () => { + const message = createQRAuthTunnelbrokerMessage(aesKey, { + type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, + }); + await sendMessage({ + deviceID: primaryDeviceID, + payload: JSON.stringify(message), + }); + })(); + }, [ + tunnelbrokerConnected, + isAuthorized, + sendMessage, + primaryDeviceID, + aesKey, + secondaryDeviceID, + ]); + + const tunnelbrokerMessageListener = React.useCallback( + async (message: TunnelbrokerMessage) => { + invariant(identityClient, 'identity context not set'); + if ( + !aesKey || + message.type !== tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE + ) { + return; + } + + let innerMessage; + try { + innerMessage = JSON.parse(message.payload); + } catch { + return; + } + if ( + !peerToPeerMessageValidator.is(innerMessage) || + innerMessage.type !== peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE + ) { + return; + } + const qrCodeAuthMessage = parseQRAuthTunnelbrokerMessage( + aesKey, + innerMessage, + ); + + if ( + qrCodeAuthMessage?.type === + qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE + ) { + return; + } + + if ( + !qrCodeAuthMessage || + qrCodeAuthMessage.type !== + qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS + ) { + return; + } + const { primaryDeviceID: receivedPrimaryDeviceID, userID } = + qrCodeAuthMessage; + setPrimaryDeviceID(receivedPrimaryDeviceID); + + await performSecondaryDeviceRegistration(userID); + setUnauthorizedDeviceID(null); + }, + [ + setUnauthorizedDeviceID, + identityClient, + aesKey, + performSecondaryDeviceRegistration, + ], + ); + + React.useEffect(() => { + if (!secondaryDeviceID) { + return () => {}; + } + addListener(tunnelbrokerMessageListener); + + return () => { + removeListener(tunnelbrokerMessageListener); + }; + }, [ + secondaryDeviceID, + addListener, + removeListener, + tunnelbrokerMessageListener, + ]); + return null; +} + +export { QRAuthHandler }; 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 @@ -5,6 +5,7 @@ import { View, Text } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; +import { QRAuthHandler } from 'lib/components/qr-auth-handler.react.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; import { uintArrayToHexString } from 'lib/media/data-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; @@ -13,19 +14,6 @@ NonceChallenge, SignedMessage, } from 'lib/types/identity-service-types.js'; -import { - tunnelbrokerMessageTypes, - type TunnelbrokerMessage, -} from 'lib/types/tunnelbroker/messages.js'; -import { - peerToPeerMessageTypes, - peerToPeerMessageValidator, -} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; -import { qrCodeAuthMessageTypes } from 'lib/types/tunnelbroker/qr-code-auth-message-types.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'; @@ -45,89 +33,14 @@ const [qrCodeValue, setQrCodeValue] = React.useState(); const [qrData, setQRData] = React.useState(); - const [primaryDeviceID, setPrimaryDeviceID] = React.useState(); - const { - setUnauthorizedDeviceID, - addListener, - removeListener, - connected: tunnelbrokerConnected, - isAuthorized, - sendMessage, - } = useTunnelbroker(); + const { setUnauthorizedDeviceID } = 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) => { + const performRegistration = React.useCallback( + async (userID: string) => { invariant(identityClient, 'identity context not set'); - if ( - !qrData || - message.type !== tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE - ) { - return; - } - - let innerMessage; - try { - innerMessage = JSON.parse(message.payload); - } catch { - return; - } - if ( - !peerToPeerMessageValidator.is(innerMessage) || - innerMessage.type !== peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE - ) { - return; - } - const qrCodeAuthMessage = parseQRAuthTunnelbrokerMessage( - qrData.aesKey, - innerMessage, - ); - - if ( - qrCodeAuthMessage?.type === - qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE - ) { - console.log('Received backup data key:', qrCodeAuthMessage); - return; - } - - if ( - !qrCodeAuthMessage || - qrCodeAuthMessage.type !== - qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS - ) { - return; - } - const { primaryDeviceID: receivedPrimaryDeviceID, userID } = - qrCodeAuthMessage; - setPrimaryDeviceID(receivedPrimaryDeviceID); - try { const nonce = await identityClient.generateNonce(); const nonceChallenge: NonceChallenge = { nonce }; @@ -150,28 +63,9 @@ ]); } }, - [setUnauthorizedDeviceID, identityClient, qrData], + [setUnauthorizedDeviceID, identityClient], ); - React.useEffect(() => { - if (!qrData) { - return undefined; - } - addListener(tunnelbrokerMessageListener); - setUnauthorizedDeviceID(qrData.deviceID); - - return () => { - removeListener(tunnelbrokerMessageListener); - setUnauthorizedDeviceID(null); - }; - }, [ - setUnauthorizedDeviceID, - qrData, - addListener, - removeListener, - tunnelbrokerMessageListener, - ]); - const generateQRCode = React.useCallback(async () => { try { const rawAESKey: Uint8Array = await AES.generateKey(); @@ -180,12 +74,13 @@ const ed25519Key: string = await getContentSigningKey(); const url = qrCodeLinkURL(aesKeyAsHexString, ed25519Key); + setUnauthorizedDeviceID(ed25519Key); setQrCodeValue(url); setQRData({ deviceID: ed25519Key, aesKey: aesKeyAsHexString }); } catch (err) { console.error('Failed to generate QR Code:', err); } - }, []); + }, [setUnauthorizedDeviceID]); React.useEffect(() => { void generateQRCode(); @@ -193,29 +88,36 @@ const styles = useStyles(unboundStyles); return ( - - Log in to Comm - - Open the Comm app on your phone and scan the QR code below - - - - How to find the scanner: - - Go to - Profile - - - Select - Linked devices - - - Click - Add - on the top right + <> + + + Log in to Comm + + Open the Comm app on your phone and scan the QR code below + + + How to find the scanner: + + Go to + Profile + + + Select + Linked devices + + + Click + Add + on the top right + + - + ); }