diff --git a/lib/components/qr-auth-provider.react.js b/lib/components/qr-auth-provider.react.js index ceb7fcecd..640ed8639 100644 --- a/lib/components/qr-auth-provider.react.js +++ b/lib/components/qr-auth-provider.react.js @@ -1,124 +1,237 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useSecondaryDeviceLogIn } from '../hooks/login-hooks.js'; -import { useQRAuth } from '../hooks/qr-auth.js'; import { uintArrayToHexString } from '../media/data-utils.js'; +import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { BackupKeys } from '../types/backup-types.js'; -import type { QRCodeAuthMessage } from '../types/tunnelbroker/peer-to-peer-message-types.js'; -import type { QRCodeAuthMessagePayload } from '../types/tunnelbroker/qr-code-auth-message-types.js'; +import { + tunnelbrokerMessageTypes, + type TunnelbrokerMessage, +} from '../types/tunnelbroker/messages.js'; +import { + type QRCodeAuthMessage, + peerToPeerMessageTypes, + peerToPeerMessageValidator, +} from '../types/tunnelbroker/peer-to-peer-message-types.js'; +import { + qrCodeAuthMessageTypes, + type QRCodeAuthMessagePayload, +} from '../types/tunnelbroker/qr-code-auth-message-types.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; type Props = { +children: React.Node, +onLoginError: (error: mixed) => void, +generateAESKey: () => Promise, +composeTunnelbrokerMessage: ( encryptionKey: string, payload: QRCodeAuthMessagePayload, ) => Promise, +processTunnelbrokerMessage: ( encryptionKey: string, message: QRCodeAuthMessage, ) => Promise, +performBackupRestore?: (backupKeys: BackupKeys) => Promise, }; type QRData = ?{ +deviceID: string, +aesKey: string }; type QRAuthContextType = { +qrData: QRData, +generateQRCode: () => Promise, }; const QRAuthContext: React.Context = React.createContext({ qrData: null, generateQRCode: async () => {}, }); function QRAuthProvider(props: Props): React.Node { const { children, onLoginError, generateAESKey, composeTunnelbrokerMessage, processTunnelbrokerMessage, performBackupRestore, } = props; + const [primaryDeviceID, setPrimaryDeviceID] = React.useState(); const [qrData, setQRData] = React.useState(); - const { setUnauthorizedDeviceID } = useTunnelbroker(); + const { + setUnauthorizedDeviceID, + addListener, + removeListener, + socketState, + sendMessage, + } = useTunnelbroker(); + + const identityContext = React.useContext(IdentityClientContext); + const identityClient = identityContext?.identityClient; const generateQRCode = React.useCallback(async () => { try { const [ed25519, rawAESKey] = await Promise.all([ getContentSigningKey(), generateAESKey(), ]); const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); setUnauthorizedDeviceID(ed25519); setQRData({ deviceID: ed25519, aesKey: aesKeyAsHexString }); } catch (err) { console.error('Failed to generate QR Code:', err); } }, [generateAESKey, setUnauthorizedDeviceID]); const logInSecondaryDevice = useSecondaryDeviceLogIn(); const performLogIn = React.useCallback( async (userID: string) => { try { await logInSecondaryDevice(userID); } catch (err) { onLoginError(err); void generateQRCode(); } }, [logInSecondaryDevice, onLoginError, generateQRCode], ); - const qrAuthInput = React.useMemo( - () => ({ - secondaryDeviceID: qrData?.deviceID, - aesKey: qrData?.aesKey, - performSecondaryDeviceRegistration: performLogIn, - composeMessage: composeTunnelbrokerMessage, - processMessage: processTunnelbrokerMessage, - performBackupRestore, - }), + React.useEffect(() => { + if (!qrData || !socketState.isAuthorized || !primaryDeviceID) { + return; + } + + void (async () => { + const message = await composeTunnelbrokerMessage(qrData?.aesKey, { + type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, + requestBackupKeys: true, + }); + await sendMessage({ + deviceID: primaryDeviceID, + payload: JSON.stringify(message), + }); + })(); + }, [ + sendMessage, + primaryDeviceID, + qrData, + socketState, + composeTunnelbrokerMessage, + ]); + + const tunnelbrokerMessageListener = React.useCallback( + async (message: TunnelbrokerMessage) => { + invariant(identityClient, 'identity context not set'); + if ( + !qrData?.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; + } + + let qrCodeAuthMessage; + try { + qrCodeAuthMessage = await processTunnelbrokerMessage( + qrData?.aesKey, + innerMessage, + ); + } catch (err) { + console.warn('Failed to decrypt Tunnelbroker QR auth message:', err); + return; + } + + if ( + qrCodeAuthMessage && + qrCodeAuthMessage.type === + qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE + ) { + const { backupID, backupDataKey, backupLogDataKey } = qrCodeAuthMessage; + void performBackupRestore?.({ + backupID, + backupDataKey, + backupLogDataKey, + }); + return; + } + + if ( + !qrCodeAuthMessage || + qrCodeAuthMessage.type !== + qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS + ) { + return; + } + const { primaryDeviceID: receivedPrimaryDeviceID, userID } = + qrCodeAuthMessage; + setPrimaryDeviceID(receivedPrimaryDeviceID); + + await performLogIn(userID); + setUnauthorizedDeviceID(null); + }, [ - qrData, + identityClient, + qrData?.aesKey, performLogIn, - composeTunnelbrokerMessage, + setUnauthorizedDeviceID, processTunnelbrokerMessage, performBackupRestore, ], ); - useQRAuth(qrAuthInput); + + React.useEffect(() => { + if (!qrData?.deviceID) { + return undefined; + } + + addListener(tunnelbrokerMessageListener); + return () => { + removeListener(tunnelbrokerMessageListener); + }; + }, [ + addListener, + removeListener, + tunnelbrokerMessageListener, + qrData?.deviceID, + ]); const value = React.useMemo( () => ({ qrData, generateQRCode, }), [qrData, generateQRCode], ); return ( {children} ); } function useQRAuthContext(): QRAuthContextType { const context = React.useContext(QRAuthContext); invariant(context, 'QRAuthContext not found'); return context; } export { QRAuthProvider, useQRAuthContext }; diff --git a/lib/hooks/qr-auth.js b/lib/hooks/qr-auth.js deleted file mode 100644 index 58120e38e..000000000 --- a/lib/hooks/qr-auth.js +++ /dev/null @@ -1,174 +0,0 @@ -// @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 type { BackupKeys } from '../types/backup-types.js'; -import { - tunnelbrokerMessageTypes, - type TunnelbrokerMessage, -} from '../types/tunnelbroker/messages.js'; -import { - peerToPeerMessageTypes, - peerToPeerMessageValidator, - type QRCodeAuthMessage, -} from '../types/tunnelbroker/peer-to-peer-message-types.js'; -import { - qrCodeAuthMessageTypes, - type QRCodeAuthMessagePayload, -} from '../types/tunnelbroker/qr-code-auth-message-types.js'; - -type QRAuthHandlerInput = { - +secondaryDeviceID: ?string, - +aesKey: ?string, - +performSecondaryDeviceRegistration: (userID: string) => Promise, - +composeMessage: ( - encryptionKey: string, - payload: QRCodeAuthMessagePayload, - ) => Promise, - +processMessage: ( - encryptionKey: string, - message: QRCodeAuthMessage, - ) => Promise, - +performBackupRestore?: (backupKeys: BackupKeys) => Promise, -}; - -function useQRAuth(input: QRAuthHandlerInput) { - const { - secondaryDeviceID, - aesKey, - processMessage, - composeMessage, - performSecondaryDeviceRegistration, - performBackupRestore, - } = input; - const [primaryDeviceID, setPrimaryDeviceID] = React.useState(); - const { - setUnauthorizedDeviceID, - addListener, - removeListener, - socketState, - sendMessage, - } = useTunnelbroker(); - - const identityContext = React.useContext(IdentityClientContext); - const identityClient = identityContext?.identityClient; - - React.useEffect(() => { - if ( - !secondaryDeviceID || - !aesKey || - !socketState.isAuthorized || - !primaryDeviceID - ) { - return; - } - - void (async () => { - const message = await composeMessage(aesKey, { - type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, - requestBackupKeys: true, - }); - await sendMessage({ - deviceID: primaryDeviceID, - payload: JSON.stringify(message), - }); - })(); - }, [ - sendMessage, - primaryDeviceID, - aesKey, - secondaryDeviceID, - composeMessage, - socketState, - ]); - - 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; - } - - let qrCodeAuthMessage; - try { - qrCodeAuthMessage = await processMessage(aesKey, innerMessage); - } catch (err) { - console.warn('Failed to decrypt Tunnelbroker QR auth message:', err); - return; - } - - if ( - qrCodeAuthMessage && - qrCodeAuthMessage.type === - qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE - ) { - const { backupID, backupDataKey, backupLogDataKey } = qrCodeAuthMessage; - void performBackupRestore?.({ - backupID, - backupDataKey, - backupLogDataKey, - }); - 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, - performBackupRestore, - processMessage, - ], - ); - - React.useEffect(() => { - if (!secondaryDeviceID) { - return undefined; - } - - addListener(tunnelbrokerMessageListener); - return () => { - removeListener(tunnelbrokerMessageListener); - }; - }, [ - secondaryDeviceID, - addListener, - removeListener, - tunnelbrokerMessageListener, - ]); -} - -export { useQRAuth };