diff options
Diffstat (limited to 'teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/send.go')
| -rw-r--r-- | teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/send.go | 724 |
1 files changed, 724 insertions, 0 deletions
diff --git a/teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/send.go b/teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/send.go new file mode 100644 index 0000000..96d888b --- /dev/null +++ b/teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/send.go @@ -0,0 +1,724 @@ +// 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 ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "go.mau.fi/libsignal/signalerror" + "google.golang.org/protobuf/proto" + + "go.mau.fi/libsignal/groups" + "go.mau.fi/libsignal/keys/prekey" + "go.mau.fi/libsignal/protocol" + "go.mau.fi/libsignal/session" + + waBinary "go.mau.fi/whatsmeow/binary" + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/types" +) + +// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp. +// +// msgID := whatsmeow.GenerateMessageID() +// cli.SendMessage(context.Background(), targetJID, &waProto.Message{...}, whatsmeow.SendRequestExtra{ID: msgID}) +func GenerateMessageID() types.MessageID { + id := make([]byte, 8) + _, err := rand.Read(id) + if err != nil { + // Out of entropy + panic(err) + } + return "3EB0" + strings.ToUpper(hex.EncodeToString(id)) +} + +type MessageDebugTimings struct { + Queue time.Duration + + Marshal time.Duration + GetParticipants time.Duration + GetDevices time.Duration + GroupEncrypt time.Duration + PeerEncrypt time.Duration + + Send time.Duration + Resp time.Duration + Retry time.Duration +} + +type SendResponse struct { + // The message timestamp returned by the server + Timestamp time.Time + + // The ID of the sent message + ID types.MessageID + + // Message handling duration, used for debugging + DebugTimings MessageDebugTimings +} + +// SendRequestExtra contains the optional parameters for SendMessage. +// +// By default, optional parameters don't have to be provided at all, e.g. +// +// cli.SendMessage(ctx, to, message) +// +// When providing optional parameters, add a single instance of this struct as the last parameter: +// +// cli.SendMessage(ctx, to, message, whatsmeow.SendRequestExtra{...}) +// +// Trying to add multiple extra parameters will return an error. +type SendRequestExtra struct { + // The message ID to use when sending. If this is not provided, a random message ID will be generated + ID types.MessageID + // Should the message be sent as a peer message (protocol messages to your own devices, e.g. app state key requests) + Peer bool +} + +// SendMessage sends the given message. +// +// This method will wait for the server to acknowledge the message before returning. +// The return value is the timestamp of the message from the server. +// +// Optional parameters like the message ID can be specified with the SendRequestExtra struct. +// Only one extra parameter is allowed, put all necessary parameters in the same struct. +// +// The message itself can contain anything you want (within the protobuf schema). +// e.g. for a simple text message, use the Conversation field: +// +// cli.SendMessage(context.Background(), targetJID, &waProto.Message{ +// Conversation: proto.String("Hello, World!"), +// }) +// +// Things like replies, mentioning users and the "forwarded" flag are stored in ContextInfo, +// which can be put in ExtendedTextMessage and any of the media message types. +// +// For uploading and sending media/attachments, see the Upload method. +// +// For other message types, you'll have to figure it out yourself. Looking at the protobuf schema +// in binary/proto/def.proto may be useful to find out all the allowed fields. Printing the RawMessage +// field in incoming message events to figure out what it contains is also a good way to learn how to +// send the same kind of message. +func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waProto.Message, extra ...SendRequestExtra) (resp SendResponse, err error) { + var req SendRequestExtra + if len(extra) > 1 { + err = errors.New("only one extra parameter may be provided to SendMessage") + return + } else if len(extra) == 1 { + req = extra[0] + } + if to.AD && !req.Peer { + err = ErrRecipientADJID + return + } + ownID := cli.getOwnID() + if ownID.IsEmpty() { + err = ErrNotLoggedIn + return + } + + if len(req.ID) == 0 { + req.ID = GenerateMessageID() + } + resp.ID = req.ID + + start := time.Now() + // Sending multiple messages at a time can cause weird issues and makes it harder to retry safely + cli.messageSendLock.Lock() + resp.DebugTimings.Queue = time.Since(start) + defer cli.messageSendLock.Unlock() + + respChan := cli.waitResponse(req.ID) + // Peer message retries aren't implemented yet + if !req.Peer { + cli.addRecentMessage(to, req.ID, message) + } + if message.GetMessageContextInfo().GetMessageSecret() != nil { + err = cli.Store.MsgSecrets.PutMessageSecret(to, ownID, req.ID, message.GetMessageContextInfo().GetMessageSecret()) + if err != nil { + cli.Log.Warnf("Failed to store message secret key for outgoing message %s: %v", req.ID, err) + } else { + cli.Log.Debugf("Stored message secret key for outgoing message %s", req.ID) + } + } + var phash string + var data []byte + switch to.Server { + case types.GroupServer, types.BroadcastServer: + phash, data, err = cli.sendGroup(ctx, to, ownID, req.ID, message, &resp.DebugTimings) + case types.DefaultUserServer: + if req.Peer { + data, err = cli.sendPeerMessage(to, req.ID, message, &resp.DebugTimings) + } else { + data, err = cli.sendDM(ctx, to, ownID, req.ID, message, &resp.DebugTimings) + } + default: + err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server) + } + start = time.Now() + if err != nil { + cli.cancelResponse(req.ID, respChan) + return + } + var respNode *waBinary.Node + select { + case respNode = <-respChan: + case <-ctx.Done(): + err = ctx.Err() + return + } + resp.DebugTimings.Resp = time.Since(start) + if isDisconnectNode(respNode) { + start = time.Now() + respNode, err = cli.retryFrame("message send", req.ID, data, respNode, ctx, 0) + resp.DebugTimings.Retry = time.Since(start) + if err != nil { + return + } + } + ag := respNode.AttrGetter() + resp.Timestamp = ag.UnixTime("t") + if errorCode := ag.Int("error"); errorCode != 0 { + err = fmt.Errorf("%w %d", ErrServerReturnedError, errorCode) + } + expectedPHash := ag.OptionalString("phash") + if len(expectedPHash) > 0 && phash != expectedPHash { + cli.Log.Warnf("Server returned different participant list hash when sending to %s. Some devices may not have received the message.", to) + // TODO also invalidate device list caches + cli.groupParticipantsCacheLock.Lock() + delete(cli.groupParticipantsCache, to) + cli.groupParticipantsCacheLock.Unlock() + } + return +} + +// RevokeMessage deletes the given message from everyone in the chat. +// +// This method will wait for the server to acknowledge the revocation message before returning. +// The return value is the timestamp of the message from the server. +// +// Deprecated: This method is deprecated in favor of BuildRevoke +func (cli *Client) RevokeMessage(chat types.JID, id types.MessageID) (SendResponse, error) { + return cli.SendMessage(context.TODO(), chat, cli.BuildRevoke(chat, types.EmptyJID, id)) +} + +// BuildRevoke builds a message revocation message using the given variables. +// The built message can be sent normally using Client.SendMessage. +// +// To revoke your own messages, pass your JID or an empty JID as the second parameter (sender). +// +// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, types.EmptyJID, originalMessageID) +// +// To revoke someone else's messages when you are group admin, pass the message sender's JID as the second parameter. +// +// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, senderJID, originalMessageID) +func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waProto.Message { + key := &waProto.MessageKey{ + FromMe: proto.Bool(true), + Id: proto.String(id), + RemoteJid: proto.String(chat.String()), + } + if !sender.IsEmpty() && sender.User != cli.getOwnID().User { + key.FromMe = proto.Bool(false) + if chat.Server != types.DefaultUserServer { + key.Participant = proto.String(sender.ToNonAD().String()) + } + } + return &waProto.Message{ + ProtocolMessage: &waProto.ProtocolMessage{ + Type: waProto.ProtocolMessage_REVOKE.Enum(), + Key: key, + }, + } +} + +// BuildEdit builds a message edit message using the given variables. +// The built message can be sent normally using Client.SendMessage. +// +// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildEdit(chat, originalMessageID, &waProto.Message{ +// Conversation: proto.String("edited message"), +// }) +func (cli *Client) BuildEdit(chat types.JID, id types.MessageID, newContent *waProto.Message) *waProto.Message { + return &waProto.Message{ + EditedMessage: &waProto.FutureProofMessage{ + Message: &waProto.Message{ + ProtocolMessage: &waProto.ProtocolMessage{ + Key: &waProto.MessageKey{ + FromMe: proto.Bool(true), + Id: proto.String(id), + RemoteJid: proto.String(chat.String()), + }, + Type: waProto.ProtocolMessage_MESSAGE_EDIT.Enum(), + EditedMessage: newContent, + TimestampMs: proto.Int64(time.Now().UnixMilli()), + }, + }, + }, + } +} + +const ( + DisappearingTimerOff = time.Duration(0) + DisappearingTimer24Hours = 24 * time.Hour + DisappearingTimer7Days = 7 * 24 * time.Hour + DisappearingTimer90Days = 90 * 24 * time.Hour +) + +// ParseDisappearingTimerString parses common human-readable disappearing message timer strings into Duration values. +// If the string doesn't look like one of the allowed values (0, 24h, 7d, 90d), the second return value is false. +func ParseDisappearingTimerString(val string) (time.Duration, bool) { + switch strings.ReplaceAll(strings.ToLower(val), " ", "") { + case "0d", "0h", "0s", "0", "off": + return DisappearingTimerOff, true + case "1day", "day", "1d", "1", "24h", "24", "86400s", "86400": + return DisappearingTimer24Hours, true + case "1week", "week", "7d", "7", "168h", "168", "604800s", "604800": + return DisappearingTimer7Days, true + case "3months", "3m", "3mo", "90d", "90", "2160h", "2160", "7776000s", "7776000": + return DisappearingTimer90Days, true + default: + return 0, false + } +} + +// SetDisappearingTimer sets the disappearing timer in a chat. Both private chats and groups are supported, but they're +// set with different methods. +// +// Note that while this function allows passing non-standard durations, official WhatsApp apps will ignore those, +// and in groups the server will just reject the change. You can use the DisappearingTimer<Duration> constants for convenience. +// +// In groups, the server will echo the change as a notification, so it'll show up as a *events.GroupInfo update. +func (cli *Client) SetDisappearingTimer(chat types.JID, timer time.Duration) (err error) { + switch chat.Server { + case types.DefaultUserServer: + _, err = cli.SendMessage(context.TODO(), chat, &waProto.Message{ + ProtocolMessage: &waProto.ProtocolMessage{ + Type: waProto.ProtocolMessage_EPHEMERAL_SETTING.Enum(), + EphemeralExpiration: proto.Uint32(uint32(timer.Seconds())), + }, + }) + case types.GroupServer: + if timer == 0 { + _, err = cli.sendGroupIQ(context.TODO(), iqSet, chat, waBinary.Node{Tag: "not_ephemeral"}) + } else { + _, err = cli.sendGroupIQ(context.TODO(), iqSet, chat, waBinary.Node{ + Tag: "ephemeral", + Attrs: waBinary.Attrs{ + "expiration": strconv.Itoa(int(timer.Seconds())), + }, + }) + if errors.Is(err, ErrIQBadRequest) { + err = wrapIQError(ErrInvalidDisappearingTimer, err) + } + } + default: + err = fmt.Errorf("can't set disappearing time in a %s chat", chat.Server) + } + return +} + +func participantListHashV2(participants []types.JID) string { + participantsStrings := make([]string, len(participants)) + for i, part := range participants { + participantsStrings[i] = part.String() + } + + sort.Strings(participantsStrings) + hash := sha256.Sum256([]byte(strings.Join(participantsStrings, ""))) + return fmt.Sprintf("2:%s", base64.RawStdEncoding.EncodeToString(hash[:6])) +} + +func (cli *Client) sendGroup(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (string, []byte, error) { + var participants []types.JID + var err error + start := time.Now() + if to.Server == types.GroupServer { + participants, err = cli.getGroupMembers(ctx, to) + if err != nil { + return "", nil, fmt.Errorf("failed to get group members: %w", err) + } + } else { + // TODO use context + participants, err = cli.getBroadcastListParticipants(to) + if err != nil { + return "", nil, fmt.Errorf("failed to get broadcast list members: %w", err) + } + } + timings.GetParticipants = time.Since(start) + start = time.Now() + plaintext, _, err := marshalMessage(to, message) + timings.Marshal = time.Since(start) + if err != nil { + return "", nil, err + } + + start = time.Now() + builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer) + senderKeyName := protocol.NewSenderKeyName(to.String(), ownID.SignalAddress()) + signalSKDMessage, err := builder.Create(senderKeyName) + if err != nil { + return "", nil, fmt.Errorf("failed to create sender key distribution message to send %s to %s: %w", id, to, err) + } + skdMessage := &waProto.Message{ + SenderKeyDistributionMessage: &waProto.SenderKeyDistributionMessage{ + GroupId: proto.String(to.String()), + AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(), + }, + } + skdPlaintext, err := proto.Marshal(skdMessage) + if err != nil { + return "", nil, fmt.Errorf("failed to marshal sender key distribution message to send %s to %s: %w", id, to, err) + } + + cipher := groups.NewGroupCipher(builder, senderKeyName, cli.Store) + encrypted, err := cipher.Encrypt(padMessage(plaintext)) + if err != nil { + return "", nil, fmt.Errorf("failed to encrypt group message to send %s to %s: %w", id, to, err) + } + ciphertext := encrypted.SignedSerialize() + timings.GroupEncrypt = time.Since(start) + + node, allDevices, err := cli.prepareMessageNode(ctx, to, ownID, id, message, participants, skdPlaintext, nil, timings) + if err != nil { + return "", nil, err + } + + phash := participantListHashV2(allDevices) + node.Attrs["phash"] = phash + node.Content = append(node.GetChildren(), waBinary.Node{ + Tag: "enc", + Content: ciphertext, + Attrs: waBinary.Attrs{"v": "2", "type": "skmsg"}, + }) + + start = time.Now() + data, err := cli.sendNodeAndGetData(*node) + timings.Send = time.Since(start) + if err != nil { + return "", nil, fmt.Errorf("failed to send message node: %w", err) + } + return phash, data, nil +} + +func (cli *Client) sendPeerMessage(to types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) ([]byte, error) { + node, err := cli.preparePeerMessageNode(to, id, message, timings) + if err != nil { + return nil, err + } + start := time.Now() + data, err := cli.sendNodeAndGetData(*node) + timings.Send = time.Since(start) + if err != nil { + return nil, fmt.Errorf("failed to send message node: %w", err) + } + return data, nil +} + +func (cli *Client) sendDM(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) ([]byte, error) { + start := time.Now() + messagePlaintext, deviceSentMessagePlaintext, err := marshalMessage(to, message) + timings.Marshal = time.Since(start) + if err != nil { + return nil, err + } + + node, _, err := cli.prepareMessageNode(ctx, to, ownID, id, message, []types.JID{to, ownID.ToNonAD()}, messagePlaintext, deviceSentMessagePlaintext, timings) + if err != nil { + return nil, err + } + start = time.Now() + data, err := cli.sendNodeAndGetData(*node) + timings.Send = time.Since(start) + if err != nil { + return nil, fmt.Errorf("failed to send message node: %w", err) + } + return data, nil +} + +func getTypeFromMessage(msg *waProto.Message) string { + switch { + case msg.ViewOnceMessage != nil: + return getTypeFromMessage(msg.ViewOnceMessage.Message) + case msg.ViewOnceMessageV2 != nil: + return getTypeFromMessage(msg.ViewOnceMessageV2.Message) + case msg.EphemeralMessage != nil: + return getTypeFromMessage(msg.EphemeralMessage.Message) + case msg.DocumentWithCaptionMessage != nil: + return getTypeFromMessage(msg.DocumentWithCaptionMessage.Message) + case msg.ReactionMessage != nil: + return "reaction" + case msg.PollCreationMessage != nil, msg.PollUpdateMessage != nil: + return "poll" + case msg.Conversation != nil, msg.ExtendedTextMessage != nil, msg.ProtocolMessage != nil: + return "text" + //TODO this requires setting mediatype in the enc nodes + //case msg.ImageMessage != nil, msg.DocumentMessage != nil, msg.AudioMessage != nil, msg.VideoMessage != nil: + // return "media" + default: + return "text" + } +} + +const ( + EditAttributeEmpty = "" + EditAttributeMessageEdit = "1" + EditAttributeSenderRevoke = "7" + EditAttributeAdminRevoke = "8" +) + +const RemoveReactionText = "" + +func getEditAttribute(msg *waProto.Message) string { + switch { + case msg.ProtocolMessage != nil && msg.ProtocolMessage.GetKey() != nil: + switch msg.ProtocolMessage.GetType() { + case waProto.ProtocolMessage_REVOKE: + if msg.ProtocolMessage.GetKey().GetFromMe() { + return EditAttributeSenderRevoke + } else { + return EditAttributeAdminRevoke + } + case waProto.ProtocolMessage_MESSAGE_EDIT: + if msg.EditedMessage != nil { + return EditAttributeMessageEdit + } + } + case msg.ReactionMessage != nil && msg.ReactionMessage.GetText() == RemoveReactionText: + return EditAttributeSenderRevoke + case msg.KeepInChatMessage != nil && msg.KeepInChatMessage.GetKey().GetFromMe() && msg.KeepInChatMessage.GetKeepType() == waProto.KeepType_UNDO_KEEP_FOR_ALL: + return EditAttributeSenderRevoke + } + return EditAttributeEmpty +} + +func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (*waBinary.Node, error) { + attrs := waBinary.Attrs{ + "id": id, + "type": "text", + "category": "peer", + "to": to, + } + if message.GetProtocolMessage().GetType() == waProto.ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST { + attrs["push_priority"] = "high" + } + start := time.Now() + plaintext, err := proto.Marshal(message) + timings.Marshal = time.Since(start) + if err != nil { + err = fmt.Errorf("failed to marshal message: %w", err) + return nil, err + } + start = time.Now() + encrypted, isPreKey, err := cli.encryptMessageForDevice(plaintext, to, nil) + timings.PeerEncrypt = time.Since(start) + if err != nil { + return nil, fmt.Errorf("failed to encrypt peer message for %s: %v", to, err) + } + content := []waBinary.Node{*encrypted} + if isPreKey { + content = append(content, cli.makeDeviceIdentityNode()) + } + return &waBinary.Node{ + Tag: "message", + Attrs: attrs, + Content: content, + }, nil +} + +func (cli *Client) prepareMessageNode(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, participants []types.JID, plaintext, dsmPlaintext []byte, timings *MessageDebugTimings) (*waBinary.Node, []types.JID, error) { + start := time.Now() + allDevices, err := cli.GetUserDevicesContext(ctx, participants) + timings.GetDevices = time.Since(start) + if err != nil { + return nil, nil, fmt.Errorf("failed to get device list: %w", err) + } + + attrs := waBinary.Attrs{ + "id": id, + "type": getTypeFromMessage(message), + "to": to, + } + if editAttr := getEditAttribute(message); editAttr != "" { + attrs["edit"] = editAttr + } + + start = time.Now() + participantNodes, includeIdentity := cli.encryptMessageForDevices(ctx, allDevices, ownID, id, plaintext, dsmPlaintext) + timings.PeerEncrypt = time.Since(start) + content := []waBinary.Node{{ + Tag: "participants", + Content: participantNodes, + }} + if includeIdentity { + content = append(content, cli.makeDeviceIdentityNode()) + } + if attrs["type"] == "poll" { + pollType := "creation" + if message.PollUpdateMessage != nil { + pollType = "vote" + } + content = append(content, waBinary.Node{ + Tag: "meta", + Attrs: waBinary.Attrs{ + "polltype": pollType, + }, + }) + } + return &waBinary.Node{ + Tag: "message", + Attrs: attrs, + Content: content, + }, allDevices, nil +} + +func marshalMessage(to types.JID, message *waProto.Message) (plaintext, dsmPlaintext []byte, err error) { + plaintext, err = proto.Marshal(message) + if err != nil { + err = fmt.Errorf("failed to marshal message: %w", err) + return + } + + if to.Server != types.GroupServer { + dsmPlaintext, err = proto.Marshal(&waProto.Message{ + DeviceSentMessage: &waProto.DeviceSentMessage{ + DestinationJid: proto.String(to.String()), + Message: message, + }, + }) + if err != nil { + err = fmt.Errorf("failed to marshal message (for own devices): %w", err) + return + } + } + + return +} + +func (cli *Client) makeDeviceIdentityNode() waBinary.Node { + deviceIdentity, err := proto.Marshal(cli.Store.Account) + if err != nil { + panic(fmt.Errorf("failed to marshal device identity: %w", err)) + } + return waBinary.Node{ + Tag: "device-identity", + Content: deviceIdentity, + } +} + +func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []types.JID, ownID types.JID, id string, msgPlaintext, dsmPlaintext []byte) ([]waBinary.Node, bool) { + includeIdentity := false + participantNodes := make([]waBinary.Node, 0, len(allDevices)) + var retryDevices []types.JID + for _, jid := range allDevices { + plaintext := msgPlaintext + if jid.User == ownID.User && dsmPlaintext != nil { + if jid == ownID { + continue + } + plaintext = dsmPlaintext + } + encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, nil) + if errors.Is(err, ErrNoSession) { + retryDevices = append(retryDevices, jid) + continue + } else if err != nil { + cli.Log.Warnf("Failed to encrypt %s for %s: %v", id, jid, err) + continue + } + participantNodes = append(participantNodes, *encrypted) + if isPreKey { + includeIdentity = true + } + } + if len(retryDevices) > 0 { + bundles, err := cli.fetchPreKeys(ctx, retryDevices) + if err != nil { + cli.Log.Warnf("Failed to fetch prekeys for %v to retry encryption: %v", retryDevices, err) + } else { + for _, jid := range retryDevices { + resp := bundles[jid] + if resp.err != nil { + cli.Log.Warnf("Failed to fetch prekey for %s: %v", jid, resp.err) + continue + } + plaintext := msgPlaintext + if jid.User == ownID.User && dsmPlaintext != nil { + plaintext = dsmPlaintext + } + encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, resp.bundle) + if err != nil { + cli.Log.Warnf("Failed to encrypt %s for %s (retry): %v", id, jid, err) + continue + } + participantNodes = append(participantNodes, *encrypted) + if isPreKey { + includeIdentity = true + } + } + } + } + return participantNodes, includeIdentity +} + +func (cli *Client) encryptMessageForDeviceAndWrap(plaintext []byte, to types.JID, bundle *prekey.Bundle) (*waBinary.Node, bool, error) { + node, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, to, bundle) + if err != nil { + return nil, false, err + } + return &waBinary.Node{ + Tag: "to", + Attrs: waBinary.Attrs{"jid": to}, + Content: []waBinary.Node{*node}, + }, includeDeviceIdentity, nil +} + +func (cli *Client) encryptMessageForDevice(plaintext []byte, to types.JID, bundle *prekey.Bundle) (*waBinary.Node, bool, error) { + builder := session.NewBuilderFromSignal(cli.Store, to.SignalAddress(), pbSerializer) + if bundle != nil { + cli.Log.Debugf("Processing prekey bundle for %s", to) + err := builder.ProcessBundle(bundle) + if cli.AutoTrustIdentity && errors.Is(err, signalerror.ErrUntrustedIdentity) { + cli.Log.Warnf("Got %v error while trying to process prekey bundle for %s, clearing stored identity and retrying", err, to) + cli.clearUntrustedIdentity(to) + err = builder.ProcessBundle(bundle) + } + if err != nil { + return nil, false, fmt.Errorf("failed to process prekey bundle: %w", err) + } + } else if !cli.Store.ContainsSession(to.SignalAddress()) { + return nil, false, ErrNoSession + } + cipher := session.NewCipher(builder, to.SignalAddress()) + ciphertext, err := cipher.Encrypt(padMessage(plaintext)) + if err != nil { + return nil, false, fmt.Errorf("cipher encryption failed: %w", err) + } + + encType := "msg" + if ciphertext.Type() == protocol.PREKEY_TYPE { + encType = "pkmsg" + } + + return &waBinary.Node{ + Tag: "enc", + Attrs: waBinary.Attrs{ + "v": "2", + "type": encType, + }, + Content: ciphertext.Serialize(), + }, encType == "pkmsg", nil +} |
