diff --git a/lib/utils/crypto-utils.js b/lib/utils/crypto-utils.js index 4f3b3f776..3bc5ec26e 100644 --- a/lib/utils/crypto-utils.js +++ b/lib/utils/crypto-utils.js @@ -1,30 +1,116 @@ // @flow import t from 'tcomb'; import { type TInterface } from 'tcomb'; +import { getConfig } from './config.js'; import { primaryIdentityPublicKeyRegex } from './siwe-utils.js'; import { tRegex, tShape } from './validation-utils.js'; +import type { AuthMetadata } from '../shared/identity-client-context.js'; +import type { ClientMessageToDevice } from '../tunnelbroker/tunnelbroker-context.js'; import type { IdentityKeysBlob, OLMIdentityKeys, SignedIdentityKeysBlob, } from '../types/crypto-types'; +import type { IdentityServiceClient } from '../types/identity-service-types'; +import { + type OutboundSessionCreation, + peerToPeerMessageTypes, +} from '../types/tunnelbroker/peer-to-peer-message-types.js'; const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); const olmIdentityKeysValidator: TInterface = tShape({ ed25519: tRegex(primaryIdentityPublicKeyRegex), curve25519: tRegex(primaryIdentityPublicKeyRegex), }); const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); -export { signedIdentityKeysBlobValidator, identityKeysBlobValidator }; +async function getContentSigningKey(): Promise { + const { olmAPI } = getConfig(); + await olmAPI.initializeCryptoAccount(); + const { + primaryIdentityPublicKeys: { ed25519 }, + } = await olmAPI.getUserPublicKey(); + return ed25519; +} + +async function createOlmSessionsWithOwnDevices( + authMetadata: AuthMetadata, + identityClient: IdentityServiceClient, + sendMessage: (message: ClientMessageToDevice) => Promise, +): Promise { + const { olmAPI } = getConfig(); + const { userID, deviceID, accessToken } = authMetadata; + await olmAPI.initializeCryptoAccount(); + + if (!userID || !deviceID || !accessToken) { + throw new Error('CommServicesAuthMetadata is missing'); + } + + const keysResponse = await identityClient.getOutboundKeysForUser(userID); + + for (const deviceKeys of keysResponse) { + const { keys } = deviceKeys; + if (!keys) { + console.log(`Keys missing for device ${deviceKeys.deviceID}`); + continue; + } + + const { primaryIdentityPublicKeys } = keys.identityKeysBlob; + + if (primaryIdentityPublicKeys.ed25519 === deviceID) { + continue; + } + const recipientDeviceID = primaryIdentityPublicKeys.ed25519; + + if (!keys.contentInitializationInfo.oneTimeKey) { + console.log(`One-time key is missing for device ${recipientDeviceID}`); + continue; + } + try { + const encryptedContent = await olmAPI.contentOutboundSessionCreator( + primaryIdentityPublicKeys, + keys.contentInitializationInfo, + ); + + const sessionCreationMessage: OutboundSessionCreation = { + type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, + senderInfo: { + userID, + deviceID, + }, + encryptedContent, + }; + + await sendMessage({ + deviceID: recipientDeviceID, + payload: JSON.stringify(sessionCreationMessage), + }); + console.log( + `Request to create a session with device ${recipientDeviceID} sent.`, + ); + } catch (e) { + console.log( + 'Error creating outbound session with ' + + `device ${recipientDeviceID}: ${e.message}`, + ); + } + } +} + +export { + signedIdentityKeysBlobValidator, + identityKeysBlobValidator, + getContentSigningKey, + createOlmSessionsWithOwnDevices, +}; diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js index 02dd3fbed..495bd9a59 100644 --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -1,257 +1,257 @@ // @flow import BottomSheet from '@gorhom/bottom-sheet'; import * as React from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import WebView from 'react-native-webview'; import { getSIWENonce, getSIWENonceActionTypes, siweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { identityGenerateNonceActionTypes, useIdentityGenerateNonce, } from 'lib/actions/user-actions.js'; import type { ServerCallSelectorParams } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { SIWEWebViewMessage, SIWEResult } from 'lib/types/siwe-types.js'; import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.js'; +import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { useKeyboardHeight } from '../keyboard/keyboard-hooks.js'; import { useSelector } from '../redux/redux-utils.js'; import type { BottomSheetRef } from '../types/bottom-sheet.js'; import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; import { defaultLandingURLPrefix } from '../utils/url-utils.js'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const identityGenerateNonceLoadingStatusSelector = createLoadingStatusSelector( identityGenerateNonceActionTypes, ); const legacySiweAuthLoadingStatusSelector = createLoadingStatusSelector(siweAuthActionTypes); type WebViewMessageEvent = { +nativeEvent: { +data: string, ... }, ... }; type Props = { +onClosed: () => mixed, +onClosing: () => mixed, +onSuccessfulWalletSignature: SIWEResult => mixed, +closing: boolean, +setLoading: boolean => mixed, +keyserverCallParamOverride?: Partial, }; function SIWEPanel(props: Props): React.Node { const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useLegacyAshoatKeyserverCall( getSIWENonce, props.keyserverCallParamOverride, ); const identityGenerateNonce = useIdentityGenerateNonce(); const legacyGetSIWENonceCallFailed = useSelector( state => getSIWENonceLoadingStatusSelector(state) === 'error', ); const identityGenerateNonceFailed = useSelector( state => identityGenerateNonceLoadingStatusSelector(state) === 'error', ); const { onClosing } = props; const legacySiweAuthCallLoading = useSelector( state => legacySiweAuthLoadingStatusSelector(state) === 'loading', ); const [nonce, setNonce] = React.useState(null); const [primaryIdentityPublicKey, setPrimaryIdentityPublicKey] = React.useState(null); React.useEffect(() => { const generateNonce = async (nonceFunction: () => Promise) => { try { const response = await nonceFunction(); setNonce(response); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: onClosing }], { cancelable: false }, ); throw e; } }; void (async () => { if (usingCommServicesAccessToken) { void dispatchActionPromise( identityGenerateNonceActionTypes, generateNonce(identityGenerateNonce), ); } else { void dispatchActionPromise( getSIWENonceActionTypes, generateNonce(getSIWENonceCall), ); } const ed25519 = await getContentSigningKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, [ dispatchActionPromise, getSIWENonceCall, identityGenerateNonce, onClosing, ]); const [isLoading, setLoading] = React.useState(true); const [walletConnectModalHeight, setWalletConnectModalHeight] = React.useState(0); const insets = useSafeAreaInsets(); const keyboardHeight = useKeyboardHeight(); const bottomInset = insets.bottom; const snapPoints = React.useMemo(() => { if (isLoading) { return [1]; } else if (walletConnectModalHeight) { const baseHeight = bottomInset + walletConnectModalHeight + keyboardHeight; if (baseHeight < 400) { return [baseHeight - 10]; } else { return [baseHeight + 5]; } } else { const baseHeight = bottomInset + keyboardHeight; return [baseHeight + 435, baseHeight + 600]; } }, [isLoading, walletConnectModalHeight, bottomInset, keyboardHeight]); const bottomSheetRef = React.useRef(); const snapToIndex = bottomSheetRef.current?.snapToIndex; React.useEffect(() => { // When the snapPoints change, always reset to the first one // Without this, when we close the WalletConnect modal we don't resize snapToIndex?.(0); }, [snapToIndex, snapPoints]); const closeBottomSheet = bottomSheetRef.current?.close; const { closing, onSuccessfulWalletSignature } = props; const handleMessage = React.useCallback( async (event: WebViewMessageEvent) => { const data: SIWEWebViewMessage = JSON.parse(event.nativeEvent.data); if (data.type === 'siwe_success') { const { address, message, signature } = data; if (address && signature) { closeBottomSheet?.(); await onSuccessfulWalletSignature({ address, message, signature }); } } else if (data.type === 'siwe_closed') { onClosing(); closeBottomSheet?.(); } else if (data.type === 'walletconnect_modal_update') { const height = data.state === 'open' ? data.height : 0; setWalletConnectModalHeight(height); } }, [onSuccessfulWalletSignature, onClosing, closeBottomSheet], ); const prevClosingRef = React.useRef(); React.useEffect(() => { if (closing && !prevClosingRef.current) { closeBottomSheet?.(); } prevClosingRef.current = closing; }, [closing, closeBottomSheet]); const source = React.useMemo( () => ({ uri: commSIWE, headers: { 'siwe-nonce': nonce, 'siwe-primary-identity-public-key': primaryIdentityPublicKey, }, }), [nonce, primaryIdentityPublicKey], ); const onWebViewLoaded = React.useCallback(() => { setLoading(false); }, []); const walletConnectModalOpen = walletConnectModalHeight !== 0; const backgroundStyle = React.useMemo( () => ({ backgroundColor: walletConnectModalOpen ? '#3396ff' : '#242529', }), [walletConnectModalOpen], ); const bottomSheetHandleIndicatorStyle = React.useMemo( () => ({ backgroundColor: 'white', }), [], ); const { onClosed } = props; const onBottomSheetChange = React.useCallback( (index: number) => { if (index === -1) { onClosed(); } }, [onClosed], ); let bottomSheet; if (nonce && primaryIdentityPublicKey) { bottomSheet = ( ); } const setLoadingProp = props.setLoading; const loading = !legacyGetSIWENonceCallFailed && !identityGenerateNonceFailed && (isLoading || legacySiweAuthCallLoading); React.useEffect(() => { setLoadingProp(loading); }, [setLoadingProp, loading]); return bottomSheet; } export default SIWEPanel; diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js index 436373fd8..861101f28 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,76 +1,76 @@ // @flow import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { fetchNativeKeychainCredentials } from '../account/native-credentials.js'; import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; type ClientBackup = { +uploadBackupProtocol: () => Promise, +restoreBackupProtocol: () => Promise, }; async function getBackupSecret(): Promise { const nativeCredentials = await fetchNativeKeychainCredentials(); if (!nativeCredentials) { throw new Error('Native credentials are missing'); } return nativeCredentials.password; } function useClientBackup(): ClientBackup { const accessToken = useSelector(state => state.commServicesAccessToken); const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const loggedIn = useSelector(isLoggedIn); const setMockCommServicesAuthMetadata = React.useCallback(async () => { if (!currentUserID) { return; } const ed25519 = await getContentSigningKey(); await commCoreModule.setCommServicesAuthMetadata( currentUserID, ed25519, accessToken ? accessToken : '', ); }, [accessToken, currentUserID]); const uploadBackupProtocol = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload backup for not logged in user.'); } console.info('Start uploading backup...'); await setMockCommServicesAuthMetadata(); const backupSecret = await getBackupSecret(); await commCoreModule.createNewBackup(backupSecret); console.info('Backup uploaded.'); }, [currentUserID, loggedIn, setMockCommServicesAuthMetadata]); const restoreBackupProtocol = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to restore backup for not logged in user.'); } console.info('Start restoring backup...'); await setMockCommServicesAuthMetadata(); const backupSecret = await getBackupSecret(); await commCoreModule.restoreBackup(backupSecret); console.info('Backup restored.'); }, [currentUserID, loggedIn, setMockCommServicesAuthMetadata]); return { uploadBackupProtocol, restoreBackupProtocol }; } export { getBackupSecret, useClientBackup }; diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js index 0651ea532..0596af300 100644 --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -1,553 +1,553 @@ // @flow import * as React from 'react'; import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { type IdentityKeysBlob, identityKeysBlobValidator, type OneTimeKeysResultValues, } from 'lib/types/crypto-types.js'; import { type SignedDeviceList, signedDeviceListHistoryValidator, type SignedMessage, type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type IdentityServiceClient, type UserDevicesOlmOutboundKeys, type UserAuthMetadata, ONE_TIME_KEYS_NUMBER, identityAuthResultValidator, type DeviceOlmInboundKeys, type UserDevicesOlmInboundKeys, deviceOlmInboundKeysValidator, userDeviceOlmInboundKeysValidator, } from 'lib/types/identity-service-types.js'; +import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { getCommServicesAuthMetadataEmitter } from '../event-emitters/csa-auth-metadata-emitter.js'; import { commCoreModule, commRustModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const userIDPromiseRef = React.useRef>(); if (!userIDPromiseRef.current) { userIDPromiseRef.current = (async () => { const { userID } = await commCoreModule.getCommServicesAuthMetadata(); return userID; })(); } React.useEffect(() => { const metadataEmitter = getCommServicesAuthMetadataEmitter(); const subscription = metadataEmitter.addListener( 'commServicesAuthMetadata', (authMetadata: UserAuthMetadata) => { userIDPromiseRef.current = Promise.resolve(authMetadata.userID); }, ); return () => subscription.remove(); }, []); const accessToken = useSelector(state => state.commServicesAccessToken); const getAuthMetadata = React.useCallback< () => Promise<{ +deviceID: string, +userID: string, +accessToken: string, }>, >(async () => { const deviceID = await getContentSigningKey(); const userID = await userIDPromiseRef.current; if (!deviceID || !userID || !accessToken) { throw new Error('Identity service client is not initialized'); } return { deviceID, userID, accessToken }; }, [accessToken]); const client = React.useMemo( () => ({ deleteUser: async () => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.deleteUser(userID, deviceID, token); }, logOut: async () => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); return commRustModule.logOut(userID, deviceID, token); }, getKeyserverKeys: async ( keyserverID: string, ): Promise => { const { deviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getKeyserverKeys( userID, deviceID, token, keyserverID, ); const resultObject = JSON.parse(result); const payload = resultObject?.payload; const keyserverKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: resultObject?.contentPrekey, prekeySignature: resultObject?.contentPrekeySignature, oneTimeKey: resultObject?.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: resultObject?.notifPrekey, prekeySignature: resultObject?.notifPrekeySignature, oneTimeKey: resultObject?.oneTimeNotifPrekey, }, payloadSignature: resultObject?.payloadSignature, socialProof: resultObject?.socialProof, }; if (!keyserverKeys.contentInitializationInfo.oneTimeKey) { throw new Error('Missing content one time key'); } if (!keyserverKeys.notifInitializationInfo.oneTimeKey) { throw new Error('Missing notif one time key'); } return assertWithValidator( keyserverKeys, deviceOlmOutboundKeysValidator, ); }, getOutboundKeysForUser: async ( targetUserID: string, ): Promise => { const { deviceID: authDeviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getOutboundKeysForUser( userID, authDeviceID, token, targetUserID, ); const resultArray = JSON.parse(result); return resultArray .map(outboundKeysInfo => { try { const payload = outboundKeysInfo?.payload; const identityKeysBlob: IdentityKeysBlob = assertWithValidator( payload ? JSON.parse(payload) : null, identityKeysBlobValidator, ); const deviceID = identityKeysBlob.primaryIdentityPublicKeys.ed25519; if ( !outboundKeysInfo.oneTimeContentPrekey || !outboundKeysInfo.oneTimeNotifPrekey ) { console.log(`Missing one time key for device ${deviceID}`); return { deviceID, keys: null, }; } const deviceKeys = { identityKeysBlob, contentInitializationInfo: { prekey: outboundKeysInfo?.contentPrekey, prekeySignature: outboundKeysInfo?.contentPrekeySignature, oneTimeKey: outboundKeysInfo?.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: outboundKeysInfo?.notifPrekey, prekeySignature: outboundKeysInfo?.notifPrekeySignature, oneTimeKey: outboundKeysInfo?.oneTimeNotifPrekey, }, payloadSignature: outboundKeysInfo?.payloadSignature, socialProof: outboundKeysInfo?.socialProof, }; try { const validatedKeys = assertWithValidator( deviceKeys, deviceOlmOutboundKeysValidator, ); return { deviceID, keys: validatedKeys, }; } catch (e) { console.log(e); return { deviceID, keys: null, }; } } catch (e) { console.log(e); return null; } }) .filter(Boolean); }, getInboundKeysForUser: async ( targetUserID: string, ): Promise => { const { deviceID: authDeviceID, userID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getInboundKeysForUser( userID, authDeviceID, token, targetUserID, ); const resultArray = JSON.parse(result); const devicesKeys: { [deviceID: string]: ?DeviceOlmInboundKeys, } = {}; resultArray.forEach(inboundKeysInfo => { try { const payload = inboundKeysInfo?.payload; const identityKeysBlob: IdentityKeysBlob = assertWithValidator( payload ? JSON.parse(payload) : null, identityKeysBlobValidator, ); const deviceID = identityKeysBlob.primaryIdentityPublicKeys.ed25519; const deviceKeys = { identityKeysBlob, signedPrekeys: { contentPrekey: inboundKeysInfo?.contentPrekey, contentPrekeySignature: inboundKeysInfo?.contentPrekeySignature, notifPrekey: inboundKeysInfo?.notifPrekey, notifPrekeySignature: inboundKeysInfo?.notifPrekeySignature, }, payloadSignature: inboundKeysInfo?.payloadSignature, }; try { devicesKeys[deviceID] = assertWithValidator( deviceKeys, deviceOlmInboundKeysValidator, ); } catch (e) { console.log(e); devicesKeys[deviceID] = null; } } catch (e) { console.log(e); } }); const device = resultArray?.[0]; const inboundUserKeys = { keys: devicesKeys, username: device?.username, walletAddress: device?.walletAddress, }; return assertWithValidator( inboundUserKeys, userDeviceOlmInboundKeysValidator, ); }, uploadOneTimeKeys: async (oneTimeKeys: OneTimeKeysResultValues) => { const { deviceID: authDeviceID, userID, accessToken: token, } = await getAuthMetadata(); await commRustModule.uploadOneTimeKeys( userID, authDeviceID, token, oneTimeKeys.contentOneTimeKeys, oneTimeKeys.notificationsOneTimeKeys, ); }, registerPasswordUser: async (username: string, password: string) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const registrationResult = await commRustModule.registerPasswordUser( username, password, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), ); const { userID, accessToken: token } = JSON.parse(registrationResult); const identityAuthResult = { accessToken: token, userID, username }; const validatedResult = assertWithValidator( identityAuthResult, identityAuthResultValidator, ); await commCoreModule.setCommServicesAuthMetadata( validatedResult.userID, primaryIdentityPublicKeys.ed25519, validatedResult.accessToken, ); return validatedResult; }, logInPasswordUser: async (username: string, password: string) => { await commCoreModule.initializeCryptoAccount(); const [{ blobPayload, signature, primaryIdentityPublicKeys }, prekeys] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.validateAndGetPrekeys(), ]); const loginResult = await commRustModule.logInPasswordUser( username, password, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, ); const { userID, accessToken: token } = JSON.parse(loginResult); const identityAuthResult = { accessToken: token, userID, username }; const validatedResult = assertWithValidator( identityAuthResult, identityAuthResultValidator, ); await commCoreModule.setCommServicesAuthMetadata( validatedResult.userID, primaryIdentityPublicKeys.ed25519, validatedResult.accessToken, ); return validatedResult; }, registerWalletUser: async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const registrationResult = await commRustModule.registerWalletUser( siweMessage, siweSignature, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), ); const { userID, accessToken: token } = JSON.parse(registrationResult); const identityAuthResult = { accessToken: token, userID, username: walletAddress, }; return assertWithValidator( identityAuthResult, identityAuthResultValidator, ); }, logInWalletUser: async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { await commCoreModule.initializeCryptoAccount(); const [{ blobPayload, signature, primaryIdentityPublicKeys }, prekeys] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.validateAndGetPrekeys(), ]); const loginResult = await commRustModule.logInWalletUser( siweMessage, siweSignature, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, ); const { userID, accessToken: token } = JSON.parse(loginResult); const identityAuthResult = { accessToken: token, userID, username: walletAddress, }; const validatedResult = assertWithValidator( identityAuthResult, identityAuthResultValidator, ); await commCoreModule.setCommServicesAuthMetadata( validatedResult.userID, primaryIdentityPublicKeys.ed25519, validatedResult.accessToken, ); return validatedResult; }, uploadKeysForRegisteredDeviceAndLogIn: async ( userID: string, nonceChallengeResponse: SignedMessage, ) => { await commCoreModule.initializeCryptoAccount(); const [ { blobPayload, signature, primaryIdentityPublicKeys }, { contentOneTimeKeys, notificationsOneTimeKeys }, prekeys, ] = await Promise.all([ commCoreModule.getUserPublicKey(), commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); const challengeResponse = JSON.stringify(nonceChallengeResponse); const registrationResult = await commRustModule.uploadSecondaryDeviceKeysAndLogIn( userID, challengeResponse, blobPayload, signature, prekeys.contentPrekey, prekeys.contentPrekeySignature, prekeys.notifPrekey, prekeys.notifPrekeySignature, getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), ); const { accessToken: token } = JSON.parse(registrationResult); const identityAuthResult = { accessToken: token, userID, username: '' }; const validatedResult = assertWithValidator( identityAuthResult, identityAuthResultValidator, ); await commCoreModule.setCommServicesAuthMetadata( validatedResult.userID, primaryIdentityPublicKeys.ed25519, validatedResult.accessToken, ); return validatedResult; }, generateNonce: commRustModule.generateNonce, getDeviceListHistoryForUser: async ( userID: string, sinceTimestamp?: number, ) => { const { deviceID: authDeviceID, userID: authUserID, accessToken: token, } = await getAuthMetadata(); const result = await commRustModule.getDeviceListForUser( authUserID, authDeviceID, token, userID, sinceTimestamp, ); const rawPayloads: string[] = JSON.parse(result); const deviceLists: SignedDeviceList[] = rawPayloads.map(payload => JSON.parse(payload), ); return assertWithValidator( deviceLists, signedDeviceListHistoryValidator, ); }, updateDeviceList: async (newDeviceList: SignedDeviceList) => { const { deviceID: authDeviceID, userID, accessToken: authAccessToken, } = await getAuthMetadata(); const payload = JSON.stringify(newDeviceList); await commRustModule.updateDeviceList( userID, authDeviceID, authAccessToken, payload, ); }, }), [getAuthMetadata], ); const value = React.useMemo( () => ({ identityClient: client, getAuthMetadata, }), [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider; diff --git a/native/profile/tunnelbroker-menu.react.js b/native/profile/tunnelbroker-menu.react.js index 29ebdedc7..4379eebf9 100644 --- a/native/profile/tunnelbroker-menu.react.js +++ b/native/profile/tunnelbroker-menu.react.js @@ -1,227 +1,237 @@ // @flow import * as React from 'react'; import { useState } from 'react'; import { Text, View } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { TunnelbrokerMessage } from 'lib/types/tunnelbroker/messages.js'; import { type EncryptedMessage, peerToPeerMessageTypes, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; +import { + createOlmSessionsWithOwnDevices, + getContentSigningKey, +} from 'lib/utils/crypto-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import Button from '../components/button.react.js'; import TextInput from '../components/text-input.react.js'; -import { commCoreModule } from '../native-modules.js'; +import { olmAPI } from '../crypto/olm-api.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.js'; -import { - createOlmSessionsWithOwnDevices, - getContentSigningKey, -} from '../utils/crypto-utils.js'; type Props = { +navigation: ProfileNavigationProp<'TunnelbrokerMenu'>, +route: NavigationRoute<'TunnelbrokerMenu'>, }; // eslint-disable-next-line no-unused-vars function TunnelbrokerMenu(props: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); + const identityContext = React.useContext(IdentityClientContext); const { connected, addListener, sendMessage, removeListener } = useTunnelbroker(); const [messages, setMessages] = useState([]); const [recipient, setRecipient] = useState(''); const [message, setMessage] = useState(''); const listener = React.useCallback((msg: TunnelbrokerMessage) => { setMessages(prev => [...prev, msg]); }, []); React.useEffect(() => { addListener(listener); return () => removeListener(listener); }, [addListener, listener, removeListener]); const onSubmit = React.useCallback(async () => { try { await sendMessage({ deviceID: recipient, payload: message }); } catch (e) { console.log(e.message); } }, [message, recipient, sendMessage]); const onCreateSessions = React.useCallback(async () => { + if (!identityContext) { + return; + } + const authMetadata = await identityContext.getAuthMetadata(); try { - await createOlmSessionsWithOwnDevices(sendMessage); + await createOlmSessionsWithOwnDevices( + authMetadata, + identityContext.identityClient, + sendMessage, + ); } catch (e) { console.log(`Error creating olm sessions with own devices: ${e.message}`); } - }, [sendMessage]); + }, [identityContext, sendMessage]); const onSendEncryptedMessage = React.useCallback(async () => { try { if (!currentUserID) { return; } - await commCoreModule.initializeCryptoAccount(); - const encrypted = await commCoreModule.encrypt( + await olmAPI.initializeCryptoAccount(); + const encrypted = await olmAPI.encrypt( `Encrypted message to ${recipient}`, recipient, ); const deviceID = await getContentSigningKey(); const encryptedMessage: EncryptedMessage = { type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, senderInfo: { deviceID, userID: currentUserID, }, encryptedContent: encrypted, }; await sendMessage({ deviceID: recipient, payload: JSON.stringify(encryptedMessage), }); } catch (e) { console.log(`Error sending encrypted content to device: ${e.message}`); } }, [currentUserID, recipient, sendMessage]); return ( INFO Connected {connected.toString()} SEND MESSAGE Recipient Message MESSAGES {messages.map(msg => ( {JSON.stringify(msg)} ))} ); } const unboundStyles = { scrollViewContentContainer: { paddingTop: 24, }, scrollView: { backgroundColor: 'panelBackground', }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, marginVertical: 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, submenuButton: { flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 10, alignItems: 'center', }, submenuText: { color: 'panelForegroundLabel', flex: 1, fontSize: 16, }, text: { color: 'panelForegroundLabel', fontSize: 16, }, row: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 14, }, textInput: { color: 'modalBackgroundLabel', flex: 1, fontSize: 16, margin: 0, padding: 0, borderBottomColor: 'transparent', }, }; export default TunnelbrokerMenu; diff --git a/native/qr-code/qr-code-screen.react.js b/native/qr-code/qr-code-screen.react.js index f7313cc7f..331983859 100644 --- a/native/qr-code/qr-code-screen.react.js +++ b/native/qr-code/qr-code-screen.react.js @@ -1,181 +1,181 @@ // @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 { 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 { NonceChallenge, SignedMessage, } 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 { 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'; -import { getContentSigningKey } from '../utils/crypto-utils.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 [qrCodeValue, setQrCodeValue] = React.useState(); 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 nonceChallenge: NonceChallenge = { nonce }; const nonceMessage = JSON.stringify(nonceChallenge); const signature = await commCoreModule.signMessage(nonceMessage); const challengeResponse: SignedMessage = { message: nonceMessage, signature, }; 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 rawAESKey: Uint8Array = await AES.generateKey(); const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); 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(); }, [generateQRCode]); 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 ); } 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/native/utils/crypto-utils.js b/native/utils/crypto-utils.js index 1d2f0c2e7..aa8dec937 100644 --- a/native/utils/crypto-utils.js +++ b/native/utils/crypto-utils.js @@ -1,132 +1,47 @@ // @flow -import { type ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; -import type { - IdentityKeysBlob, - OLMIdentityKeys, -} from 'lib/types/crypto-types.js'; -import type { OutboundKeyInfoResponse } from 'lib/types/identity-service-types'; +import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; -import { - type OutboundSessionCreation, - peerToPeerMessageTypes, -} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; -import { commCoreModule, commRustModule } from '../native-modules.js'; +import { commCoreModule } from '../native-modules.js'; function nativeNotificationsSessionCreator( notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ): Promise { const { prekey, prekeySignature, oneTimeKey } = notificationsInitializationInfo; return commCoreModule.initializeNotificationsSession( JSON.stringify(notificationsIdentityKeys), prekey, prekeySignature, oneTimeKey, keyserverID, ); } -async function getContentSigningKey(): Promise { - await commCoreModule.initializeCryptoAccount(); - const { - primaryIdentityPublicKeys: { ed25519 }, - } = await commCoreModule.getUserPublicKey(); - return ed25519; -} - function nativeOutboundContentSessionCreator( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, deviceID: string, ): Promise { const { prekey, prekeySignature, oneTimeKey } = contentInitializationInfo; const identityKeys = JSON.stringify({ curve25519: contentIdentityKeys.curve25519, ed25519: contentIdentityKeys.ed25519, }); return commCoreModule.initializeContentOutboundSession( identityKeys, prekey, prekeySignature, oneTimeKey, deviceID, ); } -async function createOlmSessionsWithOwnDevices( - sendMessage: (message: ClientMessageToDevice) => Promise, -): Promise { - const authMetadata = await commCoreModule.getCommServicesAuthMetadata(); - const { userID, deviceID, accessToken } = authMetadata; - if (!userID || !deviceID || !accessToken) { - throw new Error('CommServicesAuthMetadata is missing'); - } - - await commCoreModule.initializeCryptoAccount(); - const keysResponse = await commRustModule.getOutboundKeysForUser( - userID, - deviceID, - accessToken, - userID, - ); - - const outboundKeys: OutboundKeyInfoResponse[] = JSON.parse(keysResponse); - - for (const deviceKeys: OutboundKeyInfoResponse of outboundKeys) { - const keysPayload: IdentityKeysBlob = JSON.parse(deviceKeys.payload); - - if (keysPayload.primaryIdentityPublicKeys.ed25519 === deviceID) { - continue; - } - const recipientDeviceID = keysPayload.primaryIdentityPublicKeys.ed25519; - if (!deviceKeys.oneTimeContentPrekey) { - console.log(`One-time key is missing for device ${recipientDeviceID}`); - continue; - } - try { - const encryptedContent = await nativeOutboundContentSessionCreator( - keysPayload.primaryIdentityPublicKeys, - { - prekey: deviceKeys.contentPrekey, - prekeySignature: deviceKeys.contentPrekeySignature, - oneTimeKey: deviceKeys.oneTimeContentPrekey, - }, - recipientDeviceID, - ); - - const sessionCreationMessage: OutboundSessionCreation = { - type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, - senderInfo: { - userID, - deviceID, - }, - encryptedContent, - }; - - await sendMessage({ - deviceID: recipientDeviceID, - payload: JSON.stringify(sessionCreationMessage), - }); - console.log( - `Request to create a session with device ${recipientDeviceID} sent.`, - ); - } catch (e) { - console.log( - 'Error creating outbound session with ' + - `device ${recipientDeviceID}: ${e.message}`, - ); - } - } -} - export { - getContentSigningKey, nativeNotificationsSessionCreator, - createOlmSessionsWithOwnDevices, nativeOutboundContentSessionCreator, }; diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js index a6329fce4..bb6c12bc4 100644 --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -1,213 +1,238 @@ // @flow import * as React from 'react'; import { useLogOut, logOutActionTypes } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import { createOlmSessionsWithOwnDevices } from 'lib/utils/crypto-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './account-settings.css'; import AppearanceChangeModal from './appearance-change-modal.react.js'; import BackupTestRestoreModal from './backup-test-restore-modal.react.js'; import PasswordChangeModal from './password-change-modal.js'; import BlockListModal from './relationship/block-list-modal.react.js'; import FriendListModal from './relationship/friend-list-modal.react.js'; import TunnelbrokerMessagesScreen from './tunnelbroker-message-list.react.js'; import TunnelbrokerTestScreen from './tunnelbroker-test.react.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Button from '../components/button.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function AccountSettings(): React.Node { const sendLogoutRequest = useLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const logOutUser = React.useCallback( () => dispatchActionPromise(logOutActionTypes, sendLogoutRequest()), [dispatchActionPromise, sendLogoutRequest], ); + const identityContext = React.useContext(IdentityClientContext); const { pushModal, popModal } = useModalContext(); const showPasswordChangeModal = React.useCallback( () => pushModal(), [pushModal], ); const openFriendList = React.useCallback( () => pushModal(), [pushModal], ); const openBlockList = React.useCallback( () => pushModal(), [pushModal], ); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const currentUserInfo = useSelector(state => state.currentUserInfo); const stringForUser = useStringForUser(currentUserInfo); const staffCanSee = useStaffCanSee(); const { sendMessage, connected, addListener, removeListener } = useTunnelbroker(); const openTunnelbrokerModal = React.useCallback( () => pushModal( , ), [popModal, pushModal, sendMessage], ); const openTunnelbrokerMessagesModal = React.useCallback( () => pushModal( , ), [addListener, popModal, pushModal, removeListener], ); + const onCreateOlmSessions = React.useCallback(async () => { + if (!identityContext) { + return; + } + const authMetadata = await identityContext.getAuthMetadata(); + try { + await createOlmSessionsWithOwnDevices( + authMetadata, + identityContext.identityClient, + sendMessage, + ); + } catch (e) { + console.log(`Error creating olm sessions with own devices: ${e.message}`); + } + }, [identityContext, sendMessage]); + const openBackupTestRestoreModal = React.useCallback( () => pushModal(), [popModal, pushModal], ); const showAppearanceModal = React.useCallback( () => pushModal(), [pushModal], ); if (!currentUserInfo || currentUserInfo.anonymous) { return null; } let changePasswordSection; if (isAccountWithPassword) { changePasswordSection = (
  • Password ******
  • ); } let preferences; if (staffCanSee) { preferences = (

    Preferences

    • Appearance
    ); } let tunnelbroker; if (staffCanSee) { tunnelbroker = (

    Tunnelbroker menu

    • Connected {connected.toString()}
    • Send message to device
    • Trace received messages
    • +
    • + Create session with own devices + +
    ); } let backup; if (staffCanSee) { backup = (

    Backup menu

    • Test backup restore
    ); } return (

    My Account

    • {'Logged in as '} {stringForUser}

    • {changePasswordSection}
    • Friend List
    • Block List
    {preferences} {tunnelbroker} {backup}
    ); } export default AccountSettings; diff --git a/web/settings/tunnelbroker-test.react.js b/web/settings/tunnelbroker-test.react.js index ddb2198e3..3fb44220b 100644 --- a/web/settings/tunnelbroker-test.react.js +++ b/web/settings/tunnelbroker-test.react.js @@ -1,94 +1,152 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { type ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import { + type EncryptedMessage, + peerToPeerMessageTypes, +} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; +import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import css from './tunnelbroker-test.css'; import Button from '../components/button.react.js'; +import { olmAPI } from '../crypto/olm-api.js'; import Input from '../modals/input.react.js'; import Modal from '../modals/modal.react.js'; +import { useSelector } from '../redux/redux-utils.js'; type Props = { +sendMessage: (message: ClientMessageToDevice) => Promise, +onClose: () => void, }; function TunnelbrokerTestScreen(props: Props): React.Node { const { sendMessage, onClose } = props; const [recipient, setRecipient] = React.useState(''); const [message, setMessage] = React.useState(''); const [loading, setLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); const recipientInput = React.useRef(null); const messageInput = React.useRef(null); + const currentUserID = useSelector( + state => state.currentUserInfo && state.currentUserInfo.id, + ); + const onSubmit = React.useCallback( async (event: SyntheticEvent) => { event.preventDefault(); setLoading(true); try { await sendMessage({ deviceID: recipient, payload: message }); } catch (e) { setErrorMessage(e.message); } setLoading(false); }, [message, recipient, sendMessage], ); + const onSubmitEncrypted = React.useCallback( + async (event: SyntheticEvent) => { + event.preventDefault(); + + if (!currentUserID) { + return; + } + + setLoading(true); + try { + await olmAPI.initializeCryptoAccount(); + const encrypted = await olmAPI.encrypt( + `Encrypted message to ${recipient}`, + recipient, + ); + const deviceID = await getContentSigningKey(); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { + deviceID, + userID: currentUserID, + }, + encryptedContent: encrypted, + }; + await sendMessage({ + deviceID: recipient, + payload: JSON.stringify(encryptedMessage), + }); + } catch (e) { + setErrorMessage(e.message); + } + setLoading(false); + }, + [currentUserID, recipient, sendMessage], + ); + let errorMsg; if (errorMessage) { errorMsg =
    {errorMessage}
    ; } return (
    ) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); setRecipient(target.value); }} disabled={loading} ref={recipientInput} label="Recipient" /> ) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); setMessage(target.value); }} disabled={loading} ref={messageInput} label="Message" />
    {errorMsg}
    +
    + + {errorMsg} +
    ); } export default TunnelbrokerTestScreen;