Skip to content

Commit

Permalink
Merge pull request #10 from mautrix/e2ee
Browse files Browse the repository at this point in the history
Add support for end-to-end encrypted chats over WhatsApp
  • Loading branch information
tulir authored Feb 9, 2024
2 parents d4943fa + 683490b commit 04abd75
Show file tree
Hide file tree
Showing 31 changed files with 2,118 additions and 234 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
indent_size = 2
indent_style = space

[*.{yaml,yml,sql}]
indent_style = space
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.21"]
go-version: ["1.21", "1.22"]
name: Lint ${{ matrix.go-version == '1.22' && '(latest)' || '(old)' }}

steps:
- uses: actions/checkout@v4
Expand Down
74 changes: 73 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* [x] Files
* [x] Voice messages
* [x] Locations
* [ ] Polls
* [ ] Live location sharing
* [x] Story/reel/clip shares
* [x] Profile shares
Expand Down Expand Up @@ -67,10 +68,81 @@
* [x] Name
* [ ] Per-chat nickname
* [x] Avatar
* Matrix → WhatsApp
* [ ] Message content
* [x] Text
* [ ] Media
* [x] Files
* [x] Voice messages
* [x] Videos
* [x] Images
* [x] Stickers
* [x] Gifs
* [ ] Locations
* [ ] Polls
* [ ] Formatting (Messenger only)
* [x] Replies
* [ ] Mentions
* [x] Message redactions
* [x] Message reactions
* [x] Message edits
* [ ] Writing to chat backup
* [ ] Presence
* [ ] Typing notifications
* [x] Read receipts
* [ ] Power level
* [ ] Membership actions
* [ ] Invite
* [ ] Kick
* [ ] Leave
* [ ] Room metadata changes
* [ ] Name
* [ ] Avatar
* [ ] Per-room user nick
* WhatsApp → Matrix
* [ ] Message content
* [x] Text
* [ ] Media
* [x] Images
* [x] Videos
* [x] Gifs
* [x] Stickers
* [x] Files
* [x] Voice messages
* [x] Locations
* [ ] Polls
* [ ] Live location sharing
* [ ] Story/reel/clip shares
* [ ] Profile shares
* [ ] Product shares
* [ ] Formatting (Messenger only)
* [x] Replies
* [x] Mentions
* [ ] Polls
* [x] Message unsend
* [x] Message reactions
* [x] Message edits
* [ ] Message history/Reading chat backup
* [ ] Presence
* [ ] Typing notifications
* [x] Read receipts
* [ ] Admin status
* [ ] Membership actions
* [ ] Add member
* [ ] Remove member
* [ ] Leave
* [ ] Chat metadata changes
* [ ] Title
* [ ] Avatar
* [ ] Initial chat metadata
* [ ] User metadata
* [ ] Name
* [ ] Per-chat nickname
* [ ] Avatar
* Misc
* [x] Multi-user support
* [x] Shared group chat portals
* [ ] Messenger encryption
* [x] Messenger encryption
* [x] Matrix encryption
* [x] Automatic portal creation
* [x] At startup
Expand Down
33 changes: 33 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (br *MetaBridge) RegisterCommands() {
cmdLogin,
cmdSyncSpace,
cmdDeleteSession,
cmdToggleEncryption,
cmdSetRelay,
cmdUnsetRelay,
cmdDeletePortal,
Expand All @@ -77,6 +78,38 @@ func wrapCommand(handler func(*WrappedCommandEvent)) func(*commands.Event) {
}
}

var cmdToggleEncryption = &commands.FullHandler{
Func: wrapCommand(fnToggleEncryption),
Name: "toggle-encryption",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Toggle Messenger-side encryption for the current room",
},
RequiresPortal: true,
RequiresLogin: true,
}

func fnToggleEncryption(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Meta.Mode.IsMessenger() {
ce.Reply("Encryption support is not yet enabled in Instagram mode")
return
} else if !ce.Portal.IsPrivateChat() {
ce.Reply("Only private chats can be toggled between encrypted and unencrypted")
return
}
if ce.Portal.ThreadType.IsWhatsApp() {
ce.Portal.ThreadType = table.ONE_TO_ONE
ce.Reply("Messages in this room will now be sent unencrypted over Messenger")
} else {
ce.Portal.ThreadType = table.ENCRYPTED_OVER_WA_ONE_TO_ONE
ce.Reply("Messages in this room will now be sent encrypted over WhatsApp")
}
err := ce.Portal.Update(ce.Ctx)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to update portal in database")
}
}

