m->d support encrypted files
This commit is contained in:
		
							parent
							
								
									dd4e3aa8e0
								
							
						
					
					
						commit
						be2cdd1186
					
				
					 5 changed files with 127 additions and 15 deletions
				
			
		| 
						 | 
					@ -1,6 +1,9 @@
 | 
				
			||||||
// @ts-check
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const assert = require("assert").strict
 | 
					const assert = require("assert").strict
 | 
				
			||||||
 | 
					const crypto = require("crypto")
 | 
				
			||||||
 | 
					const {pipeline} = require("stream")
 | 
				
			||||||
 | 
					const {promisify} = require("util")
 | 
				
			||||||
const Ty = require("../../types")
 | 
					const Ty = require("../../types")
 | 
				
			||||||
const DiscordTypes = require("discord-api-types/v10")
 | 
					const DiscordTypes = require("discord-api-types/v10")
 | 
				
			||||||
const passthrough = require("../../passthrough")
 | 
					const passthrough = require("../../passthrough")
 | 
				
			||||||
| 
						 | 
					@ -14,16 +17,29 @@ const eventToMessage = sync.require("../converters/event-to-message")
 | 
				
			||||||
const api = sync.require("../../matrix/api")
 | 
					const api = sync.require("../../matrix/api")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {pendingFiles?: {name: string, url: string}[]}} message
 | 
					 * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string})[]}} message
 | 
				
			||||||
 * @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}>}
 | 
					 * @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}>}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async function resolvePendingFiles(message) {
 | 
					async function resolvePendingFiles(message) {
 | 
				
			||||||
	if (!message.pendingFiles) return message
 | 
						if (!message.pendingFiles) return message
 | 
				
			||||||
	const files = await Promise.all(message.pendingFiles.map(async p => {
 | 
						const files = await Promise.all(message.pendingFiles.map(async p => {
 | 
				
			||||||
		const file = await fetch(p.url).then(res => res.arrayBuffer()).then(x => Buffer.from(x))
 | 
							let fileBuffer
 | 
				
			||||||
 | 
							if ("key" in p) {
 | 
				
			||||||
 | 
								// Encrypted
 | 
				
			||||||
 | 
								const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url"))
 | 
				
			||||||
 | 
								fileBuffer = await fetch(p.url).then(res => res.arrayBuffer()).then(x => {
 | 
				
			||||||
 | 
									return Buffer.concat([
 | 
				
			||||||
 | 
										d.update(Buffer.from(x)),
 | 
				
			||||||
 | 
										d.final()
 | 
				
			||||||
 | 
									])
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Unencrypted
 | 
				
			||||||
 | 
								fileBuffer = await fetch(p.url).then(res => res.arrayBuffer()).then(x => Buffer.from(x))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			name: p.name,
 | 
								name: p.name,
 | 
				
			||||||
			file
 | 
								file: fileBuffer // TODO: Once SnowTransfer supports ReadableStreams for attachment uploads, pass in those instead of Buffers
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}))
 | 
						}))
 | 
				
			||||||
	const newMessage = {
 | 
						const newMessage = {
 | 
				
			||||||
| 
						 | 
					@ -34,7 +50,7 @@ async function resolvePendingFiles(message) {
 | 
				
			||||||
	return newMessage
 | 
						return newMessage
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File | Ty.Event.M_Outer_M_Sticker} event */
 | 
					/** @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker} event */
 | 
				
			||||||
async function sendEvent(event) {
 | 
					async function sendEvent(event) {
 | 
				
			||||||
	// TODO: we just assume the bridge has already been created, is that really ok?
 | 
						// TODO: we just assume the bridge has already been created, is that really ok?
 | 
				
			||||||
	const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id)
 | 
						const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -125,7 +125,7 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File | Ty.Event.M_Outer_M_Sticker} event
 | 
					 * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event
 | 
				
			||||||
 * @param {import("discord-api-types/v10").APIGuild} guild
 | 
					 * @param {import("discord-api-types/v10").APIGuild} guild
 | 
				
			||||||
 * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API
 | 
					 * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
| 
						 | 
					@ -148,7 +148,7 @@ async function eventToMessage(event, guild, di) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let content = event.content.body // ultimate fallback
 | 
						let content = event.content.body // ultimate fallback
 | 
				
			||||||
	const attachments = []
 | 
						const attachments = []
 | 
				
			||||||
	/** @type {{name: string, url: string}[]} */
 | 
						/** @type {({name: string, url: string} | {name: string, url: string, key: string, iv: string})[]} */
 | 
				
			||||||
	const pendingFiles = []
 | 
						const pendingFiles = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Convert content depending on what the message is
 | 
						// Convert content depending on what the message is
 | 
				
			||||||
| 
						 | 
					@ -281,10 +281,20 @@ async function eventToMessage(event, guild, di) {
 | 
				
			||||||
	} else if (event.type === "m.room.message" && (event.content.msgtype === "m.file" || event.content.msgtype === "m.video" || event.content.msgtype === "m.audio" || event.content.msgtype === "m.image")) {
 | 
						} else if (event.type === "m.room.message" && (event.content.msgtype === "m.file" || event.content.msgtype === "m.video" || event.content.msgtype === "m.audio" || event.content.msgtype === "m.image")) {
 | 
				
			||||||
		content = ""
 | 
							content = ""
 | 
				
			||||||
		const filename = event.content.body
 | 
							const filename = event.content.body
 | 
				
			||||||
 | 
							if ("url" in event.content) {
 | 
				
			||||||
 | 
								// Unencrypted
 | 
				
			||||||
			const url = utils.getPublicUrlForMxc(event.content.url)
 | 
								const url = utils.getPublicUrlForMxc(event.content.url)
 | 
				
			||||||
			assert(url)
 | 
								assert(url)
 | 
				
			||||||
			attachments.push({id: "0", filename})
 | 
								attachments.push({id: "0", filename})
 | 
				
			||||||
			pendingFiles.push({name: filename, url})
 | 
								pendingFiles.push({name: filename, url})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Encrypted
 | 
				
			||||||
 | 
								const url = utils.getPublicUrlForMxc(event.content.file.url)
 | 
				
			||||||
 | 
								assert(url)
 | 
				
			||||||
 | 
								assert.equal(event.content.file.key.alg, "A256CTR")
 | 
				
			||||||
 | 
								attachments.push({id: "0", filename})
 | 
				
			||||||
 | 
								pendingFiles.push({name: filename, url, key: event.content.file.key.k, iv: event.content.file.iv})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	} else if (event.type === "m.sticker") {
 | 
						} else if (event.type === "m.sticker") {
 | 
				
			||||||
		content = ""
 | 
							content = ""
 | 
				
			||||||
		let filename = event.content.body
 | 
							let filename = event.content.body
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1176,6 +1176,60 @@ test("event2message: image attachments work", async t => {
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("event2message: encrypted image attachments work", async t => {
 | 
				
			||||||
 | 
						t.deepEqual(
 | 
				
			||||||
 | 
							await eventToMessage({
 | 
				
			||||||
 | 
								type: "m.room.message",
 | 
				
			||||||
 | 
								sender: "@cadence:cadence.moe",
 | 
				
			||||||
 | 
								content: {
 | 
				
			||||||
 | 
									info: {
 | 
				
			||||||
 | 
										mimetype: "image/png",
 | 
				
			||||||
 | 
										size: 105691,
 | 
				
			||||||
 | 
										w: 1192,
 | 
				
			||||||
 | 
										h: 309,
 | 
				
			||||||
 | 
										"xyz.amorgan.blurhash": "U17USN~q9FtQ-;Rjxuj[9FIUoMM|-=WB9Ft7"
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									msgtype: "m.image",
 | 
				
			||||||
 | 
									body: "image.png",
 | 
				
			||||||
 | 
									file: {
 | 
				
			||||||
 | 
										v: "v2",
 | 
				
			||||||
 | 
										key: {
 | 
				
			||||||
 | 
											alg: "A256CTR",
 | 
				
			||||||
 | 
											ext: true,
 | 
				
			||||||
 | 
											k: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo",
 | 
				
			||||||
 | 
											key_ops: ["encrypt", "decrypt"],
 | 
				
			||||||
 | 
											kty: "oct"
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										iv: "Va9SHZpIn5kAAAAAAAAAAA",
 | 
				
			||||||
 | 
										hashes: {
 | 
				
			||||||
 | 
											sha256: "OUZqZFBcANFt42iAKET9YXfWMCdT0BX7QO0Eyk9q4Js"
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										url: "mxc://heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX",
 | 
				
			||||||
 | 
										mimetype: "image/png"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								event_id: "$JNhONhXO-5jrztZz8b7mbTMJasbU78TwQr4tog-3Mnk",
 | 
				
			||||||
 | 
								room_id: "!PnyBKvUBOhjuCucEfk:cadence.moe"
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								messagesToDelete: [],
 | 
				
			||||||
 | 
								messagesToEdit: [],
 | 
				
			||||||
 | 
								messagesToSend: [{
 | 
				
			||||||
 | 
									username: "cadence [they]",
 | 
				
			||||||
 | 
									content: "",
 | 
				
			||||||
 | 
									avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
 | 
				
			||||||
 | 
									attachments: [{id: "0", filename: "image.png"}],
 | 
				
			||||||
 | 
									pendingFiles: [{
 | 
				
			||||||
 | 
										name: "image.png",
 | 
				
			||||||
 | 
										url: "https://matrix.cadence.moe/_matrix/media/r0/download/heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX",
 | 
				
			||||||
 | 
										key: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo",
 | 
				
			||||||
 | 
										iv: "Va9SHZpIn5kAAAAAAAAAAA"
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("event2message: stickers work", async t => {
 | 
					test("event2message: stickers work", async t => {
 | 
				
			||||||
	t.deepEqual(
 | 
						t.deepEqual(
 | 
				
			||||||
		await eventToMessage({
 | 
							await eventToMessage({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,7 @@ function guard(type, fn) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
 | 
					sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for
 | 
					 * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async event => {
 | 
					async event => {
 | 
				
			||||||
	if (utils.eventSenderIsFromDiscord(event.sender)) return
 | 
						if (utils.eventSenderIsFromDiscord(event.sender)) return
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ async event => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
 | 
					sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {Ty.Event.M_Outer_M_Sticker} event it is a m.sticker because that's what this listener is filtering for
 | 
					 * @param {Ty.Event.Outer_M_Sticker} event it is a m.sticker because that's what this listener is filtering for
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async event => {
 | 
					async event => {
 | 
				
			||||||
	if (utils.eventSenderIsFromDiscord(event.sender)) return
 | 
						if (utils.eventSenderIsFromDiscord(event.sender)) return
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -80,7 +80,7 @@ export namespace Event {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export type M_Outer_M_Room_Message = Outer<M_Room_Message> & {type: "m.room.message"}
 | 
						export type Outer_M_Room_Message = Outer<M_Room_Message> & {type: "m.room.message"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export type M_Room_Message_File = {
 | 
						export type M_Room_Message_File = {
 | 
				
			||||||
		msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
 | 
							msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
 | 
				
			||||||
| 
						 | 
					@ -96,7 +96,39 @@ export namespace Event {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export type M_Outer_M_Room_Message_File = Outer<M_Room_Message_File> & {type: "m.room.message"}
 | 
						export type Outer_M_Room_Message_File = Outer<M_Room_Message_File> & {type: "m.room.message"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export type M_Room_Message_Encrypted_File = {
 | 
				
			||||||
 | 
							msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
 | 
				
			||||||
 | 
							body: string
 | 
				
			||||||
 | 
							file: {
 | 
				
			||||||
 | 
								url: string
 | 
				
			||||||
 | 
								iv: string
 | 
				
			||||||
 | 
								hashes: {
 | 
				
			||||||
 | 
									sha256: string
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								v: "v2"
 | 
				
			||||||
 | 
								key: {
 | 
				
			||||||
 | 
									/** :3 */
 | 
				
			||||||
 | 
									kty: "oct"
 | 
				
			||||||
 | 
									/** must include at least "encrypt" and "decrypt" */
 | 
				
			||||||
 | 
									key_ops: string[]
 | 
				
			||||||
 | 
									alg: "A256CTR"
 | 
				
			||||||
 | 
									k: string
 | 
				
			||||||
 | 
									ext: true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							info?: any
 | 
				
			||||||
 | 
							"m.relates_to"?: {
 | 
				
			||||||
 | 
								"m.in_reply_to": {
 | 
				
			||||||
 | 
									event_id: string
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rel_type?: "m.replace"
 | 
				
			||||||
 | 
								event_id?: string
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export type Outer_M_Room_Message_Encrypted_File = Outer<M_Room_Message_Encrypted_File> & {type: "m.room.message"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export type M_Sticker = {
 | 
						export type M_Sticker = {
 | 
				
			||||||
		body: string
 | 
							body: string
 | 
				
			||||||
| 
						 | 
					@ -111,7 +143,7 @@ export namespace Event {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export type M_Outer_M_Sticker = Outer<M_Sticker> & {type: "m.sticker"}
 | 
						export type Outer_M_Sticker = Outer<M_Sticker> & {type: "m.sticker"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export type M_Room_Member = {
 | 
						export type M_Room_Member = {
 | 
				
			||||||
		membership: string
 | 
							membership: string
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue