Encrypt media from WhatsApp when sending to encrypted portal

This commit is contained in:
Tulir Asokan 2020-06-10 15:26:14 +03:00
parent 210b1caf65
commit 7c799f1faa
5 changed files with 76 additions and 23 deletions

View file

@ -63,6 +63,8 @@ type BridgeConfig struct {
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
AllowUserInvite bool `yaml:"allow_user_invite"` AllowUserInvite bool `yaml:"allow_user_invite"`
CommandPrefix string `yaml:"command_prefix"` CommandPrefix string `yaml:"command_prefix"`

View file

@ -142,6 +142,10 @@ bridge:
# but causes room avatar/name bugs. # but causes room avatar/name bugs.
private_chat_portal_meta: false 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 # Allow invite permission for user. User can invite any bots to room with whatsapp
# users (private chat and groups) # users (private chat and groups)
allow_user_invite: false allow_user_invite: false

2
go.mod
View file

@ -15,7 +15,7 @@ require (
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0
maunium.net/go/mauflag v1.0.0 maunium.net/go/mauflag v1.0.0
maunium.net/go/maulogger/v2 v2.1.1 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 replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8

2
go.sum
View file

@ -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.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 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.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=

View file

@ -38,6 +38,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/crypto/attachment"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
waProto "github.com/Rhymen/go-whatsapp/binary/proto" waProto "github.com/Rhymen/go-whatsapp/binary/proto"
@ -197,15 +199,15 @@ func (portal *Portal) handleMessage(msg PortalMessage) {
case whatsapp.TextMessage: case whatsapp.TextMessage:
portal.HandleTextMessage(msg.source, data) portal.HandleTextMessage(msg.source, data)
case whatsapp.ImageMessage: 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: 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: 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: 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: 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: case whatsapp.ContactMessage:
portal.HandleContactMessage(msg.source, data) portal.HandleContactMessage(msg.source, data)
case whatsapp.LocationMessage: case whatsapp.LocationMessage:
@ -1154,8 +1156,11 @@ func (portal *Portal) HandleContactMessage(source *User, message whatsapp.Contac
} }
fileName := fmt.Sprintf("%s.vcf", message.DisplayName) 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 { if err != nil {
portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err) portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err)
return return
@ -1164,12 +1169,17 @@ func (portal *Portal) HandleContactMessage(source *User, message whatsapp.Contac
content := &event.MessageEventContent{ content := &event.MessageEventContent{
Body: fileName, Body: fileName,
MsgType: event.MsgFile, MsgType: event.MsgFile,
URL: uploadResp.ContentURI.CUString(), File: file,
Info: &event.FileInfo{ Info: &event.FileInfo{
MimeType: "text/vcard", MimeType: mimeType,
Size: len(message.Vcard), 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) 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) intent := portal.startHandling(source, info)
if intent == nil { if intent == nil {
return return
@ -1237,7 +1260,15 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
mimeType = "image/png" 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 { if err != nil {
portal.log.Errorfln("Failed to upload media for %s: %v", err) portal.log.Errorfln("Failed to upload media for %s: %v", err)
return return
@ -1251,24 +1282,41 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
content := &event.MessageEventContent{ content := &event.MessageEventContent{
Body: fileName, Body: fileName,
URL: uploaded.ContentURI.CUString(), File: file,
Info: &event.FileInfo{ Info: &event.FileInfo{
Size: len(data), Size: len(data),
MimeType: mimeType, 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) portal.SetReply(content, context)
if thumbnail != nil { if thumbnail != nil && portal.bridge.Config.Bridge.WhatsappThumbnail {
thumbnailMime := http.DetectContentType(thumbnail) thumbnailMime := http.DetectContentType(thumbnail)
uploadedThumbnail, _ := intent.UploadBytes(thumbnail, thumbnailMime) thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnail))
if uploadedThumbnail != nil { thumbnailSize := len(thumbnail)
content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString() thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(thumbnail, thumbnailMime)
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) 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{ content.Info.ThumbnailInfo = &event.FileInfo{
Size: len(thumbnail), Size: thumbnailSize,
Width: cfg.Width, Width: thumbnailCfg.Width,
Height: cfg.Height, Height: thumbnailCfg.Height,
MimeType: thumbnailMime, MimeType: thumbnailMime,
} }
} }
@ -1279,9 +1327,6 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
if !sendAsSticker { if !sendAsSticker {
content.MsgType = event.MsgImage content.MsgType = event.MsgImage
} }
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
content.Info.Width = cfg.Width
content.Info.Height = cfg.Height
case "video": case "video":
content.MsgType = event.MsgVideo content.MsgType = event.MsgVideo
case "audio": case "audio":