forked from lug/matterbridge
Update dependencies (#1929)
This commit is contained in:
267
vendor/go.mau.fi/whatsmeow/msgsecret.go
vendored
Normal file
267
vendor/go.mau.fi/whatsmeow/msgsecret.go
vendored
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2022 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package whatsmeow
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"go.mau.fi/whatsmeow/util/gcmutil"
|
||||
"go.mau.fi/whatsmeow/util/hkdfutil"
|
||||
)
|
||||
|
||||
type MsgSecretType string
|
||||
|
||||
const (
|
||||
EncSecretPollVote MsgSecretType = "Poll Vote"
|
||||
EncSecretReaction MsgSecretType = "Enc Reaction"
|
||||
)
|
||||
|
||||
func generateMsgSecretKey(
|
||||
modificationType MsgSecretType, modificationSender types.JID,
|
||||
origMsgID types.MessageID, origMsgSender types.JID, origMsgSecret []byte,
|
||||
) ([]byte, []byte) {
|
||||
origMsgSenderStr := origMsgSender.ToNonAD().String()
|
||||
modificationSenderStr := modificationSender.ToNonAD().String()
|
||||
|
||||
useCaseSecret := make([]byte, 0, len(origMsgID)+len(origMsgSenderStr)+len(modificationSenderStr)+len(modificationType))
|
||||
useCaseSecret = append(useCaseSecret, origMsgID...)
|
||||
useCaseSecret = append(useCaseSecret, origMsgSenderStr...)
|
||||
useCaseSecret = append(useCaseSecret, modificationSenderStr...)
|
||||
useCaseSecret = append(useCaseSecret, modificationType...)
|
||||
|
||||
secretKey := hkdfutil.SHA256(origMsgSecret, nil, useCaseSecret, 32)
|
||||
additionalData := []byte(fmt.Sprintf("%s\x00%s", origMsgID, modificationSenderStr))
|
||||
|
||||
return secretKey, additionalData
|
||||
}
|
||||
|
||||
func getOrigSenderFromKey(msg *events.Message, key *waProto.MessageKey) (types.JID, error) {
|
||||
if key.GetFromMe() {
|
||||
// fromMe always means the poll and vote were sent by the same user
|
||||
return msg.Info.Sender, nil
|
||||
} else if msg.Info.Chat.Server == types.DefaultUserServer {
|
||||
sender, err := types.ParseJID(key.GetRemoteJid())
|
||||
if err != nil {
|
||||
return types.EmptyJID, fmt.Errorf("failed to parse JID %q of original message sender: %w", key.GetRemoteJid(), err)
|
||||
}
|
||||
return sender, nil
|
||||
} else {
|
||||
sender, err := types.ParseJID(key.GetParticipant())
|
||||
if sender.Server != types.DefaultUserServer {
|
||||
err = fmt.Errorf("unexpected server")
|
||||
}
|
||||
if err != nil {
|
||||
return types.EmptyJID, fmt.Errorf("failed to parse JID %q of original message sender: %w", key.GetParticipant(), err)
|
||||
}
|
||||
return sender, nil
|
||||
}
|
||||
}
|
||||
|
||||
type messageEncryptedSecret interface {
|
||||
GetEncIv() []byte
|
||||
GetEncPayload() []byte
|
||||
}
|
||||
|
||||
func (cli *Client) decryptMsgSecret(msg *events.Message, useCase MsgSecretType, encrypted messageEncryptedSecret, origMsgKey *waProto.MessageKey) ([]byte, error) {
|
||||
pollSender, err := getOrigSenderFromKey(msg, origMsgKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseEncKey, err := cli.Store.MsgSecrets.GetMessageSecret(msg.Info.Chat, pollSender, origMsgKey.GetId())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get original message secret key: %w", err)
|
||||
} else if baseEncKey == nil {
|
||||
return nil, ErrOriginalMessageSecretNotFound
|
||||
}
|
||||
secretKey, additionalData := generateMsgSecretKey(useCase, msg.Info.Sender, origMsgKey.GetId(), pollSender, baseEncKey)
|
||||
plaintext, err := gcmutil.Decrypt(secretKey, encrypted.GetEncIv(), encrypted.GetEncPayload(), additionalData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt secret message: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.MessageID, useCase MsgSecretType, plaintext []byte) (ciphertext, iv []byte, err error) {
|
||||
ownID := *cli.Store.ID
|
||||
|
||||
baseEncKey, err := cli.Store.MsgSecrets.GetMessageSecret(chat, origSender, origMsgID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get original message secret key: %w", err)
|
||||
} else if baseEncKey == nil {
|
||||
return nil, nil, ErrOriginalMessageSecretNotFound
|
||||
}
|
||||
secretKey, additionalData := generateMsgSecretKey(useCase, ownID, origMsgID, origSender, baseEncKey)
|
||||
|
||||
iv = make([]byte, 12)
|
||||
_, err = rand.Read(iv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate iv: %w", err)
|
||||
}
|
||||
ciphertext, err = gcmutil.Encrypt(secretKey, iv, plaintext, additionalData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encrypt secret message: %w", err)
|
||||
}
|
||||
return ciphertext, iv, nil
|
||||
}
|
||||
|
||||
// DecryptReaction decrypts a reaction update message. This form of reactions hasn't been rolled out yet,
|
||||
// so this function is likely not of much use.
|
||||
//
|
||||
// if evt.Message.GetEncReactionMessage() != nil {
|
||||
// reaction, err := cli.DecryptReaction(evt)
|
||||
// if err != nil {
|
||||
// fmt.Println(":(", err)
|
||||
// return
|
||||
// }
|
||||
// fmt.Printf("Reaction message: %+v\n", reaction)
|
||||
// }
|
||||
func (cli *Client) DecryptReaction(reaction *events.Message) (*waProto.ReactionMessage, error) {
|
||||
encReaction := reaction.Message.GetEncReactionMessage()
|
||||
if encReaction != nil {
|
||||
return nil, ErrNotEncryptedReactionMessage
|
||||
}
|
||||
plaintext, err := cli.decryptMsgSecret(reaction, EncSecretReaction, encReaction, encReaction.GetTargetMessageKey())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt reaction: %w", err)
|
||||
}
|
||||
var msg waProto.ReactionMessage
|
||||
err = proto.Unmarshal(plaintext, &msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode reaction protobuf: %w", err)
|
||||
}
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// DecryptPollVote decrypts a poll update message. The vote itself includes SHA-256 hashes of the selected options.
|
||||
//
|
||||
// if evt.Message.GetPollUpdateMessage() != nil {
|
||||
// pollVote, err := cli.DecryptPollVote(evt)
|
||||
// if err != nil {
|
||||
// fmt.Println(":(", err)
|
||||
// return
|
||||
// }
|
||||
// fmt.Println("Selected hashes:")
|
||||
// for _, hash := range pollVote.GetSelectedOptions() {
|
||||
// fmt.Printf("- %X\n", hash)
|
||||
// }
|
||||
// }
|
||||
func (cli *Client) DecryptPollVote(vote *events.Message) (*waProto.PollVoteMessage, error) {
|
||||
pollUpdate := vote.Message.GetPollUpdateMessage()
|
||||
if pollUpdate == nil {
|
||||
return nil, ErrNotPollUpdateMessage
|
||||
}
|
||||
plaintext, err := cli.decryptMsgSecret(vote, EncSecretPollVote, pollUpdate.GetVote(), pollUpdate.GetPollCreationMessageKey())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt poll vote: %w", err)
|
||||
}
|
||||
var msg waProto.PollVoteMessage
|
||||
err = proto.Unmarshal(plaintext, &msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode poll vote protobuf: %w", err)
|
||||
}
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func getKeyFromInfo(msgInfo *types.MessageInfo) *waProto.MessageKey {
|
||||
creationKey := &waProto.MessageKey{
|
||||
RemoteJid: proto.String(msgInfo.Chat.String()),
|
||||
FromMe: proto.Bool(msgInfo.IsFromMe),
|
||||
Id: proto.String(msgInfo.ID),
|
||||
}
|
||||
if msgInfo.IsGroup {
|
||||
creationKey.Participant = proto.String(msgInfo.Sender.String())
|
||||
}
|
||||
return creationKey
|
||||
}
|
||||
|
||||
// HashPollOptions hashes poll option names using SHA-256 for voting.
|
||||
// This is used by BuildPollVote to convert selected option names to hashes.
|
||||
func HashPollOptions(optionNames []string) [][]byte {
|
||||
optionHashes := make([][]byte, len(optionNames))
|
||||
for i, option := range optionNames {
|
||||
optionHash := sha256.Sum256([]byte(option))
|
||||
optionHashes[i] = optionHash[:]
|
||||
}
|
||||
return optionHashes
|
||||
}
|
||||
|
||||
// BuildPollVote builds a poll vote message using the given poll message info and option names.
|
||||
// The built message can be sent normally using Client.SendMessage.
|
||||
//
|
||||
// For example, to vote for the first option after receiving a message event (*events.Message):
|
||||
//
|
||||
// if evt.Message.GetPollCreationMessage() != nil {
|
||||
// pollVoteMsg, err := cli.BuildPollVote(&evt.Info, []string{evt.Message.GetPollCreationMessage().GetOptions()[0].GetOptionName()})
|
||||
// if err != nil {
|
||||
// fmt.Println(":(", err)
|
||||
// return
|
||||
// }
|
||||
// resp, err := cli.SendMessage(context.Background(), evt.Info.Chat, "", pollVoteMsg)
|
||||
// }
|
||||
func (cli *Client) BuildPollVote(pollInfo *types.MessageInfo, optionNames []string) (*waProto.Message, error) {
|
||||
pollUpdate, err := cli.EncryptPollVote(pollInfo, &waProto.PollVoteMessage{
|
||||
SelectedOptions: HashPollOptions(optionNames),
|
||||
})
|
||||
return &waProto.Message{PollUpdateMessage: pollUpdate}, err
|
||||
}
|
||||
|
||||
// BuildPollCreation builds a poll creation message with the given poll name, options and maximum number of selections.
|
||||
// The built message can be sent normally using Client.SendMessage.
|
||||
//
|
||||
// resp, err := cli.SendMessage(context.Background(), chat, "", cli.BuildPollCreation("meow?", []string{"yes", "no"}, 1))
|
||||
func (cli *Client) BuildPollCreation(name string, optionNames []string, selectableOptionCount int) *waProto.Message {
|
||||
msgSecret := make([]byte, 32)
|
||||
_, err := rand.Read(msgSecret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if selectableOptionCount < 0 || selectableOptionCount > len(optionNames) {
|
||||
selectableOptionCount = 0
|
||||
}
|
||||
options := make([]*waProto.PollCreationMessage_Option, len(optionNames))
|
||||
for i, option := range optionNames {
|
||||
options[i] = &waProto.PollCreationMessage_Option{OptionName: proto.String(option)}
|
||||
}
|
||||
return &waProto.Message{
|
||||
PollCreationMessage: &waProto.PollCreationMessage{
|
||||
Name: proto.String(name),
|
||||
Options: options,
|
||||
SelectableOptionsCount: proto.Uint32(uint32(selectableOptionCount)),
|
||||
},
|
||||
MessageContextInfo: &waProto.MessageContextInfo{
|
||||
MessageSecret: msgSecret,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptPollVote encrypts a poll vote message. This is a slightly lower-level function, using BuildPollVote is recommended.
|
||||
func (cli *Client) EncryptPollVote(pollInfo *types.MessageInfo, vote *waProto.PollVoteMessage) (*waProto.PollUpdateMessage, error) {
|
||||
plaintext, err := proto.Marshal(vote)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal poll vote protobuf: %w", err)
|
||||
}
|
||||
ciphertext, iv, err := cli.encryptMsgSecret(pollInfo.Chat, pollInfo.Sender, pollInfo.ID, EncSecretPollVote, plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt poll vote: %w", err)
|
||||
}
|
||||
return &waProto.PollUpdateMessage{
|
||||
PollCreationMessageKey: getKeyFromInfo(pollInfo),
|
||||
Vote: &waProto.PollEncValue{
|
||||
EncPayload: ciphertext,
|
||||
EncIv: iv,
|
||||
},
|
||||
SenderTimestampMs: proto.Int64(time.Now().UnixMilli()),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user