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
|
||||||
const url = utils.getPublicUrlForMxc(event.content.url)
|
if ("url" in event.content) {
|
||||||
assert(url)
|
// Unencrypted
|
||||||
attachments.push({id: "0", filename})
|
const url = utils.getPublicUrlForMxc(event.content.url)
|
||||||
pendingFiles.push({name: filename, url})
|
assert(url)
|
||||||
|
attachments.push({id: "0", filename})
|
||||||
|
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…
Reference in a new issue