Skip to content

Commit deecc4d

Browse files
authored
decrypt,send: msmsg decryption and bot message sending support (#615)
1 parent 1295ce2 commit deecc4d

File tree

11 files changed

+476
-33
lines changed

11 files changed

+476
-33
lines changed

errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ var (
106106
ErrUnknownServer = errors.New("can't send message to unknown server")
107107
ErrRecipientADJID = errors.New("message recipient must be a user JID with no device part")
108108
ErrServerReturnedError = errors.New("server returned error")
109+
ErrInvalidInlineBotID = errors.New("invalid inline bot ID")
109110
)
110111

111112
type DownloadHTTPError struct {

mdtest/main.go

+61
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"net/http"
1919
"os"
2020
"os/signal"
21+
"regexp"
2122
"strconv"
2223
"strings"
2324
"sync/atomic"
@@ -1022,6 +1023,66 @@ func handleCmd(cmd string, args []string) {
10221023
if err != nil {
10231024
log.Errorf("Error editing label: %v", err)
10241025
}
1026+
case "sendbotmsg":
1027+
if len(args) < 1 {
1028+
log.Errorf("Usage: sendBotMsg <inline jid (optional)> <text>")
1029+
return
1030+
}
1031+
var inlineJID types.JID
1032+
if len(args) > 1 {
1033+
var numbersRegex = regexp.MustCompile(`^[0-9]+$`)
1034+
jid, ok := parseJID(args[0])
1035+
if ok && numbersRegex.MatchString(jid.User) {
1036+
inlineJID = jid
1037+
} else {
1038+
inlineJID = types.EmptyJID
1039+
}
1040+
}
1041+
1042+
personaID := proto.String("867051314767696$760019659443059") // default meta bot personality: "Assistant"
1043+
1044+
var resp, err = whatsmeow.SendResponse{}, error(nil)
1045+
if !inlineJID.IsEmpty() {
1046+
text := fmt.Sprintf("@%s %s", types.MetaAIJID.User, strings.Join(args[1:], " "))
1047+
msg := &waE2E.Message{
1048+
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
1049+
Text: &text,
1050+
ContextInfo: &waE2E.ContextInfo{
1051+
MentionedJID: []string{types.MetaAIJID.String()},
1052+
},
1053+
},
1054+
MessageContextInfo: &waE2E.MessageContextInfo{
1055+
BotMetadata: &waE2E.BotMetadata{
1056+
PersonaID: personaID,
1057+
},
1058+
},
1059+
}
1060+
1061+
resp, err = cli.SendMessage(context.Background(), inlineJID, msg, whatsmeow.SendRequestExtra{
1062+
InlineBotJID: types.MetaAIJID,
1063+
})
1064+
} else {
1065+
text := strings.Join(args, " ")
1066+
msg := &waE2E.Message{
1067+
Conversation: &text,
1068+
MessageContextInfo: &waE2E.MessageContextInfo{
1069+
BotMetadata: &waE2E.BotMetadata{
1070+
PersonaID: personaID,
1071+
},
1072+
},
1073+
}
1074+
resp, err = cli.SendMessage(context.Background(), types.MetaAIJID, msg)
1075+
}
1076+
if err != nil {
1077+
log.Errorf("Error sending bot message: %v", err)
1078+
} else {
1079+
log.Infof("Bot message sent (server timestamp: %s)", resp.Timestamp)
1080+
}
1081+
case "fetchbotprofiles":
1082+
list, _ := cli.GetBotListV2()
1083+
log.Infof("Bots list: %+v", list)
1084+
profiles, _ := cli.GetBotProfiles(list)
1085+
log.Infof("Bots profiles: %+v", profiles)
10251086
}
10261087
}
10271088

message.go

+86-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"runtime/debug"
1717
"time"
1818

