commit 919bb2e111e97b693ac566c7ad1afb0c280ccf38
parent a686f497a0fd8fb13d5f4876a8c2da2e5058633f
Author: MTRNord <mtrnord1@gmail.com>
Date: Sat, 6 May 2023 16:32:07 +0200
Decrypt images
Diffstat:
4 files changed, 132 insertions(+), 12 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -23,6 +23,7 @@
"linkify-react": "^4.1.1",
"linkifyjs": "^4.1.1",
"lucide-react": "^0.171.0",
+ "matrix-encrypt-attachment": "^1.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intersection-observer": "^9.4.3",
@@ -10975,6 +10976,14 @@
"react": ">= 0.14.0"
}
},
+ "node_modules/matrix-encrypt-attachment": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/matrix-encrypt-attachment/-/matrix-encrypt-attachment-1.0.3.tgz",
+ "integrity": "sha512-NwfoDY/yHL9Zo8KrY5GP8ymAoZJpEFKEK+IgJKdWf5xtsIFf7KIU2pbw8+Eq592j//nSDOC+/Ff4Fk8gVvDZpw==",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
"node_modules/mdast-util-definitions": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz",
diff --git a/package.json b/package.json
@@ -19,6 +19,7 @@
"linkify-react": "^4.1.1",
"linkifyjs": "^4.1.1",
"lucide-react": "^0.171.0",
+ "matrix-encrypt-attachment": "^1.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intersection-observer": "^9.4.3",
diff --git a/src/components/events/messageEvent.tsx b/src/components/events/messageEvent.tsx
@@ -6,6 +6,7 @@ import { MatrixContext, useRoom } from "../../app/sdk/client";
import Linkify from "linkify-react";
import DOMPurify from "dompurify";
import { UndecryptableEvent } from "./unknownEvent";
+import { decryptAttachment } from "matrix-encrypt-attachment";
type MessageEventProps = {
/**
@@ -35,7 +36,6 @@ const linkifyOptions = {
const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEvent }) => {
const room = useRoom(roomID);
- const client = useContext(MatrixContext);
const renderCorrectMessage = (event: IRoomEvent) => {
if (isRoomMessageTextEvent(event)) {
@@ -116,24 +116,66 @@ const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEv
)
}
} else if (isRoomMessageImageEvent(event)) {
+ const client = useContext(MatrixContext);
const [url, setUrl] = useState<string | undefined>(undefined);
const [unableToDecrypt, setUnableToDecrypt] = useState<boolean>(event.content.file !== undefined);
- useEffect(() => {
- if (event.content.url) {
- setUrl(client.convertMXC(event.content.url));
- } else {
- // Image is encrypted and we need to download and decrypt it
+ const decryptImage = (event: IRoomEvent) => {
+ console.log("Downloading image:", event.event_id);
+ fetch(client.convertMXC(event.content.file.url), {
+ headers: {
+ Authorization: `Bearer ${client.accessToken}`
+ }
+ }).then((response) => {
+ if (!response.ok) {
+ // TODO: display error?
+ console.log("Unable to decrypt image:", response.text());
+ return;
+ }
+ console.log("Downloaded image:", event.event_id);
+ response.arrayBuffer().then((responseData) => {
+ // Decrypt the array buffer using the information taken from the event content.
+ decryptAttachment(responseData, event.content.file).then((dataArray) => {
+ // Turn the array into a Blob and give it the correct MIME-type.
+
+ // IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
+ // they introduce XSS attacks if the Blob URI is viewed directly in the
+ // browser (e.g. by copying the URI into a new tab or window.)
+ // See warning at top of file.
+ let mimetype = event.content.info?.mimetype ? event.content.info.mimetype.split(";")[0].trim() : "";
+ mimetype = getBlobSafeMimeType(mimetype);
+
+ const blob = new Blob([dataArray], { type: mimetype });
+ setUrl(URL.createObjectURL(blob));
+ setUnableToDecrypt(false);
+ console.log("Decrypted image:", event.event_id);
+ }).catch((e: any) => {
+ console.log("Unable to decrypt image due to decryption error:", e);
+ setUnableToDecrypt(true);
+ });
+ });
+ });
+ }
+ useEffect(() => {
+ if (isRoomMessageImageEvent(event)) {
+ if (event.content.url) {
+ setUrl(client.convertMXC(event.content.url));
+ } else {
+ // Image is encrypted and we need to download and decrypt it
+ if (event.content.file) {
+ decryptImage(event);
+ }
+ }
}
- }, [event.content.url])
+ }, [event])
if (unableToDecrypt) {
return (<UndecryptableEvent event={event} roomID={roomID} hasPreviousEvent={hasPreviousEvent} />)
}
return (
- <div className={!hasPreviousEvent ? "flex flex-row gap-4 p-2 pb-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out items-start" : "flex flex-row p-2 pb-1 pt-0 hover:bg-gray-200 rounded-md duration-200 ease-in-out"}>
+ <div className={!hasPreviousEvent ? "flex flex-row gap-4 p-2 pb-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out items-start" : "flex flex-row p-2 pb-1 pt-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out"}>
{!hasPreviousEvent && <Avatar
displayname={room?.getMemberName(event.sender) || ""}
avatarUrl={room?.getMemberAvatar(event.sender)}
@@ -173,4 +215,72 @@ const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEv
return renderCorrectMessage(event);
});
-export default MessageEvent;
-\ No newline at end of file
+export default MessageEvent;
+
+
+// WARNING: We have to be very careful about what mime-types we allow into blobs,
+// as for performance reasons these are now rendered via URL.createObjectURL()
+// rather than by converting into data: URIs.
+//
+// This means that the content is rendered using the origin of the script which
+// called createObjectURL(), and so if the content contains any scripting then it
+// will pose a XSS vulnerability when the browser renders it. This is particularly
+// bad if the user right-clicks the URI and pastes it into a new window or tab,
+// as the blob will then execute with access to Element's full JS environment(!)
+//
+// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
+// for details.
+//
+// We mitigate this by only allowing mime-types into blobs which we know don't
+// contain any scripting, and instantiate all others as application/octet-stream
+// regardless of what mime-type the event claimed. Even if the payload itself
+// is some malicious HTML, the fact we instantiate it with a media mimetype or
+// application/octet-stream means the browser doesn't try to render it as such.
+//
+// One interesting edge case is image/svg+xml, which empirically *is* rendered
+// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
+// *even if the mimetype is application/octet-stream*. However, empirically JS
+// in the SVG isn't executed in this scenario, so we seem to be okay.
+//
+// Tested on Chrome 65 and Firefox 60
+//
+// The list below is taken mainly from
+// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
+// N.B. Matrix doesn't currently specify which mimetypes are valid in given
+// events, so we pick the ones which HTML5 browsers should be able to display
+//
+// For the record, mime-types which must NEVER enter this list below include:
+// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
+
+const ALLOWED_BLOB_MIMETYPES = [
+ "image/jpeg",
+ "image/gif",
+ "image/png",
+ "image/apng",
+ "image/webp",
+ "image/avif",
+
+ "video/mp4",
+ "video/webm",
+ "video/ogg",
+ "video/quicktime",
+
+ "audio/mp4",
+ "audio/webm",
+ "audio/aac",
+ "audio/mpeg",
+ "audio/ogg",
+ "audio/wave",
+ "audio/wav",
+ "audio/x-wav",
+ "audio/x-pn-wav",
+ "audio/flac",
+ "audio/x-flac",
+];
+
+export function getBlobSafeMimeType(mimetype: string): string {
+ if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
+ return "application/octet-stream";
+ }
+ return mimetype;
+}
+\ No newline at end of file
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
@@ -86,11 +86,11 @@ const ChatView: FC<ChatViewProps> = memo(({ roomID, scrollRef }) => {
event.content.format = event.content["m.new_content"].format;
}
} else {
- return (<UndecryptableEvent event={event} hasPreviousEvent={previousEventIsFromSameSender} roomID={roomID}></UndecryptableEvent>)
+ return (<UndecryptableEvent key={event.event_id} event={event} hasPreviousEvent={previousEventIsFromSameSender} roomID={roomID}></UndecryptableEvent>)
}
} catch (e: any) {
console.error(e);
- return (<UndecryptableEvent event={event} hasPreviousEvent={previousEventIsFromSameSender} roomID={roomID}></UndecryptableEvent>)
+ return (<UndecryptableEvent key={event.event_id} event={event} hasPreviousEvent={previousEventIsFromSameSender} roomID={roomID}></UndecryptableEvent>)
}
}