From b73685008cbafdd3b083361c99685f1be9d97438 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 21 Apr 2021 16:48:13 +1000 Subject: [PATCH] add getMessages and postMessages OpenGroupAPIV2 --- ts/components/session/ActionsPanel.tsx | 19 +++- ts/data/opengroups.ts | 8 +- ts/opengroup/opengroupV2/ApiUtil.ts | 62 +++++++++++ ts/opengroup/opengroupV2/OpenGroupAPIV2.ts | 100 +++++++++++++----- .../opengroupV2/OpenGroupMessageV2.ts | 62 +++++------ 5 files changed, 190 insertions(+), 61 deletions(-) diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index c69f75dfe..c8e42cef0 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -3,8 +3,6 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { Avatar } from '../Avatar'; import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme'; import { SessionToastContainer } from './SessionToastContainer'; -import { ConversationType } from '../../state/ducks/conversations'; -import { DefaultTheme } from 'styled-components'; import { ConversationController } from '../../session/conversations'; import { UserUtils } from '../../session/utils'; import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; @@ -35,8 +33,11 @@ import { } from '../../opengroup/opengroupV2/JoinOpenGroupV2'; import { getAuthToken, + getMessages, getModerators, + postMessage, } from '../../opengroup/opengroupV2/OpenGroupAPIV2'; +import { OpenGroupMessageV2 } from '../../opengroup/opengroupV2/OpenGroupMessageV2'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -193,7 +194,19 @@ export const ActionsPanel = () => { if (parsedRoom) { setTimeout(async () => { await joinOpenGroupV2(parsedRoom); - await getModerators({ + const oldMessages = await getMessages({ + serverUrl: parsedRoom.serverUrl, + roomId: parsedRoom.roomId, + }); + const msg = new OpenGroupMessageV2({ + base64EncodedData: 'dffdldfkldf', + sentTimestamp: Date.now(), + }); + const postedMessage = await postMessage(msg, { + serverUrl: parsedRoom.serverUrl, + roomId: parsedRoom.roomId, + }); + const newMessages = await getMessages({ serverUrl: parsedRoom.serverUrl, roomId: parsedRoom.roomId, }); diff --git a/ts/data/opengroups.ts b/ts/data/opengroups.ts index 970037c88..25af97542 100644 --- a/ts/data/opengroups.ts +++ b/ts/data/opengroups.ts @@ -1,3 +1,4 @@ +import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil'; import { channels } from './channels'; export type OpenGroupV2Room = { @@ -47,10 +48,9 @@ export async function getV2OpenGroupRoom( return opengroupv2Rooms; } -export async function getV2OpenGroupRoomByRoomId(roomInfos: { - serverUrl: string; - roomId: string; -}): Promise { +export async function getV2OpenGroupRoomByRoomId( + roomInfos: OpenGroupRequestCommonType +): Promise { console.warn('getting roomInfo', roomInfos); const room = await channels.getV2OpenGroupRoomByRoomId( roomInfos.serverUrl, diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index e138c3754..e2c354f15 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -1,7 +1,23 @@ +import _ from 'underscore'; +import { getSodium } from '../../session/crypto'; +import { PubKey } from '../../session/types'; +import { + fromBase64ToArray, + fromBase64ToArrayBuffer, + fromHex, + fromHexToArray, +} from '../../session/utils/String'; +import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; + export const defaultServer = 'https://sessionopengroup.com'; export const defaultServerPublicKey = '658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b'; +export type OpenGroupRequestCommonType = { + serverUrl: string; + roomId: string; +}; + export type OpenGroupV2Request = { method: 'GET' | 'POST' | 'DELETE' | 'PUT'; room: string; @@ -70,3 +86,49 @@ export const setCachedModerators = ( allRoomsMods!.get(roomId)?.add(m); }); }; + +export const parseMessages = async ( + rawMessages: Array> +): Promise> => { + const messages = await Promise.all( + rawMessages.map(async r => { + try { + const opengroupMessage = OpenGroupMessageV2.fromJson(r); + console.warn('opengroupMessage', opengroupMessage); + if ( + !opengroupMessage?.serverId || + !opengroupMessage.sentTimestamp || + !opengroupMessage.base64EncodedData || + !opengroupMessage.base64EncodedSignature + ) { + window.log.warn('invalid open group message received'); + return null; + } + // Validate the message signature + const senderPubKey = PubKey.cast( + opengroupMessage.sender + ).withoutPrefix(); + const signature = fromBase64ToArrayBuffer( + opengroupMessage.base64EncodedSignature + ); + const messageData = fromBase64ToArrayBuffer( + opengroupMessage.base64EncodedData + ); + // throws if signature failed + await window.libsignal.Curve.async.verifySignature( + fromHex(senderPubKey), + messageData, + signature + ); + return opengroupMessage; + } catch (e) { + window.log.error( + 'An error happened while fetching getMessages output:', + e + ); + return null; + } + }) + ); + return _.compact(messages); +}; diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts index 9f257f271..62091068c 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts @@ -1,10 +1,18 @@ +import _ from 'lodash'; import { getV2OpenGroupRoomByRoomId, saveV2OpenGroupRoom, } from '../../data/opengroups'; +import { getSodium } from '../../session/crypto'; import { sendViaOnion } from '../../session/onions/onionSend'; +import { PubKey } from '../../session/types'; import { allowOnlyOneAtATime } from '../../session/utils/Promise'; -import { fromBase64ToArrayBuffer, toHex } from '../../session/utils/String'; +import { + fromBase64ToArray, + fromBase64ToArrayBuffer, + fromHexToArray, + toHex, +} from '../../session/utils/String'; import { getIdentityKeyPair, getOurPubKeyStrFromCache, @@ -12,10 +20,13 @@ import { import { buildUrl, cachedModerators, + OpenGroupRequestCommonType, OpenGroupV2Info, OpenGroupV2Request, + parseMessages, setCachedModerators, } from './ApiUtil'; +import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; // This function might throw async function sendOpenGroupV2Request( @@ -89,10 +100,7 @@ async function sendOpenGroupV2Request( export async function requestNewAuthToken({ serverUrl, roomId, -}: { - serverUrl: string; - roomId: string; -}): Promise { +}: OpenGroupRequestCommonType): Promise { const userKeyPair = await getIdentityKeyPair(); if (!userKeyPair) { throw new Error('Failed to fetch user keypair'); @@ -211,10 +219,7 @@ async function claimAuthToken( export async function getAuthToken({ serverUrl, roomId, -}: { - serverUrl: string; - roomId: string; -}): Promise { +}: OpenGroupRequestCommonType): Promise { // first try to fetch from db a saved token. const roomDetails = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId }); if (!roomDetails) { @@ -246,10 +251,7 @@ export async function getAuthToken({ export const getModerators = async ({ serverUrl, roomId, -}: { - serverUrl: string; - roomId: string; -}): Promise> => { +}: OpenGroupRequestCommonType): Promise> => { const request: OpenGroupV2Request = { method: 'GET', room: roomId, @@ -276,10 +278,7 @@ export const getModerators = async ({ export const deleteAuthToken = async ({ serverUrl, roomId, -}: { - serverUrl: string; - roomId: string; -}) => { +}: OpenGroupRequestCommonType) => { const request: OpenGroupV2Request = { method: 'DELETE', room: roomId, @@ -298,21 +297,74 @@ export const deleteAuthToken = async ({ export const getMessages = async ({ serverUrl, roomId, -}: { - serverUrl: string; - roomId: string; -}) => { +}: OpenGroupRequestCommonType): Promise> => { + const roomInfos = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId }); + if (!roomInfos) { + throw new Error('Could not find this room getMessages'); + } + const { lastMessageFetchedServerID } = roomInfos; + + const queryParams = {} as Record; + if (lastMessageFetchedServerID) { + queryParams.from_server_id = lastMessageFetchedServerID; + } + const request: OpenGroupV2Request = { method: 'GET', room: roomId, server: serverUrl, - isAuthRequired: false, - endpoint: 'auth_token', + isAuthRequired: true, + endpoint: 'messages', }; const result = (await sendOpenGroupV2Request(request)) as any; if (result?.result?.status_code !== 200) { throw new Error( - `Could not deleteAuthToken, status code: ${result?.result?.status_code}` + `Could not getMessages, status code: ${result?.result?.status_code}` ); } + + // we have a 200 + const rawMessages = result?.result?.messages as Array>; + if (!rawMessages) { + window.log.info('no new messages'); + return []; + } + const validMessages = await parseMessages(rawMessages); + console.warn('validMessages', validMessages); + return validMessages; +}; + +export const postMessage = async ( + message: OpenGroupMessageV2, + room: OpenGroupRequestCommonType +) => { + try { + const signedMessage = await message.sign(); + const json = signedMessage.toJson(); + console.warn('posting message json', json); + + const request: OpenGroupV2Request = { + method: 'POST', + room: room.roomId, + server: room.serverUrl, + queryParams: json, + isAuthRequired: true, + endpoint: 'messages', + }; + const result = (await sendOpenGroupV2Request(request)) as any; + if (result?.result?.status_code !== 200) { + throw new Error( + `Could not postMessage, status code: ${result?.result?.status_code}` + ); + } + const rawMessage = result?.result?.message; + if (!rawMessage) { + throw new Error('postMessage parsing failed'); + } + // this will throw if the json is not valid + return OpenGroupMessageV2.fromJson(rawMessage); + } catch (e) { + window.log.error('Failed to post message to open group v2', e); + throw e; + } }; diff --git a/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts b/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts index 835e49634..b23aaf1bf 100644 --- a/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupMessageV2.ts @@ -1,6 +1,7 @@ import { getSodium } from '../../session/crypto'; import { UserUtils } from '../../session/utils'; import { + fromArrayBufferToBase64, fromBase64ToArray, fromHex, fromHexToArray, @@ -36,17 +37,39 @@ export class OpenGroupMessageV2 { this.serverId = serverId; } - public async sign() { - const ourKeyPair = await UserUtils.getUserED25519KeyPair(); + public static fromJson(json: Record) { + const { + data: base64EncodedData, + timestamp: sentTimestamp, + server_id: serverId, + public_key: sender, + signature: base64EncodedSignature, + } = json; + + if (!base64EncodedData || !sentTimestamp) { + window.log.info('invalid json to build OpenGroupMessageV2'); + throw new Error('OpengroupV2Message fromJson() failed'); + } + return new OpenGroupMessageV2({ + base64EncodedData, + base64EncodedSignature, + sentTimestamp, + serverId, + sender, + }); + } + + public async sign(): Promise { + const ourKeyPair = await UserUtils.getIdentityKeyPair(); if (!ourKeyPair) { window.log.warn("Couldn't find user X25519 key pair."); - return null; + throw new Error("Couldn't sign message"); } + const data = fromBase64ToArray(this.base64EncodedData); - const sodium = await getSodium(); - const signature = sodium.crypto_sign_detached( - data, - fromHexToArray(ourKeyPair.privKey) + const signature = await window.libsignal.Curve.async.calculateSignature( + ourKeyPair.privKey, + data.buffer ); if (!signature || signature.length === 0) { throw new Error("Couldn't sign message"); @@ -54,7 +77,7 @@ export class OpenGroupMessageV2 { return new OpenGroupMessageV2({ base64EncodedData: this.base64EncodedData, sentTimestamp: this.sentTimestamp, - base64EncodedSignature: toHex(signature), + base64EncodedSignature: fromArrayBufferToBase64(signature), sender: this.sender, serverId: this.serverId, }); @@ -74,27 +97,6 @@ export class OpenGroupMessageV2 { if (this.base64EncodedSignature) { json.signature = this.base64EncodedSignature; } - } - - public fromJson(json: Record) { - const { - data: base64EncodedData, - timestamp: sentTimestamp, - server_id: serverId, - public_key: sender, - signature: base64EncodedSignature, - } = json; - - if (!base64EncodedData || !sentTimestamp) { - window.log.info('invalid json to build OpenGroupMessageV2'); - return null; - } - return new OpenGroupMessageV2({ - base64EncodedData, - base64EncodedSignature, - sentTimestamp, - serverId, - sender, - }); + return json; } }