Update dependencies (#1929)

This commit is contained in:
Wim
2022-11-27 00:42:16 +01:00
committed by GitHub
parent 6da9d567dc
commit 4fd0a76727
1126 changed files with 1057766 additions and 1385139 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,12 @@ message DeviceProps {
CATALINA = 12;
TCL_TV = 13;
}
message HistorySyncConfig {
optional uint32 fullSyncDaysLimit = 1;
optional uint32 fullSyncSizeMbLimit = 2;
optional uint32 storageQuotaMb = 3;
}
message AppVersion {
optional uint32 primary = 1;
optional uint32 secondary = 2;
@@ -60,37 +66,48 @@ message DeviceProps {
optional AppVersion version = 2;
optional PlatformType platformType = 3;
optional bool requireFullSync = 4;
optional HistorySyncConfig historySyncConfig = 5;
}
message PollVoteMessage {
repeated bytes selectedOptions = 1;
enum PeerDataOperationRequestType {
UPLOAD_STICKER = 0;
SEND_RECENT_STICKER_BOOTSTRAP = 1;
GENERATE_LINK_PREVIEW = 2;
}
message PollUpdateMessage {
optional MessageKey pollCreationMessageKey = 1;
optional PollEncValue vote = 2;
optional PollUpdateMessageMetadata metadata = 3;
optional int64 senderTimestampMs = 4;
}
message PollUpdateMessageMetadata {
}
message PollEncValue {
optional bytes encPayload = 1;
optional bytes encIv = 2;
}
message PollCreationMessage {
message Option {
optional string optionName = 1;
message PeerDataOperationRequestResponseMessage {
message PeerDataOperationResult {
message LinkPreviewResponse {
optional string url = 1;
optional string title = 2;
optional string description = 3;
optional bytes thumbData = 4;
optional string canonicalUrl = 5;
optional string matchText = 6;
optional string previewType = 7;
}
optional MediaRetryNotification.ResultType mediaUploadResult = 1;
optional StickerMessage stickerMessage = 2;
optional LinkPreviewResponse linkPreviewResponse = 3;
}
optional bytes encKey = 1;
optional string name = 2;
repeated Option options = 3;
optional uint32 selectableOptionsCount = 4;
optional ContextInfo contextInfo = 5;
optional PeerDataOperationRequestType peerDataOperationRequestType = 1;
optional string stanzaId = 2;
repeated PeerDataOperationResult peerDataOperationResult = 3;
}
message PeerDataOperationRequestMessage {
message RequestUrlPreview {
optional string url = 1;
}
message RequestStickerReupload {
optional string fileSha256 = 1;
}
optional PeerDataOperationRequestType peerDataOperationRequestType = 1;
repeated RequestStickerReupload requestStickerReupload = 2;
repeated RequestUrlPreview requestUrlPreview = 3;
}
message PaymentInviteMessage {
@@ -369,6 +386,7 @@ message HistorySyncNotification {
optional uint32 chunkOrder = 7;
optional string originalMessageId = 8;
optional uint32 progress = 9;
optional int64 oldestMsgInChunkTimestampSec = 10;
}
message HighlyStructuredMessage {
@@ -491,6 +509,13 @@ message ExtendedTextMessage {
optional string inviteLinkParentGroupSubjectV2 = 27;
optional bytes inviteLinkParentGroupThumbnailV2 = 28;
optional InviteLinkGroupType inviteLinkGroupTypeV2 = 29;
optional bool viewOnce = 30;
}
message EncReactionMessage {
optional MessageKey targetMessageKey = 1;
optional bytes encPayload = 2;
optional bytes encIv = 3;
}
message DocumentMessage {
@@ -626,6 +651,7 @@ message AudioMessage {
optional ContextInfo contextInfo = 17;
optional bytes streamingSidecar = 18;
optional bytes waveform = 19;
optional fixed32 backgroundArgb = 20;
}
message AppStateSyncKey {
@@ -741,6 +767,7 @@ message ContextInfo {
optional bool containsAutoReply = 10;
optional bool renderLargerThumbnail = 11;
optional bool showAdAttribution = 12;
optional string ctwaClid = 13;
}
message AdReplyInfo {
@@ -778,6 +805,8 @@ message ContextInfo {
optional ActionLink actionLink = 33;
optional string groupSubject = 34;
optional string parentGroupJid = 35;
optional string trustBannerType = 37;
optional uint32 trustBannerAction = 38;
}
message ActionLink {
@@ -894,6 +923,9 @@ message Message {
optional FutureProofMessage documentWithCaptionMessage = 53;
optional RequestPhoneNumberMessage requestPhoneNumberMessage = 54;
optional FutureProofMessage viewOnceMessageV2 = 55;
optional EncReactionMessage encReactionMessage = 56;
optional FutureProofMessage editedMessage = 58;
optional FutureProofMessage viewOnceMessageV2Extension = 59;
}
message MessageContextInfo {
@@ -964,9 +996,11 @@ message TemplateMessage {
optional ContextInfo contextInfo = 3;
optional HydratedFourRowTemplate hydratedTemplate = 4;
optional string templateId = 9;
oneof format {
FourRowTemplate fourRowTemplate = 1;
HydratedFourRowTemplate hydratedFourRowTemplate = 2;
InteractiveMessage interactiveMessageTemplate = 5;
}
}
@@ -999,6 +1033,8 @@ message StickerMessage {
optional bool isAnimated = 13;
optional bytes pngThumbnail = 16;
optional ContextInfo contextInfo = 17;
optional int64 stickerSentTs = 18;
optional bool isAvatar = 19;
}
message SenderKeyDistributionMessage {
@@ -1012,11 +1048,6 @@ message SendPaymentMessage {
optional PaymentBackground background = 4;
}
enum RmrSource {
FAVORITE_STICKER = 0;
RECENT_STICKER = 1;
RECENT_STICKER_INIT = 2;
}
message RequestPhoneNumberMessage {
optional ContextInfo contextInfo = 1;
}
@@ -1031,23 +1062,6 @@ message RequestPaymentMessage {
optional PaymentBackground background = 7;
}
message RequestMediaUploadResponseMessage {
message RequestMediaUploadResult {
optional string fileSha256 = 1;
optional MediaRetryNotification.ResultType mediaUploadResult = 2;
optional StickerMessage stickerMessage = 3;
}
optional RmrSource rmrSource = 1;
optional string stanzaId = 2;
repeated RequestMediaUploadResult reuploadResult = 3;
}
message RequestMediaUploadMessage {
repeated string fileSha256 = 1;
optional RmrSource rmrSource = 2;
}
message ReactionMessage {
optional MessageKey key = 1;
optional string text = 2;
@@ -1067,8 +1081,9 @@ message ProtocolMessage {
INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC = 9;
APP_STATE_FATAL_EXCEPTION_NOTIFICATION = 10;
SHARE_PHONE_NUMBER = 11;
REQUEST_MEDIA_UPLOAD_MESSAGE = 12;
REQUEST_MEDIA_UPLOAD_RESPONSE_MESSAGE = 13;
MESSAGE_EDIT = 14;
PEER_DATA_OPERATION_REQUEST_MESSAGE = 16;
PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE = 17;
}
optional MessageKey key = 1;
optional Type type = 2;
@@ -1080,8 +1095,10 @@ message ProtocolMessage {
optional InitialSecurityNotificationSettingSync initialSecurityNotificationSettingSync = 9;
optional AppStateFatalExceptionNotification appStateFatalExceptionNotification = 10;
optional DisappearingMode disappearingMode = 11;
optional RequestMediaUploadMessage requestMediaUploadMessage = 12;
optional RequestMediaUploadResponseMessage requestMediaUploadResponseMessage = 13;
optional Message editedMessage = 14;
optional int64 timestampMs = 15;
optional PeerDataOperationRequestMessage peerDataOperationRequestMessage = 16;
optional PeerDataOperationRequestResponseMessage peerDataOperationRequestResponseMessage = 17;
}
message ProductMessage {
@@ -1113,6 +1130,37 @@ message ProductMessage {
optional ContextInfo contextInfo = 17;
}
message PollVoteMessage {
repeated bytes selectedOptions = 1;
}
message PollUpdateMessage {
optional MessageKey pollCreationMessageKey = 1;
optional PollEncValue vote = 2;
optional PollUpdateMessageMetadata metadata = 3;
optional int64 senderTimestampMs = 4;
}
message PollUpdateMessageMetadata {
}
message PollEncValue {
optional bytes encPayload = 1;
optional bytes encIv = 2;
}
message PollCreationMessage {
message Option {
optional string optionName = 1;
}
optional bytes encKey = 1;
optional string name = 2;
repeated Option options = 3;
optional uint32 selectableOptionsCount = 4;
optional ContextInfo contextInfo = 5;
}
message EphemeralSetting {
optional sfixed32 duration = 1;
optional sfixed64 timestamp = 2;
@@ -1210,6 +1258,7 @@ message GlobalSettings {
optional bool showGroupNotificationsPreview = 8;
optional int32 disappearingModeDuration = 9;
optional int64 disappearingModeTimestamp = 10;
optional AvatarUserSettings avatarUserSettings = 11;
}
message Conversation {
@@ -1256,7 +1305,14 @@ message Conversation {
optional string parentGroupId = 37;
optional string displayName = 38;
optional string pnJid = 39;
optional bool selfPnExposed = 40;
optional bool shareOwnPn = 40;
optional bool pnhDuplicateLidThread = 41;
optional string lidJid = 42;
}
message AvatarUserSettings {
optional string fbid = 1;
optional string password = 2;
}
message AutoDownloadSettings {
@@ -1300,10 +1356,13 @@ message MsgOpaqueData {
repeated PollOption pollOptions = 18;
optional uint32 pollSelectableOptionsCount = 20;
optional bytes messageSecret = 21;
optional string originalSelfAuthorJid = 51;
optional string originalSelfAuthor = 51;
optional int64 senderTimestampMs = 22;
optional string pollUpdateParentKey = 23;
optional PollEncValue encPollVote = 24;
optional string encReactionTargetMessageKey = 25;
optional bytes encReactionEncPayload = 26;
optional bytes encReactionEncIv = 27;
}
message ServerErrorReceipt {
@@ -1433,6 +1492,9 @@ message SyncActionValue {
optional NuxAction nuxAction = 31;
optional PrimaryVersionAction primaryVersionAction = 32;
optional StickerAction stickerAction = 33;
optional RemoveRecentStickerAction removeRecentStickerAction = 34;
optional ChatAssignmentAction chatAssignment = 35;
optional ChatAssignmentOpenedStatusAction chatAssignmentOpenedStatus = 36;
}
message UserStatusMuteAction {
@@ -1524,6 +1586,7 @@ message NuxAction {
message MuteAction {
optional bool muted = 1;
optional int64 muteEndTimestamp = 2;
optional bool autoMuted = 3;
}
message MarkChatAsReadAction {
@@ -1562,12 +1625,21 @@ message DeleteChatAction {
message ContactAction {
optional string fullName = 1;
optional string firstName = 2;
optional string lidJid = 3;
}
message ClearChatAction {
optional SyncActionMessageRange messageRange = 1;
}
message ChatAssignmentOpenedStatusAction {
optional bool chatOpened = 1;
}
message ChatAssignmentAction {
optional string deviceAgentID = 1;
}
message ArchiveChatAction {
optional bool archived = 1;
optional SyncActionMessageRange messageRange = 2;
@@ -1748,6 +1820,9 @@ message ClientPayload {
OCULUS_CALL = 26;
MILAN = 27;
CAPI = 28;
WEAROS = 29;
ARDEVICE = 30;
VRDEVICE = 31;
}
message AppVersion {
optional uint32 primary = 1;
@@ -1829,6 +1904,11 @@ message ClientPayload {
NETWORK_SWITCH = 4;
PING_RECONNECT = 5;
}
enum BizMarketSegment {
DEFAULT = 0;
DEVX = 1;
INBOX = 2;
}
optional uint64 username = 1;
optional bool passive = 3;
optional UserAgent userAgent = 5;
@@ -1853,6 +1933,9 @@ message ClientPayload {
optional bytes fbDeviceId = 32;
optional bool pull = 33;
optional bytes paddingBytes = 34;
optional BizMarketSegment bizMarketSegment = 35;
optional int32 yearClass = 36;
optional int32 memClass = 37;
}
message WebNotificationsInfo {
@@ -2016,6 +2099,16 @@ message WebMessageInfo {
COMMUNITY_PARENT_GROUP_DELETED = 149;
COMMUNITY_LINK_PARENT_GROUP_MEMBERSHIP_APPROVAL = 150;
GROUP_PARTICIPANT_JOINED_GROUP_AND_PARENT_GROUP = 151;
MASKED_THREAD_CREATED = 152;
MASKED_THREAD_UNMASKED = 153;
BIZ_CHAT_ASSIGNMENT = 154;
CHAT_PSA = 155;
CHAT_POLL_CREATION_MESSAGE = 156;
CAG_MASKED_THREAD_CREATED = 157;
COMMUNITY_PARENT_GROUP_SUBJECT_CHANGED = 158;
CAG_INVITE_AUTO_ADD = 159;
BIZ_CHAT_ASSIGNMENT_UNASSIGN = 160;
CAG_INVITE_AUTO_JOINED = 161;
}
enum Status {
ERROR = 0;
@@ -2252,6 +2345,8 @@ message KeepInChat {
optional int64 serverTimestamp = 2;
optional MessageKey key = 3;
optional string deviceJid = 4;
optional int64 clientTimestampMs = 5;
optional int64 serverTimestampMs = 6;
}
message NoiseCertificate {

View File

@@ -135,16 +135,17 @@ const handlerQueueSize = 2048
// The logger can be nil, it will default to a no-op logger.
//
// The device store must be set. A default SQL-backed implementation is available in the store/sqlstore package.
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
// if err != nil {
// panic(err)
// }
// // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
// deviceStore, err := container.GetFirstDevice()
// if err != nil {
// panic(err)
// }
// client := whatsmeow.NewClient(deviceStore, nil)
//
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
// if err != nil {
// panic(err)
// }
// // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
// deviceStore, err := container.GetFirstDevice()
// if err != nil {
// panic(err)
// }
// client := whatsmeow.NewClient(deviceStore, nil)
func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
if log == nil {
log = waLog.Noop
@@ -218,16 +219,18 @@ func (cli *Client) SetProxyAddress(addr string) error {
// By default, the client will find the proxy from the https_proxy environment variable like Go's net/http does.
//
// To disable reading proxy info from environment variables, explicitly set the proxy to nil:
// cli.SetProxy(nil)
//
// cli.SetProxy(nil)
//
// To use a different proxy for the websocket and media, pass a function that checks the request path or headers:
// cli.SetProxy(func(r *http.Request) (*url.URL, error) {
// if r.URL.Host == "web.whatsapp.com" && r.URL.Path == "/ws/chat" {
// return websocketProxyURL, nil
// } else {
// return mediaProxyURL, nil
// }
// })
//
// cli.SetProxy(func(r *http.Request) (*url.URL, error) {
// if r.URL.Host == "web.whatsapp.com" && r.URL.Path == "/ws/chat" {
// return websocketProxyURL, nil
// } else {
// return mediaProxyURL, nil
// }
// })
func (cli *Client) SetProxy(proxy socket.Proxy) {
cli.proxy = proxy
cli.http.Transport.(*http.Transport).Proxy = proxy
@@ -421,29 +424,31 @@ func (cli *Client) Logout() error {
//
// All registered event handlers will receive all events. You should use a type switch statement to
// filter the events you want:
// func myEventHandler(evt interface{}) {
// switch v := evt.(type) {
// case *events.Message:
// fmt.Println("Received a message!")
// case *events.Receipt:
// fmt.Println("Received a receipt!")
// }
// }
//
// func myEventHandler(evt interface{}) {
// switch v := evt.(type) {
// case *events.Message:
// fmt.Println("Received a message!")
// case *events.Receipt:
// fmt.Println("Received a receipt!")
// }
// }
//
// If you want to access the Client instance inside the event handler, the recommended way is to
// wrap the whole handler in another struct:
// type MyClient struct {
// WAClient *whatsmeow.Client
// eventHandlerID uint32
// }
//
// func (mycli *MyClient) register() {
// mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler)
// }
// type MyClient struct {
// WAClient *whatsmeow.Client
// eventHandlerID uint32
// }
//
// func (mycli *MyClient) myEventHandler(evt interface{}) {
// // Handle event and access mycli.WAClient
// }
// func (mycli *MyClient) register() {
// mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler)
// }
//
// func (mycli *MyClient) myEventHandler(evt interface{}) {
// // Handle event and access mycli.WAClient
// }
func (cli *Client) AddEventHandler(handler EventHandler) uint32 {
nextID := atomic.AddUint32(&nextHandlerID, 1)
cli.eventHandlersLock.Lock()
@@ -458,11 +463,12 @@ func (cli *Client) AddEventHandler(handler EventHandler) uint32 {
// N.B. Do not run this directly from an event handler. That would cause a deadlock because the
// event dispatcher holds a read lock on the event handler list, and this method wants a write lock
// on the same list. Instead run it in a goroutine:
// func (mycli *MyClient) myEventHandler(evt interface{}) {
// if noLongerWantEvents {
// go mycli.WAClient.RemoveEventHandler(mycli.eventHandlerID)
// }
// }
//
// func (mycli *MyClient) myEventHandler(evt interface{}) {
// if noLongerWantEvents {
// go mycli.WAClient.RemoveEventHandler(mycli.eventHandlerID)
// }
// }
func (cli *Client) RemoveEventHandler(id uint32) bool {
cli.eventHandlersLock.Lock()
defer cli.eventHandlersLock.Unlock()
@@ -575,11 +581,12 @@ func (cli *Client) dispatchEvent(evt interface{}) {
// ParseWebMessage parses a WebMessageInfo object into *events.Message to match what real-time messages have.
//
// The chat JID can be found in the Conversation data:
// chatJID, err := types.ParseJID(conv.GetId())
// for _, historyMsg := range conv.GetMessages() {
// evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
// yourNormalEventHandler(evt)
// }
//
// chatJID, err := types.ParseJID(conv.GetId())
// for _, historyMsg := range conv.GetMessages() {
// evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
// yourNormalEventHandler(evt)
// }
func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessageInfo) (*events.Message, error) {
info := types.MessageInfo{
MessageSource: types.MessageSource{

View File

@@ -151,9 +151,10 @@ func getSize(msg DownloadableMessage) int {
// DownloadThumbnail downloads a thumbnail from a message.
//
// This is primarily intended for downloading link preview thumbnails, which are in ExtendedTextMessage:
// var msg *waProto.Message
// ...
// thumbnailImageBytes, err := cli.DownloadThumbnail(msg.GetExtendedTextMessage())
//
// var msg *waProto.Message
// ...
// thumbnailImageBytes, err := cli.DownloadThumbnail(msg.GetExtendedTextMessage())
func (cli *Client) DownloadThumbnail(msg DownloadableThumbnail) ([]byte, error) {
mediaType, ok := classToThumbnailMediaType[msg.ProtoReflect().Descriptor().Name()]
if !ok {
@@ -173,9 +174,10 @@ func GetMediaType(msg DownloadableMessage) MediaType {
// Download downloads the attachment from the given protobuf message.
//
// The attachment is a specific part of a Message protobuf struct, not the message itself, e.g.
// var msg *waProto.Message
// ...
// imageData, err := cli.Download(msg.GetImageMessage())
//
// var msg *waProto.Message
// ...
// imageData, err := cli.Download(msg.GetImageMessage())
//
// You can also use DownloadAny to download the first non-nil sub-message.
func (cli *Client) Download(msg DownloadableMessage) ([]byte, error) {

View File

@@ -48,6 +48,8 @@ var (
ErrInviteLinkRevoked = errors.New("that group invite link has been revoked")
// ErrBusinessMessageLinkNotFound is returned by ResolveBusinessMessageLink if the link doesn't exist or has been revoked.
ErrBusinessMessageLinkNotFound = errors.New("that business message link does not exist or has been revoked")
// ErrContactQRLinkNotFound is returned by ResolveContactQRLink if the link doesn't exist or has been revoked.
ErrContactQRLinkNotFound = errors.New("that contact QR link does not exist or has been revoked")
// ErrInvalidImageFormat is returned by SetGroupPhoto if the given photo is not in the correct format.
ErrInvalidImageFormat = errors.New("the given data is not a valid image")
// ErrMediaNotAvailableOnPhone is returned by DecryptMediaRetryNotification if the given event contains error code 2.
@@ -79,6 +81,12 @@ var (
ErrNothingDownloadableFound = errors.New("didn't find any attachments in message")
)
var (
ErrOriginalMessageSecretNotFound = errors.New("original message secret key not found")
ErrNotEncryptedReactionMessage = errors.New("given message isn't an encrypted reaction message")
ErrNotPollUpdateMessage = errors.New("given message isn't a poll update message")
)
type wrappedIQError struct {
HumanError error
IQError error

View File

@@ -175,16 +175,24 @@ func (cli *Client) SetGroupTopic(jid types.JID, previousID, newID, topic string)
if newID == "" {
newID = GenerateMessageID()
}
attrs := waBinary.Attrs{
"id": newID,
}
if previousID != "" {
attrs["prev"] = previousID
}
content := []waBinary.Node{{
Tag: "body",
Content: []byte(topic),
}}
if len(topic) == 0 {
attrs["delete"] = "true"
content = nil
}
_, err := cli.sendGroupIQ(context.TODO(), iqSet, jid, waBinary.Node{
Tag: "description",
Attrs: waBinary.Attrs{
"prev": previousID,
"id": newID,
},
Content: []waBinary.Node{{
Tag: "body",
Content: []byte(topic),
}},
Tag: "description",
Attrs: attrs,
Content: content,
})
return err
}
@@ -525,20 +533,26 @@ func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, err
NameSetBy: cag.OptionalJIDOrEmpty("s_o"),
}
case "description":
topicChild := child.GetChildByTag("body")
topicBytes, ok := topicChild.Content.([]byte)
if !ok {
return nil, fmt.Errorf("group change description has unexpected body: %s", topicChild.XMLString())
var topicStr string
_, isDelete := child.GetOptionalChildByTag("delete")
if !isDelete {
topicChild := child.GetChildByTag("body")
topicBytes, ok := topicChild.Content.([]byte)
if !ok {
return nil, fmt.Errorf("group change description has unexpected body: %s", topicChild.XMLString())
}
topicStr = string(topicBytes)
}
var setBy types.JID
if evt.Sender != nil {
setBy = *evt.Sender
}
evt.Topic = &types.GroupTopic{
Topic: string(topicBytes),
TopicID: cag.String("id"),
TopicSetAt: evt.Timestamp,
TopicSetBy: setBy,
Topic: topicStr,
TopicID: cag.String("id"),
TopicSetAt: evt.Timestamp,
TopicSetBy: setBy,
TopicDeleted: isDelete,
}
case "announcement":
evt.Announce = &types.GroupAnnounce{

View File

@@ -7,8 +7,6 @@
package whatsmeow
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
@@ -18,6 +16,7 @@ import (
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"
)
@@ -25,18 +24,6 @@ func getMediaRetryKey(mediaKey []byte) (cipherKey []byte) {
return hkdfutil.SHA256(mediaKey, nil, []byte("WhatsApp Media Retry Notification"), 32)
}
func prepareMediaRetryGCM(mediaKey []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(getMediaRetryKey(mediaKey))
if err != nil {
return nil, fmt.Errorf("failed to initialize AES cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to initialize GCM: %w", err)
}
return gcm, nil
}
func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphertext, iv []byte, err error) {
receipt := &waProto.ServerErrorReceipt{
StanzaId: proto.String(messageID),
@@ -47,17 +34,12 @@ func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphe
err = fmt.Errorf("failed to marshal payload: %w", err)
return
}
var gcm cipher.AEAD
gcm, err = prepareMediaRetryGCM(mediaKey)
if err != nil {
return
}
iv = make([]byte, 12)
_, err = rand.Read(iv)
if err != nil {
panic(err)
}
ciphertext = gcm.Seal(plaintext[:0], iv, plaintext, []byte(messageID))
ciphertext, err = gcmutil.Encrypt(getMediaRetryKey(mediaKey), iv, plaintext, []byte(messageID))
return
}
@@ -66,35 +48,35 @@ func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphe
// This is mostly relevant when handling history syncs and getting a 404 or 410 error downloading media.
// Rough example on how to use it (will not work out of the box, you must adjust it depending on what you need exactly):
//
// var mediaRetryCache map[types.MessageID]*waProto.ImageMessage
// var mediaRetryCache map[types.MessageID]*waProto.ImageMessage
//
// evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
// imageMsg := evt.Message.GetImageMessage() // replace this with the part of the message you want to download
// data, err := cli.Download(imageMsg)
// if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
// err = cli.SendMediaRetryReceipt(&evt.Info, imageMsg.GetMediaKey())
// // You need to store the event data somewhere as it's necessary for handling the retry response.
// mediaRetryCache[evt.Info.ID] = imageMsg
// }
// evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
// imageMsg := evt.Message.GetImageMessage() // replace this with the part of the message you want to download
// data, err := cli.Download(imageMsg)
// if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
// err = cli.SendMediaRetryReceipt(&evt.Info, imageMsg.GetMediaKey())
// // You need to store the event data somewhere as it's necessary for handling the retry response.
// mediaRetryCache[evt.Info.ID] = imageMsg
// }
//
// The response will come as an *events.MediaRetry. The response will then have to be decrypted
// using DecryptMediaRetryNotification and the same media key passed here. If the media retry was successful,
// the decrypted notification should contain an updated DirectPath, which can be used to download the file.
//
// func eventHandler(rawEvt interface{}) {
// switch evt := rawEvt.(type) {
// case *events.MediaRetry:
// imageMsg := mediaRetryCache[evt.MessageID]
// retryData, err := whatsmeow.DecryptMediaRetryNotification(evt, imageMsg.GetMediaKey())
// if err != nil || retryData.GetResult != waProto.MediaRetryNotification_SUCCESS {
// return
// }
// // Use the new path to download the attachment
// imageMsg.DirectPath = retryData.DirectPath
// data, err := cli.Download(imageMsg)
// // Alternatively, you can use cli.DownloadMediaWithPath and provide the individual fields manually.
// }
// }
// func eventHandler(rawEvt interface{}) {
// switch evt := rawEvt.(type) {
// case *events.MediaRetry:
// imageMsg := mediaRetryCache[evt.MessageID]
// retryData, err := whatsmeow.DecryptMediaRetryNotification(evt, imageMsg.GetMediaKey())
// if err != nil || retryData.GetResult != waProto.MediaRetryNotification_SUCCESS {
// return
// }
// // Use the new path to download the attachment
// imageMsg.DirectPath = retryData.DirectPath
// data, err := cli.Download(imageMsg)
// // Alternatively, you can use cli.DownloadMediaWithPath and provide the individual fields manually.
// }
// }
func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []byte) error {
ciphertext, iv, err := encryptMediaRetryReceipt(message.ID, mediaKey)
if err != nil {
@@ -136,15 +118,12 @@ func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []
// See Client.SendMediaRetryReceipt for more info on how to use this.
func DecryptMediaRetryNotification(evt *events.MediaRetry, mediaKey []byte) (*waProto.MediaRetryNotification, error) {
var notif waProto.MediaRetryNotification
var plaintext []byte
if evt.Error != nil && evt.Ciphertext == nil {
if evt.Error.Code == 2 {
return nil, ErrMediaNotAvailableOnPhone
}
return nil, fmt.Errorf("%w (code: %d)", ErrUnknownMediaRetryError, evt.Error.Code)
} else if gcm, err := prepareMediaRetryGCM(mediaKey); err != nil {
return nil, err
} else if plaintext, err = gcm.Open(nil, evt.IV, evt.Ciphertext, []byte(evt.MessageID)); err != nil {
} else if plaintext, err := gcmutil.Decrypt(getMediaRetryKey(mediaKey), evt.IV, evt.Ciphertext, []byte(evt.MessageID)); err != nil {
return nil, fmt.Errorf("failed to decrypt notification: %w", err)
} else if err = proto.Unmarshal(plaintext, &notif); err != nil {
return nil, fmt.Errorf("failed to unmarshal notification (invalid encryption key?): %w", err)

View File

@@ -312,6 +312,8 @@ func (cli *Client) handleHistorySyncNotification(notif *waProto.HistorySyncNotif
cli.Log.Debugf("Received history sync (type %s, chunk %d)", historySync.GetSyncType(), historySync.GetChunkOrder())
if historySync.GetSyncType() == waProto.HistorySync_PUSH_NAME {
go cli.handleHistoricalPushNames(historySync.GetPushnames())
} else if len(historySync.GetConversations()) > 0 {
go cli.storeHistoricalMessageSecrets(historySync.GetConversations())
}
cli.dispatchEvent(&events.HistorySync{
Data: &historySync,
@@ -387,10 +389,63 @@ func (cli *Client) processProtocolParts(info *types.MessageInfo, msg *waProto.Me
cli.handleSenderKeyDistributionMessage(info.Chat, info.Sender, msg.SenderKeyDistributionMessage)
}
}
// N.B. Edits are protocol messages, but they're also wrapped inside EditedMessage,
// which is only unwrapped after processProtocolParts, so this won't trigger for edits.
if msg.GetProtocolMessage() != nil {
cli.handleProtocolMessage(info, msg)
}
if msgSecret := msg.GetMessageContextInfo().GetMessageSecret(); len(msgSecret) > 0 {
err := cli.Store.MsgSecrets.PutMessageSecret(info.Chat, info.Sender, info.ID, msgSecret)
if err != nil {
cli.Log.Errorf("Failed to store message secret key for %s: %v", info.ID, err)
} else {
cli.Log.Debugf("Stored message secret key for %s", info.ID)
}
}
}
func (cli *Client) storeHistoricalMessageSecrets(conversations []*waProto.Conversation) {
var secrets []store.MessageSecretInsert
me := cli.Store.ID.ToNonAD()
for _, conv := range conversations {
chatJID, _ := types.ParseJID(conv.GetId())
if chatJID.IsEmpty() {
continue
}
for _, msg := range conv.GetMessages() {
if secret := msg.GetMessage().GetMessageSecret(); secret != nil {
var senderJID types.JID
msgKey := msg.GetMessage().GetKey()
if msgKey.GetFromMe() {
senderJID = me
} else if chatJID.Server == types.DefaultUserServer {
senderJID = chatJID
} else if msgKey.GetParticipant() != "" {
senderJID, _ = types.ParseJID(msgKey.GetParticipant())
} else if msg.GetMessage().GetParticipant() != "" {
senderJID, _ = types.ParseJID(msg.GetMessage().GetParticipant())
}
if senderJID.IsEmpty() || msgKey.GetId() == "" {
continue
}
secrets = append(secrets, store.MessageSecretInsert{
Chat: chatJID,
Sender: senderJID,
ID: msgKey.GetId(),
Secret: secret,
})
}
}
}
if len(secrets) > 0 {
cli.Log.Debugf("Storing %d message secret keys in history sync", len(secrets))
err := cli.Store.MsgSecrets.PutMessageSecrets(secrets)
if err != nil {
cli.Log.Errorf("Failed to store message secret keys in history sync: %v", err)
} else {
cli.Log.Infof("Stored %d message secret keys from history sync", len(secrets))
}
}
}
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message) {

267
vendor/go.mau.fi/whatsmeow/msgsecret.go vendored Normal file
View 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
}

View File

@@ -84,7 +84,8 @@ func (cli *Client) SendPresence(state types.Presence) error {
//
// Also, it seems that the WhatsApp servers require you to be online to receive presence status from other users,
// so you should mark yourself as online before trying to use this function:
// cli.SendPresence(types.PresenceAvailable)
//
// cli.SendPresence(types.PresenceAvailable)
func (cli *Client) SubscribePresence(jid types.JID) error {
return cli.sendNode(waBinary.Node{
Tag: "presence",

View File

@@ -164,7 +164,8 @@ func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, se
// web works when it's not in the foreground.
//
// To mark the client as online, use
// cli.SendPresence(types.PresenceAvailable)
//
// cli.SendPresence(types.PresenceAvailable)
//
// Note that if you turn this off (i.e. call SetForceActiveDeliveryReceipts(false)),
// receipts will act like the client is offline until SendPresence is called again.

View File

@@ -34,8 +34,8 @@ import (
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
//
// msgID := whatsmeow.GenerateMessageID()
// cli.SendMessage(context.Background(), targetJID, msgID, &waProto.Message{...})
// msgID := whatsmeow.GenerateMessageID()
// cli.SendMessage(context.Background(), targetJID, msgID, &waProto.Message{...})
func GenerateMessageID() types.MessageID {
id := make([]byte, 8)
_, err := rand.Read(id)
@@ -80,9 +80,10 @@ type SendResponse 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!"),
// })
//
// 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.
@@ -114,6 +115,14 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.Messa
if !isPeerMessage {
cli.addRecentMessage(to, id, message)
}
if message.GetMessageContextInfo().GetMessageSecret() != nil {
err = cli.Store.MsgSecrets.PutMessageSecret(to, *cli.Store.ID, id, message.GetMessageContextInfo().GetMessageSecret())
if err != nil {
cli.Log.Warnf("Failed to store message secret key for outgoing message %s: %v", id, err)
} else {
cli.Log.Debugf("Stored message secret key for outgoing message %s", id)
}
}
var phash string
var data []byte
switch to.Server {
@@ -163,21 +172,68 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.Messa
}
// RevokeMessage deletes the given message from everyone in the chat.
// You can only revoke your own messages, and if the message is too old, then other users will ignore the deletion.
//
// 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, "", &waProto.Message{
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.Store.ID.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: &waProto.MessageKey{
FromMe: proto.Bool(true),
Id: proto.String(id),
RemoteJid: proto.String(chat.String()),
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 (
@@ -362,10 +418,16 @@ 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
@@ -376,17 +438,36 @@ func getTypeFromMessage(msg *waProto.Message) string {
}
}
const (
EditAttributeEmpty = ""
EditAttributeMessageEdit = "1"
EditAttributeSenderRevoke = "7"
EditAttributeAdminRevoke = "8"
)
const RemoveReactionText = ""
func getEditAttribute(msg *waProto.Message) string {
if msg.ProtocolMessage != nil && msg.GetProtocolMessage().GetType() == waProto.ProtocolMessage_REVOKE && msg.GetProtocolMessage().GetKey() != nil {
if msg.GetProtocolMessage().GetKey().GetFromMe() {
return "7"
} else {
return "8"
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
}
}
} else if msg.ReactionMessage != nil && msg.ReactionMessage.GetText() == "" {
return "7"
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 ""
return EditAttributeEmpty
}
func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (*waBinary.Node, error) {
@@ -450,6 +531,18 @@ func (cli *Client) prepareMessageNode(ctx context.Context, to types.JID, id type
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,

View File

@@ -7,7 +7,6 @@
package socket
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"fmt"
@@ -16,6 +15,8 @@ import (
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"go.mau.fi/whatsmeow/util/gcmutil"
)
type NoiseHandshake struct {
@@ -29,18 +30,6 @@ func NewNoiseHandshake() *NoiseHandshake {
return &NoiseHandshake{}
}
func newCipher(key []byte) (cipher.AEAD, error) {
aesCipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCM(aesCipher)
if err != nil {
return nil, err
}
return aesGCM, nil
}
func sha256Slice(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
@@ -55,7 +44,7 @@ func (nh *NoiseHandshake) Start(pattern string, header []byte) {
}
nh.salt = nh.hash
var err error
nh.key, err = newCipher(nh.hash)
nh.key, err = gcmutil.Prepare(nh.hash)
if err != nil {
panic(err)
}
@@ -88,9 +77,9 @@ func (nh *NoiseHandshake) Decrypt(ciphertext []byte) (plaintext []byte, err erro
func (nh *NoiseHandshake) Finish(fs *FrameSocket, frameHandler FrameHandler, disconnectHandler DisconnectHandler) (*NoiseSocket, error) {
if write, read, err := nh.extractAndExpand(nh.salt, nil); err != nil {
return nil, fmt.Errorf("failed to extract final keys: %w", err)
} else if writeKey, err := newCipher(write); err != nil {
} else if writeKey, err := gcmutil.Prepare(write); err != nil {
return nil, fmt.Errorf("failed to create final write cipher: %w", err)
} else if readKey, err := newCipher(read); err != nil {
} else if readKey, err := gcmutil.Prepare(read); err != nil {
return nil, fmt.Errorf("failed to create final read cipher: %w", err)
} else if ns, err := newNoiseSocket(fs, writeKey, readKey, frameHandler, disconnectHandler); err != nil {
return nil, fmt.Errorf("failed to create noise socket: %w", err)
@@ -114,7 +103,7 @@ func (nh *NoiseHandshake) MixIntoKey(data []byte) error {
return fmt.Errorf("failed to extract keys for mixing: %w", err)
}
nh.salt = write
nh.key, err = newCipher(read)
nh.key, err = gcmutil.Prepare(read)
if err != nil {
return fmt.Errorf("failed to create new cipher while mixing keys: %w", err)
}

View File

@@ -47,6 +47,10 @@ func newNoiseSocket(fs *FrameSocket, writeKey, readKey cipher.AEAD, frameHandler
}
func (ns *NoiseSocket) consumeFrames(ctx context.Context, frames <-chan []byte) {
if ctx == nil {
// ctx being nil implies the connection already closed somehow
return
}
ctxDone := ctx.Done()
for {
select {

View File

@@ -74,7 +74,7 @@ func (vc WAVersionContainer) ProtoAppVersion() *waProto.ClientPayload_UserAgent_
}
// waVersion is the WhatsApp web client version
var waVersion = WAVersionContainer{2, 2230, 10}
var waVersion = WAVersionContainer{2, 2245, 9}
// waVersionHash is the md5 hash of a dot-separated waVersion
var waVersionHash [16]byte

View File

@@ -38,7 +38,8 @@ var _ store.DeviceContainer = (*Container)(nil)
// The logger can be nil and will default to a no-op logger.
//
// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`:
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
//
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
func New(dialect, address string, log waLog.Logger) (*Container, error) {
db, err := sql.Open(dialect, address)
if err != nil {
@@ -59,11 +60,12 @@ func New(dialect, address string, log waLog.Logger) (*Container, error) {
// The logger can be nil and will default to a no-op logger.
//
// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`:
// db, err := sql.Open("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on")
// if err != nil {
// panic(err)
// }
// container, err := sqlstore.NewWithDB(db, "sqlite3", nil)
//
// db, err := sql.Open("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on")
// if err != nil {
// panic(err)
// }
// container, err := sqlstore.NewWithDB(db, "sqlite3", nil)
func NewWithDB(db *sql.DB, dialect string, log waLog.Logger) *Container {
if log == nil {
log = waLog.Noop
@@ -123,6 +125,7 @@ func (c *Container) scanDevice(row scannable) (*store.Device, error) {
device.AppState = innerStore
device.Contacts = innerStore
device.ChatSettings = innerStore
device.MsgSecrets = innerStore
device.Container = c
device.Initialized = true
@@ -236,6 +239,7 @@ func (c *Container) PutDevice(device *store.Device) error {
device.AppState = innerStore
device.Contacts = innerStore
device.ChatSettings = innerStore
device.MsgSecrets = innerStore
device.Initialized = true
}
return err

View File

@@ -28,7 +28,8 @@ var ErrInvalidLength = errors.New("database returned byte array with illegal len
// PostgresArrayWrapper is a function to wrap array values before passing them to the sql package.
//
// When using github.com/lib/pq, you should set
// whatsmeow.PostgresArrayWrapper = pq.Array
//
// whatsmeow.PostgresArrayWrapper = pq.Array
var PostgresArrayWrapper func(interface{}) interface {
driver.Valuer
sql.Scanner
@@ -687,3 +688,42 @@ func (s *SQLStore) GetChatSettings(chat types.JID) (settings types.LocalChatSett
}
return
}
const (
putMsgSecret = `
INSERT INTO whatsmeow_message_secrets (our_jid, chat_jid, sender_jid, message_id, key)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (our_jid, chat_jid, sender_jid, message_id) DO NOTHING
`
getMsgSecret = `
SELECT key FROM whatsmeow_message_secrets WHERE our_jid=$1 AND chat_jid=$2 AND sender_jid=$3 AND message_id=$4
`
)
func (s *SQLStore) PutMessageSecrets(inserts []store.MessageSecretInsert) (err error) {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
for _, insert := range inserts {
_, err = s.db.Exec(putMsgSecret, s.JID, insert.Chat.ToNonAD(), insert.Sender.ToNonAD(), insert.ID, insert.Secret)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return
}
func (s *SQLStore) PutMessageSecret(chat, sender types.JID, id types.MessageID, secret []byte) (err error) {
_, err = s.db.Exec(putMsgSecret, s.JID, chat.ToNonAD(), sender.ToNonAD(), id, secret)
return
}
func (s *SQLStore) GetMessageSecret(chat, sender types.JID, id types.MessageID) (secret []byte, err error) {
err = s.db.QueryRow(getMsgSecret, s.JID, chat.ToNonAD(), sender.ToNonAD(), id).Scan(&secret)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}

View File

@@ -16,7 +16,7 @@ type upgradeFunc func(*sql.Tx, *Container) error
//
// This may be of use if you want to manage the database fully manually, but in most cases you
// should just call Container.Upgrade to let the library handle everything.
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2}
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2, upgradeV3}
func (c *Container) getVersion() (int, error) {
_, err := c.db.Exec("CREATE TABLE IF NOT EXISTS whatsmeow_version (version INTEGER)")
@@ -246,3 +246,17 @@ func upgradeV2(tx *sql.Tx, container *Container) error {
}
return err
}
func upgradeV3(tx *sql.Tx, container *Container) error {
_, err := tx.Exec(`CREATE TABLE whatsmeow_message_secrets (
our_jid TEXT,
chat_jid TEXT,
sender_jid TEXT,
message_id TEXT,
key bytea NOT NULL,
PRIMARY KEY (our_jid, chat_jid, sender_jid, message_id),
FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
return err
}

View File

@@ -99,6 +99,19 @@ type DeviceContainer interface {
DeleteDevice(store *Device) error
}
type MessageSecretInsert struct {
Chat types.JID
Sender types.JID
ID types.MessageID
Secret []byte
}
type MsgSecretStore interface {
PutMessageSecrets([]MessageSecretInsert) error
PutMessageSecret(chat, sender types.JID, id types.MessageID, secret []byte) error
GetMessageSecret(chat, sender types.JID, id types.MessageID) ([]byte, error)
}
type Device struct {
Log waLog.Logger
@@ -123,6 +136,7 @@ type Device struct {
AppState AppStateStore
Contacts ContactStore
ChatSettings ChatSettingsStore
MsgSecrets MsgSecretStore
Container DeviceContainer
DatabaseErrorHandler func(device *Device, action string, attemptIndex int, err error) (retry bool)

View File

@@ -217,6 +217,7 @@ type Message struct {
IsViewOnce bool // True if the message was unwrapped from a ViewOnceMessage or ViewOnceMessageV2
IsViewOnceV2 bool // True if the message was unwrapped from a ViewOnceMessage
IsDocumentWithCaption bool // True if the message was unwrapped from a DocumentWithCaptionMessage
IsEdit bool // True if the message was unwrapped from an EditedMessage
// The raw message struct. This is the raw unmodified data, which means the actual message might
// be wrapped in DeviceSentMessage, EphemeralMessage or ViewOnceMessage.
@@ -250,6 +251,10 @@ func (evt *Message) UnwrapRaw() *Message {
evt.Message = evt.Message.GetDocumentWithCaptionMessage().GetMessage()
evt.IsDocumentWithCaption = true
}
if evt.Message.GetEditedMessage().GetMessage() != nil {
evt.Message = evt.Message.GetEditedMessage().GetMessage()
evt.IsEdit = true
}
return evt
}
@@ -259,12 +264,20 @@ type ReceiptType string
const (
// ReceiptTypeDelivered means the message was delivered to the device (but the user might not have noticed).
ReceiptTypeDelivered ReceiptType = ""
// ReceiptTypeSender is sent by your other devices when a message you sent is delivered to them.
ReceiptTypeSender ReceiptType = "sender"
// ReceiptTypeRetry means the message was delivered to the device, but decrypting the message failed.
ReceiptTypeRetry ReceiptType = "retry"
// ReceiptTypeRead means the user opened the chat and saw the message.
ReceiptTypeRead ReceiptType = "read"
// ReceiptTypeReadSelf means the current user read a message from a different device, and has read receipts disabled in privacy settings.
ReceiptTypeReadSelf ReceiptType = "read-self"
// ReceiptTypePlayed means the user opened a view-once media message.
//
// This is dispatched for both incoming and outgoing messages when played. If the current user opened the media,
// it means the media should be removed from all devices. If a recipient opened the media, it's just a notification
// for the sender that the media was viewed.
ReceiptTypePlayed ReceiptType = "played"
)
// GoString returns the name of the Go constant for the ReceiptType value.
@@ -276,6 +289,8 @@ func (rt ReceiptType) GoString() string {
return "events.ReceiptTypeReadSelf"
case ReceiptTypeDelivered:
return "events.ReceiptTypeDelivered"
case ReceiptTypePlayed:
return "events.ReceiptTypePlayed"
default:
return fmt.Sprintf("events.ReceiptType(%#v)", string(rt))
}
@@ -294,7 +309,8 @@ type Receipt struct {
// ChatPresence is emitted when a chat state update (also known as typing notification) is received.
//
// Note that WhatsApp won't send you these updates unless you mark yourself as online:
// client.SendPresence(types.PresenceAvailable)
//
// client.SendPresence(types.PresenceAvailable)
type ChatPresence struct {
types.MessageSource
State types.ChatPresence // The current state, either composing or paused
@@ -304,7 +320,8 @@ type ChatPresence struct {
// Presence is emitted when a presence update is received.
//
// Note that WhatsApp only sends you presence updates for individual users after you subscribe to them:
// client.SubscribePresence(user JID)
//
// client.SubscribePresence(user JID)
type Presence struct {
// The user whose presence event this is
From types.JID

View File

@@ -44,10 +44,11 @@ type GroupName struct {
// GroupTopic contains the topic (description) of a group along with metadata of who set it and when.
type GroupTopic struct {
Topic string
TopicID string
TopicSetAt time.Time
TopicSetBy JID
Topic string
TopicID string
TopicSetAt time.Time
TopicSetBy JID
TopicDeleted bool
}
// GroupLocked specifies whether the group info can only be edited by admins.

View File

@@ -75,6 +75,13 @@ type BusinessMessageLinkTarget struct {
Message string // The message that WhatsApp clients will pre-fill in the input box when clicking the link.
}
// ContactQRLinkTarget contains the info that is found using a contact QR link (see Client.ResolveContactQRLink)
type ContactQRLinkTarget struct {
JID JID // The JID of the user.
Type string // Might always be "contact".
PushName string // The notify / push name of the user.
}
// PrivacySetting is an individual setting value in the user's privacy settings.
type PrivacySetting string

View File

@@ -38,25 +38,26 @@ type UploadResponse struct {
// You should copy the fields in the response to the corresponding fields in a protobuf message.
//
// For example, to send an image:
// resp, err := cli.Upload(context.Background(), yourImageBytes, whatsmeow.MediaImage)
// // handle error
//
// imageMsg := &waProto.ImageMessage{
// Caption: proto.String("Hello, world!"),
// Mimetype: proto.String("image/png"), // replace this with the actual mime type
// // you can also optionally add other fields like ContextInfo and JpegThumbnail here
// resp, err := cli.Upload(context.Background(), yourImageBytes, whatsmeow.MediaImage)
// // handle error
//
// Url: &resp.URL,
// DirectPath: &resp.DirectPath,
// MediaKey: resp.MediaKey,
// FileEncSha256: resp.FileEncSHA256,
// FileSha256: resp.FileSha256,
// FileLength: &resp.FileLength,
// }
// _, err = cli.SendMessage(context.Background(), targetJID, "", &waProto.Message{
// ImageMessage: imageMsg,
// })
// // handle error again
// imageMsg := &waProto.ImageMessage{
// Caption: proto.String("Hello, world!"),
// Mimetype: proto.String("image/png"), // replace this with the actual mime type
// // you can also optionally add other fields like ContextInfo and JpegThumbnail here
//
// Url: &resp.URL,
// DirectPath: &resp.DirectPath,
// MediaKey: resp.MediaKey,
// FileEncSha256: resp.FileEncSHA256,
// FileSha256: resp.FileSha256,
// FileLength: &resp.FileLength,
// }
// _, err = cli.SendMessage(context.Background(), targetJID, "", &waProto.Message{
// ImageMessage: imageMsg,
// })
// // handle error again
//
// The same applies to the other message types like DocumentMessage, just replace the struct type and Message field name.
func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaType) (resp UploadResponse, err error) {

View File

@@ -21,7 +21,9 @@ import (
)
const BusinessMessageLinkPrefix = "https://wa.me/message/"
const ContactQRLinkPrefix = "https://wa.me/qr/"
const BusinessMessageLinkDirectPrefix = "https://api.whatsapp.com/message/"
const ContactQRLinkDirectPrefix = "https://api.whatsapp.com/qr/"
// ResolveBusinessMessageLink resolves a business message short link and returns the target JID, business name and
// text to prefill in the input field (if any).
@@ -34,7 +36,7 @@ func (cli *Client) ResolveBusinessMessageLink(code string) (*types.BusinessMessa
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:qr",
Type: "get",
Type: iqGet,
// WhatsApp android doesn't seem to have a "to" field for this one at all, not sure why but it works
Content: []waBinary.Node{{
Tag: "qr",
@@ -71,6 +73,72 @@ func (cli *Client) ResolveBusinessMessageLink(code string) (*types.BusinessMessa
return &target, ag.Error()
}
// ResolveContactQRLink resolves a link from a contact share QR code and returns the target JID and push name.
//
// The links look like https://wa.me/qr/<code> or https://api.whatsapp.com/qr/<code>. You can either provide
// the full link, or just the <code> part.
func (cli *Client) ResolveContactQRLink(code string) (*types.ContactQRLinkTarget, error) {
code = strings.TrimPrefix(code, ContactQRLinkPrefix)
code = strings.TrimPrefix(code, ContactQRLinkDirectPrefix)
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:qr",
Type: iqGet,
Content: []waBinary.Node{{
Tag: "qr",
Attrs: waBinary.Attrs{
"code": code,
},
}},
})
if errors.Is(err, ErrIQNotFound) {
return nil, wrapIQError(ErrContactQRLinkNotFound, err)
} else if err != nil {
return nil, err
}
qrChild, ok := resp.GetOptionalChildByTag("qr")
if !ok {
return nil, &ElementMissingError{Tag: "qr", In: "response to contact link query"}
}
var target types.ContactQRLinkTarget
ag := qrChild.AttrGetter()
target.JID = ag.JID("jid")
target.PushName = ag.OptionalString("notify")
target.Type = ag.String("type")
return &target, ag.Error()
}
// GetContactQRLink gets your own contact share QR link that can be resolved using ResolveContactQRLink
// (or scanned with the official apps when encoded as a QR code).
//
// If the revoke parameter is set to true, it will ask the server to revoke the previous link and generate a new one.
func (cli *Client) GetContactQRLink(revoke bool) (string, error) {
action := "get"
if revoke {
action = "revoke"
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:qr",
Type: iqSet,
Content: []waBinary.Node{{
Tag: "qr",
Attrs: waBinary.Attrs{
"type": "contact",
"action": action,
},
}},
})
if err != nil {
return "", err
}
qrChild, ok := resp.GetOptionalChildByTag("qr")
if !ok {
return "", &ElementMissingError{Tag: "qr", In: "response to own contact link fetch"}
}
ag := qrChild.AttrGetter()
return ag.String("code"), ag.Error()
}
// SetStatusMessage updates the current user's status text, which is shown in the "About" section in the user profile.
//
// This is different from the ephemeral status broadcast messages. Use SendMessage to types.StatusBroadcastJID to send

View File

@@ -0,0 +1,41 @@
// 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 gcmutil
import (
"crypto/aes"
"crypto/cipher"
"fmt"
)
func Prepare(secretKey []byte) (gcm cipher.AEAD, err error) {
var block cipher.Block
if block, err = aes.NewCipher(secretKey); err != nil {
err = fmt.Errorf("failed to initialize AES cipher: %w", err)
} else if gcm, err = cipher.NewGCM(block); err != nil {
err = fmt.Errorf("failed to initialize GCM: %w", err)
}
return
}
func Decrypt(secretKey, iv, ciphertext, additionalData []byte) ([]byte, error) {
if gcm, err := Prepare(secretKey); err != nil {
return nil, err
} else if plaintext, decryptErr := gcm.Open(nil, iv, ciphertext, additionalData); decryptErr != nil {
return nil, decryptErr
} else {
return plaintext, nil
}
}
func Encrypt(secretKey, iv, plaintext, additionalData []byte) ([]byte, error) {
if gcm, err := Prepare(secretKey); err != nil {
return nil, err
} else {
return gcm.Seal(nil, iv, plaintext, additionalData), nil
}
}