19+
"go.mau.fi/whatsmeow/proto/waE2E"
20+
1921
"go.mau.fi/libsignal/groups"
2022
"go.mau.fi/libsignal/protocol"
2123
"go.mau.fi/libsignal/session"
@@ -88,6 +90,16 @@ func (cli *Client) parseMessageSource(node *waBinary.Node, requireParticipant bo
8890
} else {
8991
source.Chat = from.ToNonAD()
9092
}
93+
} else if from.IsBot() {
94+
source.Sender = from
95+
meta := node.GetChildByTag("meta")
96+
ag = meta.AttrGetter()
97+
targetChatJID := ag.OptionalJID("target_chat_jid")
98+
if targetChatJID != nil {
99+
source.Chat = targetChatJID.ToNonAD()
100+
} else {
101+
source.Chat = from
102+
}
91103
} else {
92104
source.Chat = from.ToNonAD()
93105
source.Sender = from
@@ -96,6 +108,32 @@ func (cli *Client) parseMessageSource(node *waBinary.Node, requireParticipant bo
96108
return
97109
}
98110

111+
func (cli *Client) parseMsgBotInfo(node waBinary.Node) (botInfo types.MsgBotInfo, err error) {
112+
botNode := node.GetChildByTag("bot")
113+
114+
ag := botNode.AttrGetter()
115+
botInfo.EditType = types.BotEditType(ag.String("edit"))
116+
if botInfo.EditType == types.EditTypeInner || botInfo.EditType == types.EditTypeLast {
117+
botInfo.EditTargetID = types.MessageID(ag.String("edit_target_id"))
118+
botInfo.EditSenderTimestampMS = ag.UnixMilli("sender_timestamp_ms")
119+
}
120+
err = ag.Error()
121+
return
122+
}
123+
124+
func (cli *Client) parseMsgMetaInfo(node waBinary.Node) (metaInfo types.MsgMetaInfo, err error) {
125+
metaNode := node.GetChildByTag("meta")
126+
127+
ag := metaNode.AttrGetter()
128+
metaInfo.TargetID = types.MessageID(ag.String("target_id"))
129+
targetSenderJID := ag.OptionalJIDOrEmpty("target_sender_jid")
130+
if targetSenderJID.User != "" {
131+
metaInfo.TargetSender = targetSenderJID
132+
}
133+
err = ag.Error()
134+
return
135+
}
136+
99137
func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, error) {
100138
var info types.MessageInfo
101139
var err error
@@ -124,6 +162,16 @@ func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, er
124162
if err != nil {
125163
cli.Log.Warnf("Failed to parse verified_name node in %s: %v", info.ID, err)
126164
}
165+
case "bot":
166+
info.MsgBotInfo, err = cli.parseMsgBotInfo(child)
167+
if err != nil {
168+
cli.Log.Warnf("Failed to parse <bot> node in %s: %v", info.ID, err)
169+
}
170+
case "meta":
171+
info.MsgMetaInfo, err = cli.parseMsgMetaInfo(child)
172+
if err != nil {
173+
cli.Log.Warnf("Failed to parse <meta> node in %s: %v", info.ID, err)
174+
}
127175
case "franking":
128176
// TODO
129177
case "trace":
@@ -200,10 +248,47 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
200248
containsDirectMsg = true
201249
} else if info.IsGroup && encType == "skmsg" {
202250
decrypted, err = cli.decryptGroupMsg(&child, info.Sender, info.Chat)
251+
} else if encType == "msmsg" && info.Sender.IsBot() {
252+
// Meta AI / other bots (biz?):
253+
254+
// step 1: get message secret
255+
targetSenderJID := info.MsgMetaInfo.TargetSender
256+
if targetSenderJID.User == "" {
257+
// if no targetSenderJID in <meta> this must be ourselves (one-one-one mode)
258+
targetSenderJID = cli.getOwnID()
259+
}
260+
261+
messageSecret, err := cli.Store.MsgSecrets.GetMessageSecret(info.Chat, targetSenderJID, info.MsgMetaInfo.TargetID)
262+
if err != nil || messageSecret == nil {
263+
cli.Log.Warnf("Error getting message secret for bot msg with id %s", node.AttrGetter().String("id"))
264+
continue
265+
}
266+
267+
// step 2: get MessageSecretMessage
268+
byteContents := child.Content.([]byte) // <enc> contents
269+
var msMsg waE2E.MessageSecretMessage
270+
271+
err = proto.Unmarshal(byteContents, &msMsg)
272+
if err != nil {
273+
cli.Log.Warnf("Error decoding MessageSecretMesage protobuf %v", err)
274+
continue
275+
}
276+
277+
// step 3: determine best message id for decryption
278+
var messageID string
279+
if info.MsgBotInfo.EditType == types.EditTypeInner || info.MsgBotInfo.EditType == types.EditTypeLast {
280+
messageID = info.MsgBotInfo.EditTargetID
281+
} else {
282+
messageID = info.ID
283+
}
284+
285+
// step 4: decrypt and voila
286+
decrypted, err = cli.decryptBotMessage(messageSecret, &msMsg, messageID, targetSenderJID, info)
203287
} else {
204288
cli.Log.Warnf("Unhandled encrypted message (type %s) from %s", encType, info.SourceString())
205289
continue
206290
}
291+
207292
if err != nil {
208293
cli.Log.Warnf("Error decrypting message from %s: %v", info.SourceString(), err)
209294
isUnavailable := encType == "skmsg" && !containsDirectMsg && errors.Is(err, signalerror.ErrNoSenderKeyForUser)
@@ -220,7 +305,7 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
220305
cli.cancelDelayedRequestFromPhone(info.ID)
221306
}
222307

