diff --git a/net/web/src/api/addChannelTopic.js b/net/web/src/api/addChannelTopic.js index 69f76d2a..e30106c3 100644 --- a/net/web/src/api/addChannelTopic.js +++ b/net/web/src/api/addChannelTopic.js @@ -1,6 +1,6 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function addChannelTopic(token, channelId, message, assets ): string { +export async function addChannelTopic(token, channelId, datatype, message, assets ): string { if (message == null && (assets == null || assets.length === 0)) { let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}`, @@ -12,7 +12,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin else if (assets == null || assets.length === 0) { let subject = { data: JSON.stringify(message, (key, value) => { if (value !== null) return value - }), datatype: 'superbasictopic' }; + }), datatype }; let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}&confirm=true`, { method: 'POST', body: JSON.stringify(subject) }); @@ -78,7 +78,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin let subject = { data: JSON.stringify(message, (key, value) => { if (value !== null) return value - }), datatype: 'superbasictopic' }; + }), datatype }; let unconfirmed = await fetchWithTimeout(`/content/channels/${channelId}/topics/${slot.id}/subject?agent=${token}`, { method: 'PUT', body: JSON.stringify(subject) }); diff --git a/net/web/src/api/addContactChannelTopic.js b/net/web/src/api/addContactChannelTopic.js index 8e962b88..34c134ff 100644 --- a/net/web/src/api/addContactChannelTopic.js +++ b/net/web/src/api/addContactChannelTopic.js @@ -1,6 +1,6 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function addContactChannelTopic(server, token, channelId, message, assets ) { +export async function addContactChannelTopic(server, token, channelId, datatype, message, assets ) { let host = ""; if (server) { host = `https://${server}` @@ -16,7 +16,7 @@ export async function addContactChannelTopic(server, token, channelId, message, else if (assets == null || assets.length === 0) { let subject = { data: JSON.stringify(message, (key, value) => { if (value !== null) return value - }), datatype: 'superbasictopic' }; + }), datatype }; let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}&confirm=true`, { method: 'POST', body: JSON.stringify(subject) }); @@ -81,7 +81,7 @@ export async function addContactChannelTopic(server, token, channelId, message, let subject = { data: JSON.stringify(message, (key, value) => { if (value !== null) return value - }), datatype: 'superbasictopic' }; + }), datatype }; let unconfirmed = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics/${slot.id}/subject?contact=${token}`, { method: 'PUT', body: JSON.stringify(subject) }); diff --git a/net/web/src/context/useCardContext.hook.js b/net/web/src/context/useCardContext.hook.js index 6dbca521..7b121f24 100644 --- a/net/web/src/context/useCardContext.hook.js +++ b/net/web/src/context/useCardContext.hook.js @@ -332,7 +332,7 @@ export function useCardContext() { let token = cardProfile.guid + '.' + cardDetail.token; let node = cardProfile.node; if (files?.length) { - const topicId = await addContactChannelTopic(node, token, channelId, null, null); + const topicId = await addContactChannelTopic(node, token, channelId, null, null, null); upload.actions.addContactTopic(node, token, cardId, channelId, topicId, files, async (assets) => { message.assets = assets; await setContactChannelTopicSubject(node, token, channelId, topicId, message); @@ -346,7 +346,7 @@ export function useCardContext() { }); } else { - await addContactChannelTopic(node, token, channelId, message, files); + await addContactChannelTopic(node, token, channelId, 'superbasictopic', message, files); } try { resync.current.push(cardId); @@ -356,6 +356,17 @@ export function useCardContext() { console.log(err); } }, + addSealedChannelTopic: async (cardId, channelId, sealKey, message) => { + let { cardProfile, cardDetail } = cards.current.get(cardId).data; + let token = cardProfile.guid + '.' + cardDetail.token; + let node = cardProfile.node; + const iv = CryptoJS.lib.WordArray.random(128 / 8); + const key = CryptoJS.enc.Hex.parse(sealKey); + const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ message }), key, { iv: iv }); + const messageEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) + const messageIv = iv.toString(); + await addContactChannelTopic(node, token, channelId, 'sealedtopic', { messageEncrypted, messageIv }); + }, getChannel: (cardId, channelId) => { let card = cards.current.get(cardId); let channel = card.channels.get(channelId); diff --git a/net/web/src/context/useChannelContext.hook.js b/net/web/src/context/useChannelContext.hook.js index 5d59e211..47db14f1 100644 --- a/net/web/src/context/useChannelContext.hook.js +++ b/net/web/src/context/useChannelContext.hook.js @@ -202,7 +202,7 @@ export function useChannelContext() { }, addChannelTopic: async (channelId, message, files) => { if (files?.length) { - const topicId = await addChannelTopic(access.current, channelId, null, null); + const topicId = await addChannelTopic(access.current, channelId, null, null, null); upload.actions.addTopic(access.current, channelId, topicId, files, async (assets) => { message.assets = assets; await setChannelTopicSubject(access.current, channelId, topicId, message); @@ -216,7 +216,7 @@ export function useChannelContext() { }); } else { - await addChannelTopic(access.current, channelId, message, files); + await addChannelTopic(access.current, channelId, 'superbasictopic', message, files); } try { await setChannels(null); @@ -225,6 +225,14 @@ export function useChannelContext() { console.log(err); } }, + addSealedChannelTopic: async (channelId, sealKey, message) => { + const iv = CryptoJS.lib.WordArray.random(128 / 8); + const key = CryptoJS.enc.Hex.parse(sealKey); + const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ message }), key, { iv: iv }); + const messageEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) + const messageIv = iv.toString(); + await addChannelTopic(access.current, channelId, 'sealedtopic', { messageEncrypted, messageIv }); + }, getChannel: (channelId) => { return channels.current.get(channelId); }, diff --git a/net/web/src/context/useConversationContext.hook.js b/net/web/src/context/useConversationContext.hook.js index d6f58a15..efa914ae 100644 --- a/net/web/src/context/useConversationContext.hook.js +++ b/net/web/src/context/useConversationContext.hook.js @@ -2,6 +2,8 @@ import { useEffect, useState, useRef, useContext } from 'react'; import { ProfileContext } from 'context/ProfileContext'; import { CardContext } from 'context/CardContext'; import { ChannelContext } from 'context/ChannelContext'; +import CryptoJS from 'crypto-js'; +import { JSEncrypt } from 'jsencrypt' export function useConversationContext() { const TOPIC_BATCH = 32; @@ -22,6 +24,7 @@ export function useConversationContext() { enabelAudio: null, enableVideo: null, sealed: false, + seals: null, image: null, logoUrl: null, logoImg: null, @@ -55,6 +58,18 @@ export function useConversationContext() { setState((s) => ({ ...s, ...value })); } + const getSeals = (conversation) => { + try { + if (conversation.data.channelDetail.dataType === 'sealed') { + return JSON.parse(conversation.data.channelDetail.data).seals; + } + } + catch (err) { + console.log(err); + } + return null; + } + const getSubject = (conversation) => { if (!conversation) { return null; @@ -160,11 +175,13 @@ export function useConversationContext() { if(topic.data.topicDetail) { cur.data.topicDetail = topic.data.topicDetail; cur.data.detailRevision = topic.data.detailRevision; + cur.data.unsealedMessage = null; } else { let slot = await getTopic(topic.id); cur.data.topicDetail = slot.data.topicDetail; cur.data.detailRevision = slot.data.detailRevision; + cur.data.unsealedMessage = null; } } cur.revision = topic.revision; @@ -212,6 +229,7 @@ export function useConversationContext() { let contacts = getContacts(chan); let subject = getSubject(chan); let members = getMembers(chan); + const seals = getSeals(chan); const enableImage = chan?.data?.channelDetail?.enableImage; const enableAudio = chan?.data?.channelDetail?.enableAudio; const enableVideo = chan?.data?.channelDetail?.enableVideo; @@ -237,6 +255,7 @@ export function useConversationContext() { init: true, error: false, sealed, + seals, subject, logoImg, logoUrl, @@ -337,6 +356,23 @@ export function useConversationContext() { return await channel.actions.removeChannel(channelId); } }, + unsealTopic: async (topicId, sealKey) => { + try { + const topic = topics.current.get(topicId); + const { messageEncrypted, messageIv } = JSON.parse(topic.data.topicDetail.data); + const iv = CryptoJS.enc.Hex.parse(messageIv); + const key = CryptoJS.enc.Hex.parse(sealKey); + const enc = CryptoJS.enc.Base64.parse(messageEncrypted); + let cipher = CryptoJS.lib.CipherParams.create({ ciphertext: enc, iv: iv }); + const dec = CryptoJS.AES.decrypt(cipher, key, { iv: iv }); + topic.data.unsealedMessage = JSON.parse(dec.toString(CryptoJS.enc.Utf8)); + topics.current.set(topicId, topic); + updateState({ topics: topics.current }); + } + catch(err) { + console.log(err); + } + }, removeTopic: async (topicId) => { const { cardId, channelId } = channelView.current; if (cardId) { diff --git a/net/web/src/session/conversation/Conversation.jsx b/net/web/src/session/conversation/Conversation.jsx index f68c2a59..c44284a6 100644 --- a/net/web/src/session/conversation/Conversation.jsx +++ b/net/web/src/session/conversation/Conversation.jsx @@ -14,7 +14,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId const thread = useRef(null); const topicRenderer = (topic) => { - return () + return () } // an unfortunate cludge for the mobile browser @@ -106,8 +106,10 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
)}
-
- +
+ { (!state.sealed || state.sealKey) && ( + + )} { state.uploadError && (
{ state.display === 'small' && ( diff --git a/net/web/src/session/conversation/addTopic/AddTopic.jsx b/net/web/src/session/conversation/addTopic/AddTopic.jsx index 9b202a28..4a0c7f6a 100644 --- a/net/web/src/session/conversation/addTopic/AddTopic.jsx +++ b/net/web/src/session/conversation/addTopic/AddTopic.jsx @@ -8,7 +8,7 @@ import { AudioFile } from './audioFile/AudioFile'; import { VideoFile } from './videoFile/VideoFile'; import { Carousel } from 'carousel/Carousel'; -export function AddTopic({ cardId, channelId }) { +export function AddTopic({ cardId, channelId, sealed, sealKey }) { const { state, actions } = useAddTopic(cardId, channelId); const attachImage = useRef(null); @@ -26,7 +26,7 @@ export function AddTopic({ cardId, channelId }) { const addTopic = async () => { if (state.messageText || state.assets.length) { try { - await actions.addTopic(); + await actions.addTopic(sealed, sealKey); } catch (err) { console.log(err); diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index aba68a7d..35203e72 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -83,7 +83,7 @@ export function useAddTopic(cardId, channelId) { setTextSize: (value) => { updateState({ textSizeSet: true, textSize: value }); }, - addTopic: async () => { + addTopic: async (sealed, sealKey) => { if (!state.busy) { try { updateState({ busy: true }); @@ -93,10 +93,20 @@ export function useAddTopic(cardId, channelId) { textSize: state.textSizeSet ? state.textSize : null, }; if (cardId) { - await card.actions.addChannelTopic(cardId, channelId, message, state.assets); + if (sealed) { + await card.actions.addSealedChannelTopic(cardId, channelId, sealKey, message, state.assets); + } + else { + await card.actions.addChannelTopic(cardId, channelId, message, state.assets); + } } else { - await channel.actions.addChannelTopic(channelId, message, state.assets); + if (sealed) { + await channel.actions.addSealedChannelTopic(channelId, sealKey, message, state.assets); + } + else { + await channel.actions.addChannelTopic(channelId, message, state.assets); + } } updateState({ busy: false, messageText: null, textColor: '#444444', textColorSet: false, textSize: 12, textSizeSet: false, assets: [] }); diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx index a5bf5809..2fa9ddb8 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.jsx +++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx @@ -8,9 +8,9 @@ import { Space, Skeleton, Button, Modal, Input } from 'antd'; import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons'; import { Carousel } from 'carousel/Carousel'; -export function TopicItem({ host, topic }) { +export function TopicItem({ host, topic, sealKey }) { - const { state, actions } = useTopicItem(topic); + const { state, actions } = useTopicItem(topic, sealKey); let name = state.name ? state.name : state.handle; let nameClass = state.name ? 'set' : 'unset'; @@ -76,7 +76,7 @@ export function TopicItem({ host, topic }) { if (state.editing) { return (
- actions.setEdit(e.target.value)} rows={3} bordered={false}/>
@@ -88,7 +88,7 @@ export function TopicItem({ host, topic }) {
); } - return
{ state.message?.text }
+ return
{ state.text }
} return ( @@ -103,9 +103,11 @@ export function TopicItem({ host, topic }) {
{ name }
{ state.created }
-
- -
+ { !state.sealed && ( +
+ +
+ )}
{ !state.confirmed && (
@@ -130,7 +132,12 @@ export function TopicItem({ host, topic }) {
)}
- + { !state.sealed && ( + + )} + { state.sealed && ( +
sealed message
+ )}
)} diff --git a/net/web/src/session/conversation/topicItem/TopicItem.styled.js b/net/web/src/session/conversation/topicItem/TopicItem.styled.js index 917e7793..cb2fb250 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.styled.js +++ b/net/web/src/session/conversation/topicItem/TopicItem.styled.js @@ -81,6 +81,11 @@ export const TopicItemWrapper = styled.div` } } + .sealed-message { + font-style: italic; + color: #aaaaaa; + } + .asset-placeholder { width: 128px; height: 128px; diff --git a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js index 57b1e0ac..34d2e0f2 100644 --- a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js +++ b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js @@ -3,14 +3,14 @@ import { ConversationContext } from 'context/ConversationContext'; import { ProfileContext } from 'context/ProfileContext'; import { CardContext } from 'context/CardContext'; -export function useTopicItem(topic) { +export function useTopicItem(topic, sealKey) { const [state, setState] = useState({ init: false, name: null, handle: null, imageUrl: null, - message: null, + text: null, created: null, confirmed: false, ready: false, @@ -39,6 +39,7 @@ export function useTopicItem(topic) { owner = true; } + let text = null; let textColor = '#444444'; let textSize = 14; @@ -47,35 +48,50 @@ export function useTopicItem(topic) { return; } - const { status, transform, data } = topic.data.topicDetail; + const { status, transform, data, dataType } = topic.data.topicDetail; let message; + let sealed = false; let ready = false; let error = false; let confirmed = false; let assets = []; if (status === 'confirmed') { confirmed = true; - try { - message = JSON.parse(data); - if (message.textColor != null) { - textColor = message.textColor; + if (dataType === 'superbasictopic') { + try { + message = JSON.parse(data); + text = message.text; + if (message.textColor != null) { + textColor = message.textColor; + } + if (message.textSize != null) { + textSize = message.textSize; + } + if (message.assets) { + assets = message.assets; + delete message.assets; + } + if (transform === 'complete') { + ready = true; + } + if (transform === 'error') { + error = true; + } } - if (message.textSize != null) { - textSize = message.textSize; - } - if (message.assets) { - assets = message.assets; - delete message.assets; - } - if (transform === 'complete') { - ready = true; - } - if (transform === 'error') { - error = true; + catch(err) { + console.log(err); } } - catch(err) { - console.log(err); + else if (dataType === 'sealedtopic') { + if (topic.data.unsealedMessage) { + text = topic.data.unsealedMessage.message.text; + sealed = false; + } + else { + conversation.actions.unsealTopic(topic.id, sealKey); + sealed = true; + } + ready = true; } } @@ -98,11 +114,11 @@ export function useTopicItem(topic) { if (profile.state.profile.guid === guid) { const { name, handle, imageUrl } = profile.actions.getProfile(); - updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true }); + updateState({ sealed, name, handle, imageUrl, status, text, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true }); } else { const { name, handle, imageUrl } = card.actions.getCardProfileByGuid(guid); - updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true }); + updateState({ sealed, name, handle, imageUrl, status, text, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true }); } } }, [profile, card, conversation, topic]); diff --git a/net/web/src/session/conversation/useConversation.hook.js b/net/web/src/session/conversation/useConversation.hook.js index 2334c71e..d9e3cee0 100644 --- a/net/web/src/session/conversation/useConversation.hook.js +++ b/net/web/src/session/conversation/useConversation.hook.js @@ -1,10 +1,13 @@ import { useContext, useState, useEffect } from 'react'; import { ViewportContext } from 'context/ViewportContext'; +import { AccountContext } from 'context/AccountContext'; import { CardContext } from 'context/CardContext'; import { ChannelContext } from 'context/ChannelContext'; import { ConversationContext } from 'context/ConversationContext'; import { UploadContext } from 'context/UploadContext'; import { StoreContext } from 'context/StoreContext'; +import CryptoJS from 'crypto-js'; +import { JSEncrypt } from 'jsencrypt' export function useConversation(cardId, channelId) { @@ -19,8 +22,11 @@ export function useConversation(cardId, channelId) { uploadError: false, uploadPercent: 0, error: false, + sealed: false, + sealKey: null, }); + const account = useContext(AccountContext); const viewport = useContext(ViewportContext); const card = useContext(CardContext); const channel = useContext(ChannelContext); @@ -36,6 +42,21 @@ export function useConversation(cardId, channelId) { updateState({ display: viewport.state.display }); }, [viewport]); + useEffect(() => { + let sealKey; + const seals = conversation.state.seals; + if (seals?.length > 0) { + seals.forEach(seal => { + if (seal.publicKey === account.state.sealKey?.public) { + let crypto = new JSEncrypt(); + crypto.setPrivateKey(account.state.sealKey.private); + sealKey = crypto.decrypt(seal.sealedKey); + } + }); + } + updateState({ sealed: conversation.state.sealed, sealKey }); + }, [account.state.sealKey, conversation.state.seals, conversation.state.sealed]); + useEffect(() => { let active = false; let uploadError = false;