unsealing messages in sealed conversation in webapp

This commit is contained in:
Roland Osborne 2022-12-15 13:57:36 -08:00
parent c44bf282de
commit faca16f748
12 changed files with 165 additions and 49 deletions

View File

@ -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) });

View File

@ -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) });

View File

@ -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);

View File

@ -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);
},

View File

@ -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) {

View File

@ -14,7 +14,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
const thread = useRef(null);
const topicRenderer = (topic) => {
return (<TopicItem host={cardId == null} topic={topic} />)
return (<TopicItem host={cardId == null} topic={topic} sealKey={state.sealKey} />)
}
// an unfortunate cludge for the mobile browser
@ -106,8 +106,10 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
<div class="progress-idle" />
)}
</div>
<div class="topic">
<AddTopic cardId={cardId} channelId={channelId} />
<div class="topic">
{ (!state.sealed || state.sealKey) && (
<AddTopic cardId={cardId} channelId={channelId} sealed={state.sealed} sealKey={state.sealKey} />
)}
{ state.uploadError && (
<div class="upload-error">
{ state.display === 'small' && (

View File

@ -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);

View File

@ -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: [] });

View File

@ -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 (
<div class="editing">
<Input.TextArea defaultValue={state.message?.text} placeholder="message"
<Input.TextArea defaultValue={state.text} placeholder="message"
style={{ resize: 'none', color: state.textColor, fontSize: state.textSize }}
onChange={(e) => actions.setEdit(e.target.value)} rows={3} bordered={false}/>
<div class="controls">
@ -88,7 +88,7 @@ export function TopicItem({ host, topic }) {
</div>
);
}
return <div style={{ color: state.textColor, fontSize: state.textSize }}>{ state.message?.text }</div>
return <div style={{ color: state.textColor, fontSize: state.textSize }}>{ state.text }</div>
}
return (
@ -103,9 +103,11 @@ export function TopicItem({ host, topic }) {
<div class={nameClass}>{ name }</div>
<div>{ state.created }</div>
</div>
<div class="topic-options">
<Options />
</div>
{ !state.sealed && (
<div class="topic-options">
<Options />
</div>
)}
</div>
{ !state.confirmed && (
<div class="skeleton">
@ -130,7 +132,12 @@ export function TopicItem({ host, topic }) {
</div>
)}
<div class="message">
<Message />
{ !state.sealed && (
<Message />
)}
{ state.sealed && (
<div class="sealed-message">sealed message</div>
)}
</div>
</div>
)}

View File

@ -81,6 +81,11 @@ export const TopicItemWrapper = styled.div`
}
}
.sealed-message {
font-style: italic;
color: #aaaaaa;
}
.asset-placeholder {
width: 128px;
height: 128px;

View File

@ -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]);

View File

@ -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;