var cmdSetRelay = &commands.FullHandler{
Func: wrapCommand(fnSetRelay),
Name: "set-relay",
Expand Down
21 changes: 21 additions & 0 deletions database/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const (
SELECT id, part_index, thread_id, thread_receiver, msg_sender, otid, mxid, mx_room, timestamp, edit_count FROM message
WHERE id=$1 AND thread_receiver=$2
`
getMessagesBetweenTimeQuery = `
SELECT id, part_index, thread_id, thread_receiver, msg_sender, otid, mxid, mx_room, timestamp, edit_count FROM message
WHERE thread_id=$1 AND thread_receiver=$2 AND timestamp>$3 AND timestamp<=$4 AND part_index=0
ORDER BY timestamp ASC
`
findEditTargetPortalFromMessageQuery = `
SELECT thread_id, thread_receiver FROM message
WHERE id=$1 AND (thread_receiver=$2 OR thread_receiver=0) AND part_index=0
Expand Down Expand Up @@ -121,6 +126,10 @@ func (mq *MessageQuery) GetAllPartsByID(ctx context.Context, id string, receiver
return mq.QueryMany(ctx, getAllMessagePartsByIDQuery, id, receiver)
}

func (mq *MessageQuery) GetAllBetweenTimestamps(ctx context.Context, key PortalKey, min, max time.Time) ([]*Message, error) {
return mq.QueryMany(ctx, getMessagesBetweenTimeQuery, key.ThreadID, key.Receiver, min.UnixMilli(), max.UnixMilli())
}

func (mq *MessageQuery) FindEditTargetPortal(ctx context.Context, id string, receiver int64) (key PortalKey, err error) {
err = mq.GetDB().QueryRow(ctx, findEditTargetPortalFromMessageQuery, id, receiver).Scan(&key.ThreadID, &key.Receiver)
if errors.Is(err, sql.ErrNoRows) {
Expand Down Expand Up @@ -211,3 +220,15 @@ func (msg *Message) UpdateEditCount(ctx context.Context, count int64) error {
msg.EditCount = count
return msg.qh.Exec(ctx, updateMessageEditCountQuery, msg.ID, msg.ThreadReceiver, msg.PartIndex, msg.EditCount)
}

func (msg *Message) EditTimestamp() int64 {
return msg.EditCount
}

func (msg *Message) UpdateEditTimestamp(ctx context.Context, ts int64) error {
return msg.UpdateEditCount(ctx, ts)
}

func (msg *Message) IsUnencrypted() bool {
return strings.HasPrefix(msg.ID, "mid.$")
}
53 changes: 40 additions & 13 deletions database/portal.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ package database
import (
"context"
"database/sql"
"strconv"

"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/id"

"go.mau.fi/mautrix-meta/messagix/table"
Expand All @@ -30,7 +32,8 @@ const (
portalBaseSelect = `
SELECT thread_id, receiver, thread_type, mxid,
name, avatar_id, avatar_url, name_set, avatar_set,
encrypted, relay_user_id, oldest_message_id, oldest_message_ts, more_to_backfill
whatsapp_server, encrypted, relay_user_id,
oldest_message_id, oldest_message_ts, more_to_backfill
FROM portal
`
getPortalByMXIDQuery = portalBaseSelect + `WHERE mxid=$1`
Expand All @@ -47,14 +50,16 @@ const (
INSERT INTO portal (
thread_id, receiver, thread_type, mxid,
name, avatar_id, avatar_url, name_set, avatar_set,
encrypted, relay_user_id, oldest_message_id, oldest_message_ts, more_to_backfill
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
whatsapp_server, encrypted, relay_user_id,
oldest_message_id, oldest_message_ts, more_to_backfill
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`
updatePortalQuery = `
UPDATE portal SET
thread_type=$3, mxid=$4,
name=$5, avatar_id=$6, avatar_url=$7, name_set=$8, avatar_set=$9,
encrypted=$10, relay_user_id=$11, oldest_message_id=$12, oldest_message_ts=$13, more_to_backfill=$14
whatsapp_server=$10, encrypted=$11, relay_user_id=$12,
oldest_message_id=$13, oldest_message_ts=$14, more_to_backfill=$15
WHERE thread_id=$1 AND receiver=$2
`
deletePortalQuery = `DELETE FROM portal WHERE thread_id=$1 AND receiver=$2`
Expand All @@ -73,15 +78,17 @@ type Portal struct {
qh *dbutil.QueryHelper[*Portal]

PortalKey
ThreadType table.ThreadType
MXID id.RoomID
Name string
AvatarID string
AvatarURL id.ContentURI
NameSet bool
AvatarSet bool
Encrypted bool
RelayUserID id.UserID
ThreadType table.ThreadType
MXID id.RoomID
Name string
AvatarID string
AvatarURL id.ContentURI
NameSet bool
AvatarSet bool

WhatsAppServer string
Encrypted bool
RelayUserID id.UserID

OldestMessageID string
OldestMessageTS int64
Expand Down Expand Up @@ -128,6 +135,24 @@ func (p *Portal) IsPrivateChat() bool {
return p.ThreadType.IsOneToOne()
}

func (p *Portal) JID() types.JID {
jid := types.JID{
User: strconv.FormatInt(p.ThreadID, 10),
Server: p.WhatsAppServer,
}
if jid.Server == "" {
switch p.ThreadType {
case table.ENCRYPTED_OVER_WA_GROUP:
jid.Server = types.GroupServer
//case table.ENCRYPTED_OVER_WA_ONE_TO_ONE:
// jid.Server = types.DefaultUserServer
default:
jid.Server = types.MessengerServer
}
}
return jid
}

func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
var mxid sql.NullString
err := row.Scan(
Expand All @@ -140,6 +165,7 @@ func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
&p.AvatarURL,
&p.NameSet,
&p.AvatarSet,
&p.WhatsAppServer,
&p.Encrypted,
&p.RelayUserID,
&p.OldestMessageID,
Expand All @@ -164,6 +190,7 @@ func (p *Portal) sqlVariables() []any {
&p.AvatarURL,
p.NameSet,
p.AvatarSet,
p.WhatsAppServer,
p.Encrypted,
p.RelayUserID,
p.OldestMessageID,
Expand Down
23 changes: 19 additions & 4 deletions database/puppet.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ import (
"database/sql"

"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/id"
)

const (
puppetBaseSelect = `
SELECT id, name, username, avatar_id, avatar_url, name_set, avatar_set,
contact_info_set, custom_mxid, access_token
contact_info_set, whatsapp_server, custom_mxid, access_token
FROM puppet
`
getPuppetByMetaIDQuery = puppetBaseSelect + `WHERE id=$1`
Expand All @@ -36,15 +37,15 @@ const (
updatePuppetQuery = `
UPDATE puppet SET
name=$2, username=$3, avatar_id=$4, avatar_url=$5, name_set=$6, avatar_set=$7,
contact_info_set=$8, custom_mxid=$9, access_token=$10
contact_info_set=$8, whatsapp_server=$9, custom_mxid=$10, access_token=$11
WHERE id=$1
`
insertPuppetQuery = `
INSERT INTO puppet (
id, name, username, avatar_id, avatar_url, name_set, avatar_set,
contact_info_set, custom_mxid, access_token
contact_info_set, whatsapp_server, custom_mxid, access_token
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
)

Expand All @@ -64,6 +65,7 @@ type Puppet struct {
AvatarSet bool

ContactInfoSet bool
WhatsAppServer string

CustomMXID id.UserID
AccessToken string
Expand Down Expand Up @@ -96,6 +98,7 @@ func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) {
&p.NameSet,
&p.AvatarSet,
&p.ContactInfoSet,
&p.WhatsAppServer,
&customMXID,
&p.AccessToken,
)
Expand All @@ -106,6 +109,17 @@ func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) {
return p, nil
}

func (p *Puppet) JID() types.JID {
jid := types.JID{
User: p.Username,
Server: p.WhatsAppServer,
}
if jid.Server == "" {
jid.Server = types.MessengerServer
}
return jid
}

func (p *Puppet) sqlVariables() []any {
return []any{
p.ID,
Expand All @@ -116,6 +130,7 @@ func (p *Puppet) sqlVariables() []any {
p.NameSet,
p.AvatarSet,
p.ContactInfoSet,
p.WhatsAppServer,
dbutil.StrPtr(p.CustomMXID),
p.AccessToken,
}
Expand Down
Loading

0 comments on commit 04abd75

Please sign in to comment.