Porównaj commity

...

17 Commity
develop ... pgp

Autor SHA1 Wiadomość Data
Alex Gleason 0607719c80
Merge remote-tracking branch 'origin/develop' into pgp 2022-06-28 16:13:30 -05:00
Alex Gleason 43cbdb700f
Merge remote-tracking branch 'origin/develop' into pgp 2022-06-16 20:42:40 -05:00
Alex Gleason 45ef9337d0
OpenPGP.js: configure a comment for armored messages 2022-06-15 14:59:21 -05:00
Alex Gleason b9282a20f1
ChatBox: don't clear encrypted state after sending a message 2022-06-14 20:22:40 -05:00
Alex Gleason 0ec1c73baa
ChatMessageList: 'Encrypted message' --> 'Encryption key' 2022-06-14 20:20:53 -05:00
Alex Gleason 7f765e511b
ChatMessageList: decrypt encrypted messages 2022-06-14 20:19:08 -05:00
Alex Gleason 583a08ea96
ChatBox: also encrypt message to self 2022-06-14 20:18:57 -05:00
Alex Gleason 82b61bcf78
ChatBox: allow sending PGP encrypted message 2022-06-14 19:19:59 -05:00
Alex Gleason ad7741847d
ChatBox: allow toggling encryption button 2022-06-14 18:49:23 -05:00
Alex Gleason 195edae41f
Store contact PGP publicKey in browser storage 2022-06-14 18:43:01 -05:00
Alex Gleason ba7a5a80b4
utils/pgp: don't use openpgp/lightweight as it doesn't work with jest 2022-06-14 18:32:32 -05:00
Alex Gleason 25337f80c5
ChatMessageList: fix message author 2022-06-14 18:12:25 -05:00
Alex Gleason e8433a88ab
ChatBox: click lock button to initiate key exchange 2022-06-14 18:02:32 -05:00
Alex Gleason c65239dd15
ChatBox: add encryption lock button 2022-06-14 17:54:28 -05:00
Alex Gleason 42eb9e8ddc
Generate a PGP key on pageload 2022-06-14 17:27:53 -05:00
Alex Gleason eafa26d8c1
ChatMessageList: rudimentary display of PGP messages 2022-06-14 16:53:34 -05:00
Alex Gleason 44dd45e27b
Add pgp utils to identify pgp messages 2022-06-14 16:30:12 -05:00
10 zmienionych plików z 318 dodań i 30 usunięć

Wyświetl plik

