From 7c799f1faa2e99df3ea14663dc0173021d5eb44a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 10 Jun 2020 15:26:14 +0300 Subject: [PATCH] Encrypt media from WhatsApp when sending to encrypted portal --- config/bridge.go | 2 + example-config.yaml | 4 ++ go.mod | 2 +- go.sum | 2 + portal.go | 89 ++++++++++++++++++++++++++++++++++----------- 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index 1d30dbb..b18e118 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -63,6 +63,8 @@ type BridgeConfig struct { InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` + WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"` + AllowUserInvite bool `yaml:"allow_user_invite"` CommandPrefix string `yaml:"command_prefix"` diff --git a/example-config.yaml b/example-config.yaml index 8b5ca80..3953e85 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -142,6 +142,10 @@ bridge: # but causes room avatar/name bugs. private_chat_portal_meta: false + # Whether or not thumbnails from WhatsApp should be sent. + # They're disabled by default due to very low resolution. + whatsapp_thumbnail: false + # Allow invite permission for user. User can invite any bots to room with whatsapp # users (private chat and groups) allow_user_invite: false diff --git a/go.mod b/go.mod index 301de6d..a902f09 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.1.1 - maunium.net/go/mautrix v0.5.0-rc.2 + maunium.net/go/mautrix v0.5.0-rc.3 ) replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8 diff --git a/go.sum b/go.sum index aa1b9ae..fa7586d 100644 --- a/go.sum +++ b/go.sum @@ -67,3 +67,5 @@ maunium.net/go/mautrix v0.4.11 h1:cONVoAkD7AOvtzEMvuuq79Y+2vRrNKfpKZoR8HdyPAw= maunium.net/go/mautrix v0.4.11/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= maunium.net/go/mautrix v0.5.0-rc.2 h1:ohx+dprvMS6Txm+suMx5pbjl0rjDpfftFxgXhx/+Usc= maunium.net/go/mautrix v0.5.0-rc.2/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= +maunium.net/go/mautrix v0.5.0-rc.3 h1:ltb6mF6pck1YzEkuC13V4UtSGDIxaq+XqzIdSg7vgMI= +maunium.net/go/mautrix v0.5.0-rc.3/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= diff --git a/portal.go b/portal.go index 34b79a6..ac45a76 100644 --- a/portal.go +++ b/portal.go @@ -38,6 +38,8 @@ import ( "github.com/pkg/errors" log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/crypto/attachment" + "github.com/Rhymen/go-whatsapp" waProto "github.com/Rhymen/go-whatsapp/binary/proto" @@ -197,15 +199,15 @@ func (portal *Portal) handleMessage(msg PortalMessage) { case whatsapp.TextMessage: portal.HandleTextMessage(msg.source, data) case whatsapp.ImageMessage: - portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, false) + portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, 0, false) case whatsapp.StickerMessage: - portal.HandleMediaMessage(msg.source, data.Download, nil, data.Info, data.ContextInfo, data.Type, "", true) + portal.HandleMediaMessage(msg.source, data.Download, nil, data.Info, data.ContextInfo, data.Type, "", 0, true) case whatsapp.VideoMessage: - portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, false) + portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, data.Length, false) case whatsapp.AudioMessage: - portal.HandleMediaMessage(msg.source, data.Download, nil, data.Info, data.ContextInfo, data.Type, "", false) + portal.HandleMediaMessage(msg.source, data.Download, nil, data.Info, data.ContextInfo, data.Type, "", data.Length, false) case whatsapp.DocumentMessage: - portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Title, false) + portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Title, 0, false) case whatsapp.ContactMessage: portal.HandleContactMessage(msg.source, data) case whatsapp.LocationMessage: @@ -1154,8 +1156,11 @@ func (portal *Portal) HandleContactMessage(source *User, message whatsapp.Contac } fileName := fmt.Sprintf("%s.vcf", message.DisplayName) + data := []byte(message.Vcard) + mimeType := "text/vcard" + data, uploadMimeType, file := portal.encryptFile(data, mimeType) - uploadResp, err := intent.UploadBytesWithName([]byte(message.Vcard), "text/vcard", fileName) + uploadResp, err := intent.UploadBytesWithName(data, uploadMimeType, fileName) if err != nil { portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err) return @@ -1164,12 +1169,17 @@ func (portal *Portal) HandleContactMessage(source *User, message whatsapp.Contac content := &event.MessageEventContent{ Body: fileName, MsgType: event.MsgFile, - URL: uploadResp.ContentURI.CUString(), + File: file, Info: &event.FileInfo{ - MimeType: "text/vcard", + MimeType: mimeType, Size: len(message.Vcard), }, } + if content.File != nil { + content.File.URL = uploadResp.ContentURI.CUString() + } else { + content.URL = uploadResp.ContentURI.CUString() + } portal.SetReply(content, message.ContextInfo) @@ -1195,7 +1205,20 @@ func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.In } } -func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, context whatsapp.ContextInfo, mimeType, caption string, sendAsSticker bool) { +func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) { + if !portal.Encrypted { + return data, mimeType, nil + } + + file := &event.EncryptedFileInfo{ + EncryptedFile: *attachment.NewEncryptedFile(), + URL: "", + } + return file.Encrypt(data), "application/octet-stream", file + +} + +func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, context whatsapp.ContextInfo, mimeType, caption string, length uint32, sendAsSticker bool) { intent := portal.startHandling(source, info) if intent == nil { return @@ -1237,7 +1260,15 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, mimeType = "image/png" } - uploaded, err := intent.UploadBytes(data, mimeType) + var width, height int + if strings.HasPrefix(mimeType, "image/") { + cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) + width, height = cfg.Width, cfg.Height + } + + data, uploadMimeType, file := portal.encryptFile(data, mimeType) + + uploaded, err := intent.UploadBytes(data, uploadMimeType) if err != nil { portal.log.Errorfln("Failed to upload media for %s: %v", err) return @@ -1251,24 +1282,41 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, content := &event.MessageEventContent{ Body: fileName, - URL: uploaded.ContentURI.CUString(), + File: file, Info: &event.FileInfo{ Size: len(data), MimeType: mimeType, + Width: width, + Height: height, + Duration: int(length), }, } + if content.File != nil { + content.File.URL = uploaded.ContentURI.CUString() + } else { + content.URL = uploaded.ContentURI.CUString() + } portal.SetReply(content, context) - if thumbnail != nil { + if thumbnail != nil && portal.bridge.Config.Bridge.WhatsappThumbnail { thumbnailMime := http.DetectContentType(thumbnail) - uploadedThumbnail, _ := intent.UploadBytes(thumbnail, thumbnailMime) - if uploadedThumbnail != nil { - content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString() - cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) + thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnail)) + thumbnailSize := len(thumbnail) + thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(thumbnail, thumbnailMime) + uploadedThumbnail, err := intent.UploadBytes(thumbnail, thumbnailUploadMime) + if err != nil { + portal.log.Warnfln("Failed to upload thumbnail for %s: %v", info.Id, err) + } else if uploadedThumbnail != nil { + if thumbnailFile != nil { + thumbnailFile.URL = uploadedThumbnail.ContentURI.CUString() + content.Info.ThumbnailFile = thumbnailFile + } else { + content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString() + } content.Info.ThumbnailInfo = &event.FileInfo{ - Size: len(thumbnail), - Width: cfg.Width, - Height: cfg.Height, + Size: thumbnailSize, + Width: thumbnailCfg.Width, + Height: thumbnailCfg.Height, MimeType: thumbnailMime, } } @@ -1279,9 +1327,6 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, if !sendAsSticker { content.MsgType = event.MsgImage } - cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) - content.Info.Width = cfg.Width - content.Info.Height = cfg.Height case "video": content.MsgType = event.MsgVideo case "audio":