223-
var msg waProto.Message
308+
var msg waE2E.Message
224309
switch ag.Int("v") {
225310
case 2:
226311
err = proto.Unmarshal(decrypted, &msg)

msgsecret.go

+30-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
"fmt"
1212
"time"
1313

14+
"go.mau.fi/whatsmeow/proto/waCommon"
15+
"go.mau.fi/whatsmeow/proto/waE2E"
16+
1417
"go.mau.fi/util/random"
1518
"google.golang.org/protobuf/proto"
1619

@@ -26,8 +29,13 @@ type MsgSecretType string
2629
const (
2730
EncSecretPollVote MsgSecretType = "Poll Vote"
2831
EncSecretReaction MsgSecretType = "Enc Reaction"
32+
EncSecretBotMsg MsgSecretType = "Bot Message"
2933
)
3034

35+
func applyBotMessageHKDF(messageSecret []byte) []byte {
36+
return hkdfutil.SHA256(messageSecret, nil, []byte(EncSecretBotMsg), 32)
37+
}
38+
3139
func generateMsgSecretKey(
3240
modificationType MsgSecretType, modificationSender types.JID,
3341
origMsgID types.MessageID, origMsgSender types.JID, origMsgSecret []byte,
@@ -47,7 +55,7 @@ func generateMsgSecretKey(
4755
return secretKey, additionalData
4856
}
4957

50-
func getOrigSenderFromKey(msg *events.Message, key *waProto.MessageKey) (types.JID, error) {
58+
func getOrigSenderFromKey(msg *events.Message, key *waCommon.MessageKey) (types.JID, error) {
5159
if key.GetFromMe() {
5260
// fromMe always means the poll and vote were sent by the same user
5361
return msg.Info.Sender, nil
@@ -74,18 +82,18 @@ type messageEncryptedSecret interface {
7482
GetEncPayload() []byte
7583
}
7684

77-
func (cli *Client) decryptMsgSecret(msg *events.Message, useCase MsgSecretType, encrypted messageEncryptedSecret, origMsgKey *waProto.MessageKey) ([]byte, error) {
85+
func (cli *Client) decryptMsgSecret(msg *events.Message, useCase MsgSecretType, encrypted messageEncryptedSecret, origMsgKey *waCommon.MessageKey) ([]byte, error) {
7886
pollSender, err := getOrigSenderFromKey(msg, origMsgKey)
7987
if err != nil {
8088
return nil, err
8189
}
82-
baseEncKey, err := cli.Store.MsgSecrets.GetMessageSecret(msg.Info.Chat, pollSender, origMsgKey.GetId())
90+
baseEncKey, err := cli.Store.MsgSecrets.GetMessageSecret(msg.Info.Chat, pollSender, origMsgKey.GetID())
8391
if err != nil {
8492
return nil, fmt.Errorf("failed to get original message secret key: %w", err)
8593
} else if baseEncKey == nil {
8694
return nil, ErrOriginalMessageSecretNotFound
8795
}
88-
secretKey, additionalData := generateMsgSecretKey(useCase, msg.Info.Sender, origMsgKey.GetId(), pollSender, baseEncKey)
96+
secretKey, additionalData := generateMsgSecretKey(useCase, msg.Info.Sender, origMsgKey.GetID(), pollSender, baseEncKey)
8997
plaintext, err := gcmutil.Decrypt(secretKey, encrypted.GetEncIV(), encrypted.GetEncPayload(), additionalData)
9098
if err != nil {
9199
return nil, fmt.Errorf("failed to decrypt secret message: %w", err)
@@ -115,6 +123,18 @@ func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.
115123
return ciphertext, iv, nil
116124
}
117125

126+
func (cli *Client) decryptBotMessage(messageSecret []byte, msMsg messageEncryptedSecret, messageID types.MessageID, targetSenderJID types.JID, info *types.MessageInfo) ([]byte, error) {
127+
// gcm decrypt key generation
128+
newKey, additionalData := generateMsgSecretKey("", info.Sender, messageID, targetSenderJID, applyBotMessageHKDF(messageSecret))
129+
130+
plaintext, err := gcmutil.Decrypt(newKey, msMsg.GetEncIV(), msMsg.GetEncPayload(), additionalData)
131+
if err != nil {
132+
return nil, fmt.Errorf("failed to decrypt secret message: %w", err)
133+
}
134+
135+
return plaintext, nil
136+
}
137+
118138
// DecryptReaction decrypts a reaction update message. This form of reactions hasn't been rolled out yet,
119139
// so this function is likely not of much use.
120140
//
@@ -126,7 +146,7 @@ func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.
126146
// }
127147
// fmt.Printf("Reaction message: %+v\n", reaction)
128148
// }
129-
func (cli *Client) DecryptReaction(reaction *events.Message) (*waProto.ReactionMessage, error) {
149+
func (cli *Client) DecryptReaction(reaction *events.Message) (*waE2E.ReactionMessage, error) {
130150
encReaction := reaction.Message.GetEncReactionMessage()
131151
if encReaction == nil {
132152
return nil, ErrNotEncryptedReactionMessage
@@ -135,7 +155,7 @@ func (cli *Client) DecryptReaction(reaction *events.Message) (*waProto.ReactionM
135155
if err != nil {
136156
return nil, fmt.Errorf("failed to decrypt reaction: %w", err)
137157
}
138-
var msg waProto.ReactionMessage
158+
var msg waE2E.ReactionMessage
139159
err = proto.Unmarshal(plaintext, &msg)
140160
if err != nil {
141161
return nil, fmt.Errorf("failed to decode reaction protobuf: %w", err)
@@ -156,7 +176,7 @@ func (cli *Client) DecryptReaction(reaction *events.Message) (*waProto.ReactionM
156176
// fmt.Printf("- %X\n", hash)
157177
// }
158178
// }
159-
func (cli *Client) DecryptPollVote(vote *events.Message) (*waProto.PollVoteMessage, error) {
179+
func (cli *Client) DecryptPollVote(vote *events.Message) (*waE2E.PollVoteMessage, error) {
160180
pollUpdate := vote.Message.GetPollUpdateMessage()
161181
if pollUpdate == nil {
162182
return nil, ErrNotPollUpdateMessage
@@ -165,16 +185,16 @@ func (cli *Client) DecryptPollVote(vote *events.Message) (*waProto.PollVoteMessa
165185
if err != nil {
166186
return nil, fmt.Errorf("failed to decrypt poll vote: %w", err)
167187
}
168-
var msg waProto.PollVoteMessage
188+
var msg waE2E.PollVoteMessage
169189
err = proto.Unmarshal(plaintext, &msg)
170190
if err != nil {
171191
return nil, fmt.Errorf("failed to decode poll vote protobuf: %w", err)
172192
}
173193
return &msg, nil
174194
}
175195

176-
func getKeyFromInfo(msgInfo *types.MessageInfo) *waProto.MessageKey {
177-
creationKey := &waProto.MessageKey{
196+
func getKeyFromInfo(msgInfo *types.MessageInfo) *waCommon.MessageKey {
197+
creationKey := &waCommon.MessageKey{
178198
RemoteJID: proto.String(msgInfo.Chat.String()),
179199
FromMe: proto.Bool(msgInfo.IsFromMe),
180200
ID: proto.String(msgInfo.ID),

prekeys.go

+22-7
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,34 @@ func nodeToPreKeyBundle(deviceID uint32, node waBinary.Node) (*prekey.Bundle, er
174174
}
175175
identityKeyPub := *(*[32]byte)(identityKeyRaw)
176176

177-
preKey, err := nodeToPreKey(keysNode.GetChildByTag("key"))
178-
if err != nil {
179-
return nil, fmt.Errorf("invalid prekey in prekey response: %w", err)
177+
preKeyNode, ok := keysNode.GetOptionalChildByTag("key")
178+
preKey := &keys.PreKey{}
179+
if ok {
180+
var err error
181+
preKey, err = nodeToPreKey(preKeyNode)
182+
if err != nil {
183+
return nil, fmt.Errorf("invalid prekey in prekey response: %w", err)
184+
}
180185
}
186+
181187
signedPreKey, err := nodeToPreKey(keysNode.GetChildByTag("skey"))
182188
if err != nil {
183189
return nil, fmt.Errorf("invalid signed prekey in prekey response: %w", err)
184190
}
185191

186-
return prekey.NewBundle(registrationID, deviceID,
187-
optional.NewOptionalUint32(preKey.KeyID), signedPreKey.KeyID,
188-
ecc.NewDjbECPublicKey(*preKey.Pub), ecc.NewDjbECPublicKey(*signedPreKey.Pub), *signedPreKey.Signature,
189-
identity.NewKey(ecc.NewDjbECPublicKey(identityKeyPub))), nil
192+
var bundle *prekey.Bundle
193+
if ok {
194+
bundle = prekey.NewBundle(registrationID, deviceID,
195+
optional.NewOptionalUint32(preKey.KeyID), signedPreKey.KeyID,
196+
ecc.NewDjbECPublicKey(*preKey.Pub), ecc.NewDjbECPublicKey(*signedPreKey.Pub), *signedPreKey.Signature,
197+
identity.NewKey(ecc.NewDjbECPublicKey(identityKeyPub)))
198+
} else {
199+
bundle = prekey.NewBundle(registrationID, deviceID, optional.NewEmptyUint32(), signedPreKey.KeyID,
200+
nil, ecc.NewDjbECPublicKey(*signedPreKey.Pub), *signedPreKey.Signature,
201+
identity.NewKey(ecc.NewDjbECPublicKey(identityKeyPub)))
202+
}
203+
204+
return bundle, nil
190205
}
191206

192207
func nodeToPreKey(node waBinary.Node) (*keys.PreKey, error) {

retry.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
280280
}
281281
var content []waBinary.Node
282282
if msg.wa != nil {
283-
content = cli.getMessageContent(*encrypted, msg.wa, attrs, includeDeviceIdentity)
283+
content = cli.getMessageContent(*encrypted, msg.wa, attrs, includeDeviceIdentity, nil)
284284
} else {
285285
content = []waBinary.Node{
286286
*encrypted,

0 commit comments

Comments
 (0)