diff --git a/lib/handlers/peer-to-peer-message-handler.js b/lib/handlers/peer-to-peer-message-handler.js index 587bfbf97..f8f659832 100644 --- a/lib/handlers/peer-to-peer-message-handler.js +++ b/lib/handlers/peer-to-peer-message-handler.js @@ -1,80 +1,76 @@ // @flow -import { olmEncryptedMessageTypes } from '../types/crypto-types.js'; import type { IdentityServiceClient, DeviceOlmInboundKeys, } from '../types/identity-service-types.js'; import { peerToPeerMessageTypes, type PeerToPeerMessage, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; async function peerToPeerMessageHandler( message: PeerToPeerMessage, identityClient: IdentityServiceClient, ): Promise { const { olmAPI } = getConfig(); if (message.type === peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION) { try { const { senderInfo, encryptedContent } = message; const { keys } = await identityClient.getInboundKeysForUser( senderInfo.userID, ); const deviceKeys: ?DeviceOlmInboundKeys = keys[senderInfo.deviceID]; if (!deviceKeys) { throw new Error( 'No keys for the device that requested creating a session, ' + `deviceID: ${senderInfo.deviceID}`, ); } await olmAPI.initializeCryptoAccount(); const result = await olmAPI.contentInboundSessionCreator( deviceKeys.identityKeysBlob.primaryIdentityPublicKeys, encryptedContent, ); console.log( 'Created inbound session with device ' + `${message.senderInfo.deviceID}: ${result}`, ); } catch (e) { console.log( 'Error creating inbound session with device ' + `${message.senderInfo.deviceID}: ${e.message}`, ); } } else if (message.type === peerToPeerMessageTypes.ENCRYPTED_MESSAGE) { try { await olmAPI.initializeCryptoAccount(); const decrypted = await olmAPI.decrypt( - { - messageType: olmEncryptedMessageTypes.TEXT, - message: message.encryptedContent, - }, + message.encryptedData, message.senderInfo.deviceID, ); console.log( 'Decrypted message from device ' + `${message.senderInfo.deviceID}: ${decrypted}`, ); } catch (e) { console.log( 'Error decrypting message from device ' + `${message.senderInfo.deviceID}: ${e.message}`, ); } } else if (message.type === peerToPeerMessageTypes.REFRESH_KEY_REQUEST) { try { await olmAPI.initializeCryptoAccount(); const oneTimeKeys = await olmAPI.getOneTimeKeys(message.numberOfKeys); await identityClient.uploadOneTimeKeys(oneTimeKeys); } catch (e) { console.log(`Error uploading one-time keys: ${e.message}`); } } } export { peerToPeerMessageHandler }; diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index cd2c425d3..2547fb3ea 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,157 +1,162 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { OlmSessionInitializationInfo } from './request-types.js'; import { type AuthMetadata } from '../shared/identity-client-context.js'; import { tShape } from '../utils/validation-utils.js'; export type OLMIdentityKeys = { +ed25519: string, +curve25519: string, }; const olmIdentityKeysValidator: TInterface = tShape({ ed25519: t.String, curve25519: t.String, }); export type OLMPrekey = { +curve25519: { +[key: string]: string, }, }; export type SignedPrekeys = { +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; export const signedPrekeysValidator: TInterface = tShape({ contentPrekey: t.String, contentPrekeySignature: t.String, notifPrekey: t.String, notifPrekeySignature: t.String, }); export type OLMOneTimeKeys = { +curve25519: { +[string]: string }, }; export type OneTimeKeysResult = { +contentOneTimeKeys: OLMOneTimeKeys, +notificationsOneTimeKeys: OLMOneTimeKeys, }; export type OneTimeKeysResultValues = { +contentOneTimeKeys: $ReadOnlyArray, +notificationsOneTimeKeys: $ReadOnlyArray, }; export type PickledOLMAccount = { +picklingKey: string, +pickledAccount: string, }; export type NotificationsOlmDataType = { +mainSession: string, +picklingKey: string, +pendingSessionUpdate: string, +updateCreationTimestamp: number, }; export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, }; export const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); export type SignedIdentityKeysBlob = { +payload: string, +signature: string, }; export const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); export type UserDetail = { +username: string, +userID: string, }; // This type should not be changed without making equivalent changes to // `Message` in Identity service's `reserved_users` module export type ReservedUsernameMessage = | { +statement: 'Add the following usernames to reserved list', +payload: $ReadOnlyArray, +issuedAt: string, } | { +statement: 'Remove the following username from reserved list', +payload: string, +issuedAt: string, } | { +statement: 'This user is the owner of the following username and user ID', +payload: UserDetail, +issuedAt: string, }; export const olmEncryptedMessageTypes = Object.freeze({ PREKEY: 0, TEXT: 1, }); export type OlmEncryptedMessageTypes = $Values; export type EncryptedData = { +message: string, +messageType: OlmEncryptedMessageTypes, }; +export const encryptedDataValidator: TInterface = + tShape({ + message: t.String, + messageType: t.Number, + }); export type ClientPublicKeys = { +primaryIdentityPublicKeys: { +ed25519: string, +curve25519: string, }, +notificationIdentityPublicKeys: { +ed25519: string, +curve25519: string, }, +blobPayload: string, +signature: string, }; export type OlmAPI = { +initializeCryptoAccount: () => Promise, +getUserPublicKey: () => Promise, +encrypt: (content: string, deviceID: string) => Promise, +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise, +contentInboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, initialEncryptedContent: string, ) => Promise, +contentOutboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ) => Promise, +notificationsSessionCreator: ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise, +getOneTimeKeys: (numberOfKeys: number) => Promise, +validateAndUploadPrekeys: (authMetadata: AuthMetadata) => Promise, +signMessage: (message: string) => Promise, }; diff --git a/lib/types/tunnelbroker/peer-to-peer-message-types.js b/lib/types/tunnelbroker/peer-to-peer-message-types.js index 34195634a..235257360 100644 --- a/lib/types/tunnelbroker/peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/peer-to-peer-message-types.js @@ -1,100 +1,101 @@ // @flow import type { TInterface, TUnion } from 'tcomb'; import t from 'tcomb'; import { tShape, tString } from '../../utils/validation-utils.js'; +import { type EncryptedData, encryptedDataValidator } from '../crypto-types.js'; import { signedDeviceListValidator, type SignedDeviceList, } from '../identity-service-types.js'; export type SenderInfo = { +userID: string, +deviceID: string, }; const senderInfoValidator: TInterface = tShape({ userID: t.String, deviceID: t.String, }); export const peerToPeerMessageTypes = Object.freeze({ OUTBOUND_SESSION_CREATION: 'OutboundSessionCreation', ENCRYPTED_MESSAGE: 'EncryptedMessage', REFRESH_KEY_REQUEST: 'RefreshKeyRequest', QR_CODE_AUTH_MESSAGE: 'QRCodeAuthMessage', DEVICE_LIST_UPDATED: 'DeviceListUpdated', }); export type OutboundSessionCreation = { +type: 'OutboundSessionCreation', +senderInfo: SenderInfo, +encryptedContent: string, }; export const outboundSessionCreationValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION), senderInfo: senderInfoValidator, encryptedContent: t.String, }); export type EncryptedMessage = { +type: 'EncryptedMessage', +senderInfo: SenderInfo, - +encryptedContent: string, + +encryptedData: EncryptedData, }; export const encryptedMessageValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.ENCRYPTED_MESSAGE), senderInfo: senderInfoValidator, - encryptedContent: t.String, + encryptedData: encryptedDataValidator, }); export type RefreshKeyRequest = { +type: 'RefreshKeyRequest', +deviceID: string, +numberOfKeys: number, }; export const refreshKeysRequestValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.REFRESH_KEY_REQUEST), deviceID: t.String, numberOfKeys: t.Number, }); export type QRCodeAuthMessage = { +type: 'QRCodeAuthMessage', +encryptedContent: string, }; export const qrCodeAuthMessageValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE), encryptedContent: t.String, }); export type DeviceListUpdated = { +type: 'DeviceListUpdated', +userID: string, +signedDeviceList: SignedDeviceList, }; export const deviceListUpdatedValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.DEVICE_LIST_UPDATED), userID: t.String, signedDeviceList: signedDeviceListValidator, }); export type PeerToPeerMessage = | OutboundSessionCreation | EncryptedMessage | RefreshKeyRequest | QRCodeAuthMessage | DeviceListUpdated; export const peerToPeerMessageValidator: TUnion = t.union([ outboundSessionCreationValidator, encryptedMessageValidator, refreshKeysRequestValidator, qrCodeAuthMessageValidator, deviceListUpdatedValidator, ]); diff --git a/native/profile/tunnelbroker-menu.react.js b/native/profile/tunnelbroker-menu.react.js index b58aad225..7dbe44cb6 100644 --- a/native/profile/tunnelbroker-menu.react.js +++ b/native/profile/tunnelbroker-menu.react.js @@ -1,238 +1,238 @@ // @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 { 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'; 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( authMetadata, identityContext.identityClient, sendMessage, ); } catch (e) { console.log(`Error creating olm sessions with own devices: ${e.message}`); } }, [identityContext, sendMessage]); const onSendEncryptedMessage = React.useCallback(async () => { try { if (!currentUserID) { return; } await olmAPI.initializeCryptoAccount(); - const { message: encrypted } = await olmAPI.encrypt( + const encryptedData = 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, + encryptedData, }; 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/web/settings/tunnelbroker-test.react.js b/web/settings/tunnelbroker-test.react.js index f1e73a56e..ec612b067 100644 --- a/web/settings/tunnelbroker-test.react.js +++ b/web/settings/tunnelbroker-test.react.js @@ -1,152 +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 { message: encrypted } = await olmAPI.encrypt( + const encryptedData = 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, + encryptedData, }; 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;