@ -1,4 +1,5 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { encrypt, createMessage, readKey } from 'openpgp';
import React, { useRef, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
@ -7,11 +8,12 @@ import {
markChatRead,
} from 'soapbox/actions/chats';
import { uploadMedia } from 'soapbox/actions/media';
import IconButton from 'soapbox/components/icon_button';
import { Stack, HStack, IconButton } from 'soapbox/components/ui';
import UploadProgress from 'soapbox/components/upload-progress';
import UploadButton from 'soapbox/features/compose/components/upload_button';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { truncateFilename } from 'soapbox/utils/media';
import { initPgpKey, getPgpKey } from 'soapbox/utils/pgp';
import ChatMessageList from './chat-message-list';
@ -35,6 +37,9 @@ interface IChatBox {
const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const chat = useAppSelector(state => state.chats.items.get(chatId));
const account = useOwnAccount();
const chatMessageIds = useAppSelector(state => state.chat_message_lists.get(chatId, ImmutableOrderedSet<string>()));
const [content, setContent] = useState('');
@ -42,6 +47,7 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [resetFileKey, setResetFileKey] = useState<number>(fileKeyGen());
const [encrypted, setEncrypted] = useState(false);
const inputElem = useRef<HTMLTextAreaElement | null>(null);
@ -53,9 +59,31 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
setResetFileKey(fileKeyGen());
};
const getParams = () => {
const encryptMessage = async() => {
if (!account) return null;
if (!chat?.account) return null;
const keys = await getPgpKey(account.fqn) as any;
const recipientKeys = await getPgpKey(chat.account) as any;
const publicKeys = await Promise.all([
readKey({ armoredKey: keys.publicKey }),
readKey({ armoredKey: recipientKeys.publicKey }),
]);
const privateKey = await readKey({ armoredKey: keys.privateKey });
const message = await createMessage({ text: content });
return await encrypt({
message,
encryptionKeys: publicKeys,
signingKeys: privateKey as any,
});
};
const getParams = async() => {
return {
content,
content: encrypted ? await encryptMessage() : content,
media_id: attachment && attachment.id,
};
};
@ -69,9 +97,9 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
return conds.some(c => c);
};
const sendMessage = () => {
const sendMessage = async() => {
if (canSubmit() && !isUploading) {
const params = getParams();
const params = await getParams();
dispatch(sendChatMessage(chatId, params));
clearState();
@ -152,15 +180,50 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
);
};
const sendPublicKey = async() => {
if (!account) return;
const { publicKey } = await initPgpKey(account.fqn) as any;
const params = {
content: publicKey,
};
dispatch(sendChatMessage(chatId, params));
};
const handleEncryptionButtonClick = () => {
setEncrypted(true);
sendPublicKey();
};
const renderEncryptionButton = () => {
return (
<IconButton
className='text-gray-400 hover:text-gray-600'
iconClassName='h-5 w-5'
src={encrypted ? require('@tabler/icons/icons/lock.svg') : require('@tabler/icons/icons/lock-open.svg')}
onClick={handleEncryptionButtonClick}
transparent
/>
);
};
const renderActionButton = () => {
return canSubmit() ? (
<IconButton
className='text-gray-400 hover:text-gray-600'
iconClassName='h-5 w-5'
src={require('@tabler/icons/icons/send.svg')}
title={intl.formatMessage(messages.send)}
onClick={sendMessage}
transparent
/>
) : (
<UploadButton onSelectFile={handleFiles} resetFileKey={resetFileKey} />
<UploadButton
iconClassName='h-5 w-5'
onSelectFile={handleFiles}
resetFileKey={resetFileKey}
/>
);
};
@ -174,9 +237,12 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
<UploadProgress progress={uploadProgress * 100} />
)}
<div className='chat-box__actions simple_form'>
<div className='chat-box__send'>
{renderActionButton()}
</div>
<Stack justifyContent='center' className='absolute right-2.5 inset-y-0'>
<HStack>
{renderEncryptionButton()}
{renderActionButton()}
</HStack>
</Stack>
<textarea
rows={1}
placeholder={intl.formatMessage(messages.placeholder)}

Wyświetl plik

@ -6,6 +6,7 @@ import {
} from 'immutable';
import escape from 'lodash/escape';
import throttle from 'lodash/throttle';
import { readMessage, decrypt, readKey } from 'openpgp';
import React, { useState, useEffect, useRef, useLayoutEffect, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { createSelector } from 'reselect';
@ -13,12 +14,14 @@ import { createSelector } from 'reselect';
import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats';
import { openModal } from 'soapbox/actions/modals';
import { initReportById } from 'soapbox/actions/reports';
import { Text } from 'soapbox/components/ui';
import { Text, Icon, HStack } from 'soapbox/components/ui';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import emojify from 'soapbox/features/emoji/emoji';
import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useAppDispatch, useRefEventHandler } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useRefEventHandler, useOwnAccount } from 'soapbox/hooks';
import { unescapeHTML } from 'soapbox/utils/html';
import { isPgpMessage, isPgpEncryptedMessage, getPgpKey } from 'soapbox/utils/pgp';
import { onlyEmoji } from 'soapbox/utils/rich_content';
import type { Menu } from 'soapbox/components/dropdown_menu';
@ -76,10 +79,12 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
const dispatch = useAppDispatch();
const me = useAppSelector(state => state.me);
const account = useOwnAccount();
const chatMessages = useAppSelector(state => getChatMessages(state.chat_messages, chatMessageIds));
const [initialLoad, setInitialLoad] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [decryptedMessages, setDecryptedMessages] = useState<any>({});
const node = useRef<HTMLDivElement>(null);
const messagesEnd = useRef<HTMLDivElement>(null);
@ -214,7 +219,68 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
};
};
const renderMessage = (chatMessage: ChatMessageEntity) => {
const decryptMessage = async(chatMessage: ChatMessageEntity) => {
if (!account) return;
const content = unescapeHTML(chatMessage.content);
const keys = await getPgpKey(account.fqn) as any;
const recipientKeys = await getPgpKey(chatMessage.account_id) as any;
const publicKey = await readKey({ armoredKey: recipientKeys.publicKey });
const privateKey = await readKey({ armoredKey: keys.privateKey });
const message = await readMessage({ armoredMessage: content });
const { data: decrypted } = await decrypt({
message,
verificationKeys: publicKey,
decryptionKeys: privateKey as any,
});
setDecryptedMessages({ ...decryptedMessages, [chatMessage.id]: decrypted });
};
const renderDecryptedMessage = (chatMessage: ChatMessageEntity) => {
const decryptedContent = decryptedMessages[chatMessage.id];
if (!decryptedContent) {
decryptMessage(chatMessage);
}
const newMessage = chatMessage.set('content', decryptedContent || '...');
return renderMessage(newMessage, true);
};
const renderEncryptedMessage = (chatMessage: ChatMessageEntity) => {
if (isPgpEncryptedMessage(unescapeHTML(chatMessage.content))) {
return renderDecryptedMessage(chatMessage);
}
return (
<div
className={classNames('chat-message', {
'chat-message--me': chatMessage.get('account_id') === me,
'chat-message--pending': chatMessage.get('pending', false) === true,
})}
key={chatMessage.get('id')}
>
<div
title={getFormattedTimestamp(chatMessage)}
className='chat-message__bubble'
ref={setBubbleRef}
tabIndex={0}
>
<HStack space={1} alignItems='center'>
<Icon size={14} src={require('@tabler/icons/icons/info-circle.svg')} />
<Text size='xs' className='italic'>
Encryption key
</Text>
</HStack>
</div>
</div>
);
};
const renderMessage = (chatMessage: ChatMessageEntity, encrypted = false) => {
const menu: Menu = [
{
text: intl.formatMessage(messages.delete),
@ -247,7 +313,13 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
tabIndex={0}
>
{maybeRenderMedia(chatMessage)}
<Text size='sm' dangerouslySetInnerHTML={{ __html: parseContent(chatMessage) }} />
{encrypted && (
<Icon size={14} src={require('@tabler/icons/icons/lock.svg')} />
)}
<div className='chat-message__menu'>
<DropdownMenuContainer
items={menu}
@ -324,7 +396,12 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
}
}
acc.push(renderMessage(curr));
if (isPgpMessage(curr.get('content'))) {
acc.push(renderEncryptedMessage(curr));
} else {
acc.push(renderMessage(curr));
}
return acc;
}, [] as React.ReactNode[])}
<div style={{ float: 'left', clear: 'both' }} ref={messagesEnd} />

