diff options
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/msgsecret.go')
-rw-r--r-- | vendor/go.mau.fi/whatsmeow/msgsecret.go | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/vendor/go.mau.fi/whatsmeow/msgsecret.go b/vendor/go.mau.fi/whatsmeow/msgsecret.go new file mode 100644 index 00000000..75e9b271 --- /dev/null +++ b/vendor/go.mau.fi/whatsmeow/msgsecret.go @@ -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 +} |