diff --git a/lib/components/qr-auth-handler.react.js b/lib/hooks/qr-auth.js similarity index 95% rename from lib/components/qr-auth-handler.react.js rename to lib/hooks/qr-auth.js index 3d6472d1b..9e647d859 100644 --- a/lib/components/qr-auth-handler.react.js +++ b/lib/hooks/qr-auth.js @@ -1,177 +1,176 @@ // @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 QRAuthHandlerProps = { +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, + +performBackupRestore?: (backupKeys: BackupKeys) => Promise, }; -function QRAuthHandler(props: QRAuthHandlerProps): React.Node { +function useQRAuth(input: QRAuthHandlerInput) { const { secondaryDeviceID, aesKey, processMessage, composeMessage, performSecondaryDeviceRegistration, performBackupRestore, - } = props; + } = input; 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 = await composeMessage(aesKey, { type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, }); await sendMessage({ deviceID: primaryDeviceID, payload: JSON.stringify(message), }); })(); }, [ tunnelbrokerConnected, isAuthorized, sendMessage, primaryDeviceID, aesKey, secondaryDeviceID, composeMessage, ]); 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, ]); - return null; } -export { QRAuthHandler }; +export { useQRAuth }; diff --git a/native/qr-code/qr-code-screen.react.js b/native/qr-code/qr-code-screen.react.js index ce5c4cb9c..209b65854 100644 --- a/native/qr-code/qr-code-screen.react.js +++ b/native/qr-code/qr-code-screen.react.js @@ -1,180 +1,183 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; 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 { useQRAuth } from 'lib/hooks/qr-auth.js'; import { uintArrayToHexString } from 'lib/media/data-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { BackupKeys } from 'lib/types/backup-types.js'; import type { SignedNonce } from 'lib/types/identity-service-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import type { QRCodeSignInNavigationProp } from './qr-code-sign-in-navigator.react.js'; import { composeTunnelbrokerQRAuthMessage, parseTunnelbrokerQRAuthMessage, } from './qr-code-utils.js'; import { olmAPI } from '../crypto/olm-api.js'; import { commCoreModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import * as AES from '../utils/aes-crypto-module.js'; import Alert from '../utils/alert.js'; type QRCodeScreenProps = { +navigation: QRCodeSignInNavigationProp<'QRCodeScreen'>, +route: NavigationRoute<'QRCodeScreen'>, }; function performBackupRestore(backupKeys: BackupKeys): Promise { const { backupID, backupDataKey, backupLogDataKey } = backupKeys; return commCoreModule.restoreBackupData( backupID, backupDataKey, backupLogDataKey, ); } // eslint-disable-next-line no-unused-vars function QRCodeScreen(props: QRCodeScreenProps): React.Node { const [qrData, setQRData] = React.useState(); const { setUnauthorizedDeviceID } = useTunnelbroker(); const identityContext = React.useContext(IdentityClientContext); const identityClient = identityContext?.identityClient; const performRegistration = React.useCallback( async (userID: string) => { invariant(identityClient, 'identity context not set'); try { const nonce = await identityClient.generateNonce(); const nonceSignature = await olmAPI.signMessage(nonce); const challengeResponse: SignedNonce = { nonce, nonceSignature, }; await identityClient.uploadKeysForRegisteredDeviceAndLogIn( userID, challengeResponse, ); setUnauthorizedDeviceID(null); } catch (err) { console.error('Secondary device registration error:', err); Alert.alert('Registration failed', 'Failed to upload device keys', [ { text: 'OK' }, ]); } }, [setUnauthorizedDeviceID, identityClient], ); const generateQRCode = React.useCallback(async () => { try { const [ed25519, rawAESKey] = await Promise.all([ getContentSigningKey(), AES.generateKey(), ]); const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); setUnauthorizedDeviceID(ed25519); setQRData({ deviceID: ed25519, aesKey: aesKeyAsHexString }); } catch (err) { console.error('Failed to generate QR Code:', err); } }, [setUnauthorizedDeviceID]); React.useEffect(() => { void generateQRCode(); }, [generateQRCode]); const qrCodeURL = React.useMemo( () => (qrData ? qrCodeLinkURL(qrData.aesKey, qrData.deviceID) : undefined), [qrData], ); + const qrAuthInput = React.useMemo( + () => ({ + secondaryDeviceID: qrData?.deviceID, + aesKey: qrData?.aesKey, + performSecondaryDeviceRegistration: performRegistration, + composeMessage: composeTunnelbrokerQRAuthMessage, + processMessage: parseTunnelbrokerQRAuthMessage, + performBackupRestore, + }), + [qrData, performRegistration], + ); + useQRAuth(qrAuthInput); + const styles = useStyles(unboundStyles); return ( - <> - - - Log in to Comm - - Open the Comm app on your phone and scan the QR code below + + 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 - - - How to find the scanner: - - Go to - Profile - - - Select - Linked devices - - - Click - Add - on the top right - - - + ); } const unboundStyles = { container: { flex: 1, alignItems: 'center', marginTop: 125, }, heading: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 12, }, headingSubtext: { fontSize: 12, color: 'panelForegroundLabel', paddingBottom: 30, }, instructionsBox: { alignItems: 'center', width: 300, marginTop: 40, padding: 15, borderColor: 'panelForegroundLabel', borderWidth: 2, borderRadius: 8, }, instructionsTitle: { fontSize: 12, color: 'panelForegroundLabel', paddingBottom: 15, }, instructionsStep: { fontSize: 12, padding: 1, color: 'panelForegroundLabel', }, instructionsBold: { fontWeight: 'bold', }, }; export default QRCodeScreen; diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js index 1f3405fb2..c47b2dc23 100644 --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -1,161 +1,164 @@ // @flow import invariant from 'invariant'; import { QRCodeSVG } from 'qrcode.react'; import * as React from 'react'; import { identityLogInActionTypes } from 'lib/actions/user-actions.js'; -import { QRAuthHandler } from 'lib/components/qr-auth-handler.react.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; +import { useQRAuth } from 'lib/hooks/qr-auth.js'; import { generateKeyCommon } from 'lib/media/aes-crypto-utils-common.js'; import * as AES from 'lib/media/aes-crypto-utils-common.js'; import { hexToUintArray, uintArrayToHexString } from 'lib/media/data-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { SignedNonce } from 'lib/types/identity-service-types.js'; import { peerToPeerMessageTypes, type QRCodeAuthMessage, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { qrCodeAuthMessagePayloadValidator, type QRCodeAuthMessagePayload, } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './qr-code-login.css'; import { olmAPI } from '../crypto/olm-api.js'; import { base64DecodeBuffer, base64EncodeBuffer, } from '../utils/base64-utils.js'; import { convertBytesToObj, convertObjToBytes, } from '../utils/conversion-utils.js'; async function composeTunnelbrokerMessage( encryptionKey: string, obj: QRCodeAuthMessagePayload, ): Promise { const objBytes = convertObjToBytes(obj); const keyBytes = hexToUintArray(encryptionKey); const encryptedBytes = await AES.encryptCommon(crypto, keyBytes, objBytes); const encryptedContent = base64EncodeBuffer(encryptedBytes); return { type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, encryptedContent, }; } async function parseTunnelbrokerMessage( encryptionKey: string, message: QRCodeAuthMessage, ): Promise { const encryptedData = base64DecodeBuffer(message.encryptedContent); const decryptedData = await AES.decryptCommon( crypto, hexToUintArray(encryptionKey), new Uint8Array(encryptedData), ); const payload = convertBytesToObj(decryptedData); if (!qrCodeAuthMessagePayloadValidator.is(payload)) { return null; } return payload; } function QRCodeLogin(): React.Node { const [qrData, setQRData] = React.useState(); const { setUnauthorizedDeviceID } = useTunnelbroker(); const identityContext = React.useContext(IdentityClientContext); const identityClient = identityContext?.identityClient; const dispatchActionPromise = useDispatchActionPromise(); const performRegistration = React.useCallback( async (userID: string) => { invariant(identityClient, 'identity context not set'); try { const nonce = await identityClient.generateNonce(); const nonceSignature = await olmAPI.signMessage(nonce); const challengeResponse: SignedNonce = { nonce, nonceSignature, }; await dispatchActionPromise( identityLogInActionTypes, identityClient.uploadKeysForRegisteredDeviceAndLogIn( userID, challengeResponse, ), ); setUnauthorizedDeviceID(null); } catch (err) { console.error('Secondary device registration error:', err); } }, [dispatchActionPromise, identityClient, setUnauthorizedDeviceID], ); const generateQRCode = React.useCallback(async () => { try { const [ed25519, rawAESKey] = await Promise.all([ getContentSigningKey(), generateKeyCommon(crypto), ]); const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); setUnauthorizedDeviceID(ed25519); setQRData({ deviceID: ed25519, aesKey: aesKeyAsHexString }); } catch (err) { console.error('Failed to generate QR Code:', err); } }, [setUnauthorizedDeviceID]); React.useEffect(() => { void generateQRCode(); }, [generateQRCode]); const qrCodeURL = React.useMemo( () => (qrData ? qrCodeLinkURL(qrData.aesKey, qrData.deviceID) : undefined), [qrData], ); + const qrAuthInput = React.useMemo( + () => ({ + secondaryDeviceID: qrData?.deviceID, + aesKey: qrData?.aesKey, + performSecondaryDeviceRegistration: performRegistration, + composeMessage: composeTunnelbrokerMessage, + processMessage: parseTunnelbrokerMessage, + }), + [qrData, performRegistration], + ); + useQRAuth(qrAuthInput); + return ( - <> - -
-
Log in to Comm
-
- Open the Comm app on your phone and scan the QR code below +
+
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
- -
-
How to find the scanner:
-
- Go to Profile -
-
- Select Linked devices -
-
- Click Add on the top right -
+
+ Click Add on the top right
- +
); } export default QRCodeLogin;