// Copyright (c) 2021 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/hmac" "crypto/sha256" "encoding/binary" "fmt" "time" "go.mau.fi/libsignal/ecc" "go.mau.fi/libsignal/groups" "go.mau.fi/libsignal/keys/prekey" "go.mau.fi/libsignal/protocol" "google.golang.org/protobuf/proto" waBinary "go.mau.fi/whatsmeow/binary" "go.mau.fi/whatsmeow/binary/armadillo/waCommon" "go.mau.fi/whatsmeow/binary/armadillo/waConsumerApplication" "go.mau.fi/whatsmeow/binary/armadillo/waMsgApplication" "go.mau.fi/whatsmeow/binary/armadillo/waMsgTransport" waProto "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" ) // Number of sent messages to cache in memory for handling retry receipts. const recentMessagesSize = 256 type recentMessageKey struct { To types.JID ID types.MessageID } type RecentMessage struct { wa *waProto.Message fb *waMsgApplication.MessageApplication } func (rm RecentMessage) IsEmpty() bool { return rm.wa == nil && rm.fb == nil } func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, wa *waProto.Message, fb *waMsgApplication.MessageApplication) { cli.recentMessagesLock.Lock() key := recentMessageKey{to, id} if cli.recentMessagesList[cli.recentMessagesPtr].ID != "" { delete(cli.recentMessagesMap, cli.recentMessagesList[cli.recentMessagesPtr]) } cli.recentMessagesMap[key] = RecentMessage{wa: wa, fb: fb} cli.recentMessagesList[cli.recentMessagesPtr] = key cli.recentMessagesPtr++ if cli.recentMessagesPtr >= len(cli.recentMessagesList) { cli.recentMessagesPtr = 0 } cli.recentMessagesLock.Unlock() } func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) RecentMessage { cli.recentMessagesLock.RLock() msg, _ := cli.recentMessagesMap[recentMessageKey{to, id}] cli.recentMessagesLock.RUnlock() return msg } func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.MessageID) (RecentMessage, error) { msg := cli.getRecentMessage(receipt.Chat, messageID) if msg.IsEmpty() { waMsg := cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID) if waMsg == nil { return RecentMessage{}, fmt.Errorf("couldn't find message %s", messageID) } else { cli.Log.Debugf("Found message in GetMessageForRetry to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender) } msg = RecentMessage{wa: waMsg} } else { cli.Log.Debugf("Found message in local cache to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender) } return msg, nil } const recreateSessionTimeout = 1 * time.Hour func (cli *Client) shouldRecreateSession(retryCount int, jid types.JID) (reason string, recreate bool) { cli.sessionRecreateHistoryLock.Lock() defer cli.sessionRecreateHistoryLock.Unlock() if !cli.Store.ContainsSession(jid.SignalAddress()) { cli.sessionRecreateHistory[jid] = time.Now() return "we don't have a Signal session with them", true } else if retryCount < 2 { return "", false } prevTime, ok := cli.sessionRecreateHistory[jid] if !ok || prevTime.Add(recreateSessionTimeout).Before(time.Now()) { cli.sessionRecreateHistory[jid] = time.Now() return "retry count > 1 and over an hour since last recreation", true } return "", false } type incomingRetryKey struct { jid types.JID messageID types.MessageID } // handleRetryReceipt handles an incoming retry receipt for an outgoing message. func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.Node) error { retryChild, ok := node.GetOptionalChildByTag("retry") if !ok { return &ElementMissingError{Tag: "retry", In: "retry receipt"} } ag := retryChild.AttrGetter() messageID := ag.String("id") timestamp := ag.UnixTime("t") retryCount := ag.Int("count") if !ag.OK() { return ag.Error() } msg, err := cli.getMessageForRetry(receipt, messageID) if err != nil { return err } var fbConsumerMsg *waConsumerApplication.ConsumerApplication if msg.fb != nil { subProto, ok := msg.fb.GetPayload().GetSubProtocol().GetSubProtocol().(*waMsgApplication.MessageApplication_SubProtocolPayload_ConsumerMessage) if ok { fbConsumerMsg, err = subProto.Decode() if err != nil { return fmt.Errorf("failed to decode consumer message for retry: %w", err) } } } retryKey := incomingRetryKey{receipt.Sender, messageID} cli.incomingRetryRequestCounterLock.Lock() cli.incomingRetryRequestCounter[retryKey]++ internalCounter := cli.incomingRetryRequestCounter[retryKey] cli.incomingRetryRequestCounterLock.Unlock() if internalCounter >= 10 { cli.Log.Warnf("Dropping retry request from %s for %s: internal retry counter is %d", messageID, receipt.Sender, internalCounter) return nil } ownID := cli.getOwnID() if ownID.IsEmpty() { return ErrNotLoggedIn } var fbSKDM *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage var fbDSM *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage if receipt.IsGroup { builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer) senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), ownID.SignalAddress()) signalSKDMessage, err := builder.Create(senderKeyName) if err != nil { cli.Log.Warnf("Failed to create sender key distribution message to include in retry of %s in %s to %s: %v", messageID, receipt.Chat, receipt.Sender, err) } if msg.wa != nil { msg.wa.SenderKeyDistributionMessage = &waProto.SenderKeyDistributionMessage{ GroupId: proto.String(receipt.Chat.String()), AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(), } } else { fbSKDM = &waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage{ GroupID: receipt.Chat.String(), AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(), } } } else if receipt.IsFromMe { if msg.wa != nil { msg.wa = &waProto.Message{ DeviceSentMessage: &waProto.DeviceSentMessage{ DestinationJid: proto.String(receipt.Chat.String()), Message: msg.wa, }, } } else { fbDSM = &waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage{ DestinationJID: receipt.Chat.String(), } } } // TODO pre-retry callback for fb if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, messageID, retryCount, msg.wa) { cli.Log.Debugf("Cancelled retry receipt in PreRetryCallback") return nil } var plaintext, frankingTag []byte if msg.wa != nil { plaintext, err = proto.Marshal(msg.wa) if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } } else { plaintext, err = proto.Marshal(msg.fb) if err != nil { return fmt.Errorf("failed to marshal consumer message: %w", err) } frankingHash := hmac.New(sha256.New, msg.fb.GetMetadata().GetFrankingKey()) frankingHash.Write(plaintext) frankingTag = frankingHash.Sum(nil) } _, hasKeys := node.GetOptionalChildByTag("keys") var bundle *prekey.Bundle if hasKeys { bundle, err = nodeToPreKeyBundle(uint32(receipt.Sender.Device), *node) if err != nil { return fmt.Errorf("failed to read prekey bundle in retry receipt: %w", err) } } else if reason, recreate := cli.shouldRecreateSession(retryCount, receipt.Sender); recreate { cli.Log.Debugf("Fetching prekeys for %s for handling retry receipt with no prekey bundle because %s", receipt.Sender, reason) var keys map[types.JID]preKeyResp keys, err = cli.fetchPreKeys(context.TODO(), []types.JID{receipt.Sender}) if err != nil { return err } bundle, err = keys[receipt.Sender].bundle, keys[receipt.Sender].err if err != nil { return fmt.Errorf("failed to fetch prekeys: %w", err) } else if bundle == nil { return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", receipt.Sender, len(keys)) } } encAttrs := waBinary.Attrs{} var msgAttrs messageAttrs if msg.wa != nil { msgAttrs.MediaType = getMediaTypeFromMessage(msg.wa) msgAttrs.Type = getTypeFromMessage(msg.wa) } else if fbConsumerMsg != nil { msgAttrs = getAttrsFromFBMessage(fbConsumerMsg) } else { msgAttrs.Type = "text" } if msgAttrs.MediaType != "" { encAttrs["mediatype"] = msgAttrs.MediaType } var encrypted *waBinary.Node var includeDeviceIdentity bool if msg.wa != nil { encrypted, includeDeviceIdentity, err = cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle, encAttrs) } else { encrypted, err = cli.encryptMessageForDeviceV3(&waMsgTransport.MessageTransport_Payload{ ApplicationPayload: &waCommon.SubProtocol{ Payload: plaintext, Version: FBMessageApplicationVersion, }, FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER, }, fbSKDM, fbDSM, receipt.Sender, bundle, encAttrs) } if err != nil { return fmt.Errorf("failed to encrypt message for retry: %w", err) } encrypted.Attrs["count"] = retryCount attrs := waBinary.Attrs{ "to": node.Attrs["from"], "type": msgAttrs.Type, "id": messageID, "t": timestamp.Unix(), } if !receipt.IsGroup { attrs["device_fanout"] = false } if participant, ok := node.Attrs["participant"]; ok { attrs["participant"] = participant } if recipient, ok := node.Attrs["recipient"]; ok { attrs["recipient"] = recipient } if edit, ok := node.Attrs["edit"]; ok { attrs["edit"] = edit } var content []waBinary.Node if msg.wa != nil { content = cli.getMessageContent(*encrypted, msg.wa, attrs, includeDeviceIdentity) } else { content = []waBinary.Node{ *encrypted, {Tag: "franking", Content: []waBinary.Node{{Tag: "franking_tag", Content: frankingTag}}}, } } err = cli.sendNode(waBinary.Node{ Tag: "message", Attrs: attrs, Content: content, }) if err != nil { return fmt.Errorf("failed to send retry message: %w", err) } cli.Log.Debugf("Sent retry #%d for %s/%s to %s", retryCount, receipt.Chat, messageID, receipt.Sender) return nil } func (cli *Client) cancelDelayedRequestFromPhone(msgID types.MessageID) { if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil { return } cli.pendingPhoneRerequestsLock.RLock() cancelPendingRequest, ok := cli.pendingPhoneRerequests[msgID] if ok { cancelPendingRequest() } cli.pendingPhoneRerequestsLock.RUnlock() } // RequestFromPhoneDelay specifies how long to wait for the sender to resend the message before requesting from your phone. // This is only used if Client.AutomaticMessageRerequestFromPhone is true. var RequestFromPhoneDelay = 5 * time.Second func (cli *Client) delayedRequestMessageFromPhone(info *types.MessageInfo) { if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil { return } cli.pendingPhoneRerequestsLock.Lock() _, alreadyRequesting := cli.pendingPhoneRerequests[info.ID] if alreadyRequesting { cli.pendingPhoneRerequestsLock.Unlock() return } ctx, cancel := context.WithCancel(context.Background()) defer cancel() cli.pendingPhoneRerequests[info.ID] = cancel cli.pendingPhoneRerequestsLock.Unlock() defer func() { cli.pendingPhoneRerequestsLock.Lock() delete(cli.pendingPhoneRerequests, info.ID) cli.pendingPhoneRerequestsLock.Unlock() }() select { case <-time.After(RequestFromPhoneDelay): case <-ctx.Done(): cli.Log.Debugf("Cancelled delayed request for message %s from phone", info.ID) return } _, err := cli.SendMessage( ctx, cli.getOwnID().ToNonAD(), cli.BuildUnavailableMessageRequest(info.Chat, info.Sender, info.ID), SendRequestExtra{Peer: true}, ) if err != nil { cli.Log.Warnf("Failed to send request for unavailable message %s to phone: %v", info.ID, err) } else { cli.Log.Debugf("Requested message %s from phone", info.ID) } } // sendRetryReceipt sends a retry receipt for an incoming message. func (cli *Client) sendRetryReceipt(node *waBinary.Node, info *types.MessageInfo, forceIncludeIdentity bool) { id, _ := node.Attrs["id"].(string) children := node.GetChildren() var retryCountInMsg int if len(children) == 1 && children[0].Tag == "enc" { retryCountInMsg = children[0].AttrGetter().OptionalInt("count") } cli.messageRetriesLock.Lock() cli.messageRetries[id]++ retryCount := cli.messageRetries[id] // In case the message is a retry response, and we restarted in between, find the count from the message if retryCount == 1 && retryCountInMsg > 0 { retryCount = retryCountInMsg + 1 cli.messageRetries[id] = retryCount } cli.messageRetriesLock.Unlock() if retryCount >= 5 { cli.Log.Warnf("Not sending any more retry receipts for %s", id) return } if retryCount == 1 { go cli.delayedRequestMessageFromPhone(info) } var registrationIDBytes [4]byte binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID) attrs := waBinary.Attrs{ "id": id, "type": "retry", "to": node.Attrs["from"], } if recipient, ok := node.Attrs["recipient"]; ok { attrs["recipient"] = recipient } if participant, ok := node.Attrs["participant"]; ok { attrs["participant"] = participant } payload := waBinary.Node{ Tag: "receipt", Attrs: attrs, Content: []waBinary.Node{ {Tag: "retry", Attrs: waBinary.Attrs{ "count": retryCount, "id": id, "t": node.Attrs["t"], "v": 1, }}, {Tag: "registration", Content: registrationIDBytes[:]}, }, } if retryCount > 1 || forceIncludeIdentity { if key, err := cli.Store.PreKeys.GenOnePreKey(); err != nil { cli.Log.Errorf("Failed to get prekey for retry receipt: %v", err) } else if deviceIdentity, err := proto.Marshal(cli.Store.Account); err != nil { cli.Log.Errorf("Failed to marshal account info: %v", err) return } else { payload.Content = append(payload.GetChildren(), waBinary.Node{ Tag: "keys", Content: []waBinary.Node{ {Tag: "type", Content: []byte{ecc.DjbType}}, {Tag: "identity", Content: cli.Store.IdentityKey.Pub[:]}, preKeyToNode(key), preKeyToNode(cli.Store.SignedPreKey), {Tag: "device-identity", Content: deviceIdentity}, }, }) } } err := cli.sendNode(payload) if err != nil { cli.Log.Errorf("Failed to send retry receipt for %s: %v", id, err) } }