Wyświetl plik

@ -20,6 +20,8 @@ interface IUploadButton {
onSelectFile: (files: FileList) => void,
style?: React.CSSProperties,
resetFileKey: number,
/** Class name for the <svg> icon. */
iconClassName?: string,
}
const UploadButton: React.FC<IUploadButton> = ({
@ -27,6 +29,7 @@ const UploadButton: React.FC<IUploadButton> = ({
unavailable = false,
onSelectFile,
resetFileKey,
iconClassName,
}) => {
const intl = useIntl();
@ -56,6 +59,7 @@ const UploadButton: React.FC<IUploadButton> = ({
<IconButton
src={src}
className='text-gray-400 hover:text-gray-600'
iconClassName={iconClassName}
title={intl.formatMessage(messages.upload)}
disabled={disabled}
onClick={handleClick}

Wyświetl plik

@ -34,6 +34,7 @@ import ProfilePage from 'soapbox/pages/profile_page';
import RemoteInstancePage from 'soapbox/pages/remote_instance_page';
import StatusPage from 'soapbox/pages/status_page';
import { getAccessToken, getVapidKey } from 'soapbox/utils/auth';
import { initPgpKey } from 'soapbox/utils/pgp';
import { cacheCurrentUrl } from 'soapbox/utils/redirect';
import { isStandalone } from 'soapbox/utils/state';
// import GroupSidebarPanel from '../groups/sidebar_panel';
@ -443,6 +444,8 @@ const UI: React.FC = ({ children }) => {
const loadAccountData = () => {
if (!account) return;
initPgpKey(account.fqn);
dispatch(expandHomeTimeline());
dispatch(expandNotifications())

Wyświetl plik

@ -11,6 +11,8 @@ import {
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { normalizeChatMessage } from 'soapbox/normalizers';
import { unescapeHTML } from 'soapbox/utils/html';
import { isPgpPublicKeyMessage, savePgpKey } from 'soapbox/utils/pgp';
import type { AnyAction } from 'redux';
@ -21,7 +23,15 @@ type APIEntities = Array<APIEntity>;
type State = ImmutableMap<string, ChatMessageRecord>;
const importMessage = (state: State, message: APIEntity) => {
return state.set(message.id, normalizeChatMessage(message));
const normalMessage = normalizeChatMessage(message);
const content = unescapeHTML(normalMessage.content);
if (isPgpPublicKeyMessage(content)) {
// FIXME: use account fqn instead of account_id
savePgpKey(normalMessage.account_id, content);
}
return state.set(normalMessage.id, normalMessage);
};
const importMessages = (state: State, messages: APIEntities) =>

Wyświetl plik

@ -0,0 +1,62 @@
import { isPgpMessage } from '../pgp';
describe('isPgpMessage()', () => {
it('rejects a non-PGP message', () => {
expect(isPgpMessage('abcdefg-----BEGIN PGP')).toBe(false);
});
it('identifies a PGP public key', () => {
const message = `-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=LtJq
-----END PGP PUBLIC KEY BLOCK-----`;
expect(isPgpMessage(message)).toBe(true);
});
});

Wyświetl plik

@ -0,0 +1,57 @@
import { generateKey, config } from 'openpgp';
import KVStore from 'soapbox/storage/kv_store';
// Configure OpenPGP.js
// https://docs.openpgpjs.org/module-config.html
config.commentString = 'https://soapbox.pub/#pgp';
config.showComment = true;
/**
* Detect whether a message contains valid PGP headers.
* @see {@link https://datatracker.ietf.org/doc/html/rfc4880#section-7}
*/
const isPgpMessage = (message: string): boolean => {
return /^-----BEGIN PGP [A-Z ]+-----/.test(message);
};
/** Check whether a message contains a PGP public key. */
const isPgpPublicKeyMessage = (message: string): boolean => {
return /^-----BEGIN PGP PUBLIC KEY BLOCK-----/.test(message);
};
/** Whether the message is a PGP encrypted message. */
const isPgpEncryptedMessage = (message: string): boolean => {
return /^-----BEGIN PGP MESSAGE-----/.test(message);
};
/** Generate a key and store it in the browser, if one doesn't already exist. */
const initPgpKey = async(fqn: string) => {
const item = await KVStore.getItem(`pgp:${fqn}`);
if (item) {
return item;
} else {
const key = generateKey({ userIDs: [{ name: fqn }] });
return await KVStore.setItem(`pgp:${fqn}`, key);
}
};
/** Store the public key of another user. */
const savePgpKey = async(fqn: string, publicKey: string) => {
return await KVStore.setItem(`pgp:${fqn}`, { publicKey });
};
/** Get PGP keys for the given account. */
const getPgpKey = async(fqn: string) => {
return await KVStore.getItem(`pgp:${fqn}`);
};
export {
isPgpMessage,
isPgpPublicKeyMessage,
isPgpEncryptedMessage,
initPgpKey,
savePgpKey,
getPgpKey,
};

Wyświetl plik

@ -286,20 +286,6 @@
overflow: hidden;
}
}
&__send {
.icon-button,
button {
@apply absolute right-2.5 w-auto h-auto border-0 p-0 m-0 text-black dark:text-white;
top: 50%;
transform: translateY(-50%);
.svg-icon,
svg {
@apply w-[18px] h-[18px];
}
}
}
}
@media (max-width: 630px) {

Wyświetl plik

@ -142,6 +142,7 @@
"object-assign": "^4.1.1",
"object-fit-images": "^3.2.3",
"object.values": "^1.1.0",
"openpgp": "^5.3.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.0",

Wyświetl plik

@ -3423,6 +3423,16 @@ arrify@^1.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
asn1.js@^5.0.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
dependencies:
bn.js "^4.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
ast-types-flow@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
@ -3769,6 +3779,11 @@ blurhash@^1.1.5:
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.5.tgz#3034104cd5dce5a3e5caa871ae2f0f1f2d0ab566"
integrity sha512-a+LO3A2DfxTaTztsmkbLYmUzUeApi0LZuKalwbNmqAHR6HhJGMt1qSV/R3wc+w4DL28holjqO3Bg74aUGavGjg==
bn.js@^4.0.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
body-parser@1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
@ -8739,6 +8754,13 @@ opener@^1.5.2:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
openpgp@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.3.0.tgz#e8fc97e538865b8c095dbd91c7be4203bd1dd1df"
integrity sha512-qjCj0vYpV3dmmkE+vURiJ5kVAJwrMk8BPukvpWJiHcTNWKwPVsRS810plIe4klIcHVf1ScgUQwqtBbv99ff+kQ==
dependencies:
asn1.js "^5.0.0"
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@ -10349,7 +10371,7 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0,
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
"safer-buffer@>= 2.1.2 < 3":
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==