summaryrefslogtreecommitdiff
path: root/protocols/WhatsApp/src
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2022-11-12 14:54:25 +0300
committerGeorge Hazan <ghazan@miranda.im>2022-11-12 14:54:25 +0300
commitc500e50812be6919257b406344218cf6ab69b95f (patch)
treeb08dd86a0984cc735e5188ba3437cd4810070fd8 /protocols/WhatsApp/src
parent6aa070f80a4256d220359dfed902aba2bb81e4c4 (diff)
WhatsApp:
- attempt to implement group chat encryption; - signal session loading optimization; - support for image, video & audio messages
Diffstat (limited to 'protocols/WhatsApp/src')
-rw-r--r--protocols/WhatsApp/src/chats.cpp110
-rw-r--r--protocols/WhatsApp/src/message.cpp68
-rw-r--r--protocols/WhatsApp/src/proto.cpp3
-rw-r--r--protocols/WhatsApp/src/proto.h21
-rw-r--r--protocols/WhatsApp/src/server.cpp6
-rw-r--r--protocols/WhatsApp/src/signal.cpp80
-rw-r--r--protocols/WhatsApp/src/utils.cpp52
7 files changed, 274 insertions, 66 deletions
diff --git a/protocols/WhatsApp/src/chats.cpp b/protocols/WhatsApp/src/chats.cpp
index 8861693fca..774df86db9 100644
--- a/protocols/WhatsApp/src/chats.cpp
+++ b/protocols/WhatsApp/src/chats.cpp
@@ -7,38 +7,40 @@ Copyright © 2019-22 George Hazan
#include "stdafx.h"
-void WhatsAppProto::GC_Init(WAUser *pUser)
+void WhatsAppProto::GC_GetAllMetadata()
{
- CMStringW wszId(Utf2T(pUser->szId));
-
- pUser->si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszId, getMStringW(pUser->hContact, "Nick"));
-
- Chat_AddGroup(pUser->si, TranslateT("Owner"));
- Chat_AddGroup(pUser->si, TranslateT("SuperAdmin"));
- Chat_AddGroup(pUser->si, TranslateT("Admin"));
- Chat_AddGroup(pUser->si, TranslateT("Participant"));
-
- if (pUser->bInited) {
- Chat_Control(m_szModuleName, wszId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE);
- Chat_Control(m_szModuleName, wszId, SESSION_ONLINE);
- }
- else GC_GetMetadata(pUser->szId);
+ WANodeIq iq(IQ::GET, "w:g2", "@g.us");
+ auto *pRoot = iq.addChild("participating");
+ *pRoot << XCHILD("participants") << XCHILD("description");
+ WSSendNode(iq, &WhatsAppProto::OnIqGcGetAllMetadata);
}
-void WhatsAppProto::GC_GetMetadata(const char *szId)
+void WhatsAppProto::OnIqGcGetAllMetadata(const WANode &node)
{
- WANodeIq iq(IQ::GET, "w:g2", szId);
- iq.addChild("query")->addAttr("request", "interactive");
- WSSendNode(iq, &WhatsAppProto::OnIqGcMetadata);
+ if (auto *pGroup = node.getChild("groups"))
+ for (auto &it : pGroup->getChildren())
+ GC_ParseMetadata(it);
}
-void WhatsAppProto::OnIqGcMetadata(const WANode &node)
+void WhatsAppProto::GC_ParseMetadata(const WANode *pGroup)
{
- auto *pGroup = node.getChild("group");
- auto *pChatUser = FindUser(node.getAttr("from"));
- if (pChatUser == nullptr || pGroup == nullptr)
+ auto *pszId = pGroup->getAttr("id");
+ if (pszId == nullptr)
return;
+ auto *pChatUser = AddUser(CMStringA(pszId) + "@g.us", false);
+ if (pChatUser == nullptr)
+ return;
+
+ CMStringW wszId(Utf2T(pChatUser->szId));
+
+ pChatUser->si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszId, getMStringW(pChatUser->hContact, "Nick"));
+
+ Chat_AddGroup(pChatUser->si, TranslateT("Owner"));
+ Chat_AddGroup(pChatUser->si, TranslateT("SuperAdmin"));
+ Chat_AddGroup(pChatUser->si, TranslateT("Admin"));
+ Chat_AddGroup(pChatUser->si, TranslateT("Participant"));
+
CMStringA szOwner(pGroup->getAttr("creator")), szNick, szRole;
for (auto &it : pGroup->getChildren()) {
@@ -57,7 +59,7 @@ void WhatsAppProto::OnIqGcMetadata(const WANode &node)
}
else if (it->title == "participant") {
auto *jid = it->getAttr("jid");
-
+
// if role isn't specified, use the default one
auto *role = it->getAttr("type");
if (role == nullptr)
@@ -112,7 +114,65 @@ void WhatsAppProto::OnIqGcMetadata(const WANode &node)
}
pChatUser->bInited = true;
- CMStringW wszId(Utf2T(pChatUser->szId));
Chat_Control(m_szModuleName, wszId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE);
Chat_Control(m_szModuleName, wszId, SESSION_ONLINE);
}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+int WhatsAppProto::GcEventHook(WPARAM, LPARAM lParam)
+{
+ GCHOOK *gch = (GCHOOK*)lParam;
+ if (gch == nullptr)
+ return 0;
+
+ if (mir_strcmpi(gch->si->pszModule, m_szModuleName))
+ return 0;
+
+ auto *pUser = FindUser(T2Utf(gch->si->ptszID));
+ if (pUser == nullptr)
+ return 0;
+
+ switch (gch->iType) {
+ case GC_USER_MESSAGE:
+ if (gch->ptszText && mir_wstrlen(gch->ptszText) > 0) {
+ rtrimw(gch->ptszText);
+ Chat_UnescapeTags(gch->ptszText);
+ SendTextMessage(pUser->szId, T2Utf(gch->ptszText));
+ }
+ break;
+
+ case GC_USER_PRIVMESS:
+ break;
+
+ case GC_USER_LOGMENU:
+ break;
+
+ case GC_USER_NICKLISTMENU:
+ break;
+ }
+
+ return 1;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+int WhatsAppProto::GcMenuHook(WPARAM, LPARAM lParam)
+{
+ GCMENUITEMS* gcmi = (GCMENUITEMS*)lParam;
+ if (gcmi == nullptr)
+ return 0;
+
+ if (mir_strcmpi(gcmi->pszModule, m_szModuleName))
+ return 0;
+
+ auto *pUser = FindUser(T2Utf(gcmi->pszID));
+ if (pUser == nullptr)
+ return 0;
+
+ if (gcmi->Type == MENU_ON_LOG) {
+ }
+ else if (gcmi->Type == MENU_ON_NICKLIST) {
+ }
+ return 0;
+}
diff --git a/protocols/WhatsApp/src/message.cpp b/protocols/WhatsApp/src/message.cpp
index 32d42cad3d..3d1bf5e872 100644
--- a/protocols/WhatsApp/src/message.cpp
+++ b/protocols/WhatsApp/src/message.cpp
@@ -358,31 +358,71 @@ bool WhatsAppProto::CreateMsgParticipant(WANode *pParticipants, const WAJid &jid
int WhatsAppProto::SendTextMessage(const char *jid, const char *pszMsg)
{
+ WAJid toJid(jid);
+
char szMsgId[40];
__int64 msgId;
Utils_GetRandom(&msgId, sizeof(msgId));
_i64toa(msgId, szMsgId, 10);
+ // mother node for all participants
+ WANode payLoad("message");
+ payLoad << CHAR_PARAM("id", szMsgId) << CHAR_PARAM("type", "text") << CHAR_PARAM("to", jid);
+
+ auto *pParticipants = payLoad.addChild("participants");
+
+ // basic message
Wa__Message body;
body.conversation = (char*)pszMsg;
- Wa__Message__DeviceSentMessage sentBody;
- sentBody.message = &body;
- sentBody.destinationjid = (char*)jid;
+ bool shouldIncludeIdentity = false;
- Wa__Message msg;
- msg.devicesentmessage = &sentBody;
+ if (toJid.isGroup()) {
+ MBinBuffer encodedMsg(proto::Serialize(&body));
+ padBuffer16(encodedMsg);
- MBinBuffer encMsg(proto::Serialize(&msg));
- padBuffer16(encMsg);
+ MBinBuffer skmsgKey;
+ MBinBuffer cipherText(m_signalStore.encryptSenderKey(toJid, m_szJid, encodedMsg, skmsgKey));
- WANode payLoad("message");
- payLoad << CHAR_PARAM("id", szMsgId) << CHAR_PARAM("type", "text") << CHAR_PARAM("to", jid);
-
- auto *pParticipants = payLoad.addChild("participants");
- bool shouldIncludeIdentity = CreateMsgParticipant(pParticipants, WAJid(jid), encMsg);
- for (auto &it : m_arDevices)
- shouldIncludeIdentity |= CreateMsgParticipant(pParticipants, it->jid, encMsg);
+ auto *pEnc = payLoad.addChild("enc");
+ *pEnc << CHAR_PARAM("v", "2") << CHAR_PARAM("type", "skmsg");
+ pEnc->content.append(cipherText.data(), cipherText.length());
+
+ Wa__Message__SenderKeyDistributionMessage sentBody;
+ sentBody.axolotlsenderkeydistributionmessage.data = skmsgKey.data();
+ sentBody.axolotlsenderkeydistributionmessage.len = skmsgKey.length();
+ sentBody.has_axolotlsenderkeydistributionmessage = true;
+ sentBody.groupid = (char*)jid;
+
+ Wa__Message msg;
+ msg.senderkeydistributionmessage = &sentBody;
+
+ MBinBuffer encodedMeMsg(proto::Serialize(&msg));
+ padBuffer16(encodedMeMsg);
+
+ if (auto *pUser = FindUser(jid))
+ if (pUser->si)
+ for (auto &it : pUser->si->arUsers)
+ shouldIncludeIdentity = CreateMsgParticipant(pParticipants, WAJid(T2Utf(it->pszUID)), encodedMeMsg);
+
+ for (auto &it : m_arDevices)
+ shouldIncludeIdentity |= CreateMsgParticipant(pParticipants, it->jid, encodedMeMsg);
+ }
+ else {
+ Wa__Message__DeviceSentMessage sentBody;
+ sentBody.message = &body;
+ sentBody.destinationjid = (char*)jid;
+
+ Wa__Message msg;
+ msg.devicesentmessage = &sentBody;
+
+ MBinBuffer encodedMeMsg(proto::Serialize(&msg));
+ padBuffer16(encodedMeMsg);
+
+ shouldIncludeIdentity = CreateMsgParticipant(pParticipants, toJid, encodedMeMsg);
+ for (auto &it : m_arDevices)
+ shouldIncludeIdentity |= CreateMsgParticipant(pParticipants, it->jid, encodedMeMsg);
+ }
if (shouldIncludeIdentity) {
MBinBuffer encIdentity(m_signalStore.encodeSignedIdentity(true));
diff --git a/protocols/WhatsApp/src/proto.cpp b/protocols/WhatsApp/src/proto.cpp
index d101a26255..fbc81be06f 100644
--- a/protocols/WhatsApp/src/proto.cpp
+++ b/protocols/WhatsApp/src/proto.cpp
@@ -97,6 +97,9 @@ WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) :
gcr.ptszDispName = m_tszUserName;
gcr.pszModule = m_szModuleName;
Chat_Register(&gcr);
+
+ HookProtoEvent(ME_GC_EVENT, &WhatsAppProto::GcEventHook);
+ HookProtoEvent(ME_GC_BUILDMENU, &WhatsAppProto::GcMenuHook);
}
WhatsAppProto::~WhatsAppProto()
diff --git a/protocols/WhatsApp/src/proto.h b/protocols/WhatsApp/src/proto.h
index 4f2fee4609..4d5c8773b7 100644
--- a/protocols/WhatsApp/src/proto.h
+++ b/protocols/WhatsApp/src/proto.h
@@ -190,24 +190,27 @@ public:
{
MBinBuffer priv, pub;
}
- signedIdentity;
+ signedIdentity;
struct
{
MBinBuffer priv, pub, signature;
uint32_t keyid;
- } preKey;
+ }
+ preKey;
MSignalStore(PROTO_INTERFACE *_1, const char *_2);
~MSignalStore();
__forceinline signal_context *CTX() const { return m_pContext; }
- MSignalSession *createSession(const CMStringA &szName, int deviceId);
+ MSignalSession* createSession(const CMStringA &szName, int deviceId);
+ MSignalSession* getSession(const signal_protocol_address *address);
MBinBuffer decryptSignalProto(const CMStringA &from, const char *pszType, const MBinBuffer &encrypted);
MBinBuffer decryptGroupSignalProto(const CMStringA &from, const CMStringA &author, const MBinBuffer &encrypted);
+ MBinBuffer encryptSenderKey(const WAJid &to, const CMStringA &from, const MBinBuffer &buf, MBinBuffer &skmsgKey);
MBinBuffer encryptSignalProto(const WAJid &to, const MBinBuffer &buf, int &type);
MBinBuffer encodeSignedIdentity(bool);
@@ -288,8 +291,11 @@ class WhatsAppProto : public PROTO<WhatsAppProto>
// Group chats /////////////////////////////////////////////////////////////////////////
- void GC_Init(WAUser *pUser);
- void GC_GetMetadata(const char *szJid);
+ void GC_GetAllMetadata();
+ void GC_ParseMetadata(const WANode *pGroup);
+
+ int __cdecl GcEventHook(WPARAM, LPARAM);
+ int __cdecl GcMenuHook(WPARAM, LPARAM);
// UI //////////////////////////////////////////////////////////////////////////////////
@@ -311,6 +317,7 @@ class WhatsAppProto : public PROTO<WhatsAppProto>
uint16_t m_wMsgPrefix[2];
CMStringA GenerateMessageId();
CMStringA GetMessageText(const Wa__Message *pMessage);
+ void GetMessageContent(CMStringA &txt, const char *szType, const char *szUrl, const char *szMimetype, const char *szDirectPath, const ProtobufCBinaryData &szMediaKey, const char *szCaption = nullptr);
void ProcessMessage(WAMSG type, const Wa__WebMessageInfo &msg);
bool CreateMsgParticipant(WANode *pParticipants, const WAJid &jid, const MBinBuffer &orig);
@@ -345,8 +352,6 @@ class WhatsAppProto : public PROTO<WhatsAppProto>
/// Request handlers ///////////////////////////////////////////////////////////////////
- void OnGetChatInfo(const JSONNode &node, void*);
-
void OnProcessHandshake(const uint8_t *pData, int cbLen);
void InitPersistentHandlers();
@@ -354,7 +359,7 @@ class WhatsAppProto : public PROTO<WhatsAppProto>
void OnIqBlockList(const WANode &node);
void OnIqCountPrekeys(const WANode &node);
void OnIqDoNothing(const WANode &node);
- void OnIqGcMetadata(const WANode &node);
+ void OnIqGcGetAllMetadata(const WANode &node);
void OnIqGetAvatar(const WANode &node);
void OnIqGetUsync(const WANode &node);
void OnIqPairDevice(const WANode &node);
diff --git a/protocols/WhatsApp/src/server.cpp b/protocols/WhatsApp/src/server.cpp
index 5dfbd34270..16cdb86a79 100644
--- a/protocols/WhatsApp/src/server.cpp
+++ b/protocols/WhatsApp/src/server.cpp
@@ -312,8 +312,10 @@ void WhatsAppProto::OnLoggedIn()
&WhatsAppProto::OnIqDoNothing);
for (auto &it : m_arUsers)
- if (it->bIsGroupChat)
- GC_Init(it);
+ if (it->bIsGroupChat) {
+ GC_GetAllMetadata();
+ return;
+ }
}
void WhatsAppProto::OnLoggedOut(void)
diff --git a/protocols/WhatsApp/src/signal.cpp b/protocols/WhatsApp/src/signal.cpp
index d8d8f2c2b3..47bf9462af 100644
--- a/protocols/WhatsApp/src/signal.cpp
+++ b/protocols/WhatsApp/src/signal.cpp
@@ -110,10 +110,8 @@ static int encrypt_func(signal_buffer **output,
static int contains_session_func(const signal_protocol_address *address, void *user_data)
{
auto *pStore = (MSignalStore *)user_data;
-
- MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id);
- ptrA data(pStore->pProto->getStringA(tmp.getSetting()));
- return data != nullptr;
+ auto *pSession = pStore->getSession(address);
+ return pSession != nullptr;
}
static int delete_all_sessions_func(const char *name, size_t name_len, void *user_data)
@@ -171,18 +169,11 @@ int load_session_func(signal_buffer **record, signal_buffer **user_data_storage,
{
auto *pStore = (MSignalStore *)user_data;
- MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id);
- auto *pSession = pStore->arSessions.find(&tmp);
- if (pSession == nullptr) {
- MBinBuffer blob(pStore->pProto->getBlob(tmp.getSetting()));
- if (blob.data() == nullptr)
- return 0;
-
- pSession = new MSignalSession(tmp);
- pSession->sessionData.assign(blob.data(), blob.length());
- }
+ auto *pSession = pStore->getSession(address);
+ if (pSession == nullptr)
+ return 0;
- *record = signal_buffer_create((uint8_t *)pSession->sessionData.data(), pSession->sessionData.length());
+ *record = signal_buffer_create(pSession->sessionData.data(), pSession->sessionData.length());
*user_data_storage = 0;
return 1;
}
@@ -521,6 +512,23 @@ MSignalSession* MSignalStore::createSession(const CMStringA &szName, int deviceI
return pSession;
}
+MSignalSession* MSignalStore::getSession(const signal_protocol_address *address)
+{
+ MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id);
+ auto *pSession = arSessions.find(&tmp);
+ if (pSession == nullptr) {
+ MBinBuffer blob(pProto->getBlob(tmp.getSetting()));
+ if (blob.data() == nullptr)
+ return nullptr;
+
+ pSession = new MSignalSession(tmp);
+ pSession->sessionData.assign(blob.data(), blob.length());
+ arSessions.insert(pSession);
+ }
+
+ return pSession;
+}
+
/////////////////////////////////////////////////////////////////////////////////////////
MBinBuffer MSignalStore::decryptSignalProto(const CMStringA &from, const char *pszType, const MBinBuffer &encrypted)
@@ -625,6 +633,48 @@ void MSignalStore::processSenderKeyMessage(const CMStringA &author, const Wa__Me
/////////////////////////////////////////////////////////////////////////////////////////
// encryption
+MBinBuffer MSignalStore::encryptSenderKey(const WAJid &to, const CMStringA &from, const MBinBuffer &buf, MBinBuffer &skmsgKey)
+{
+ signal_protocol_sender_key_name senderKeyName;
+ senderKeyName.group_id = to.user.c_str();
+ senderKeyName.group_id_len = to.user.GetLength();
+ senderKeyName.sender.device_id = 0;
+ senderKeyName.sender.name = from.c_str();
+ senderKeyName.sender.name_len = from.GetLength();
+
+ group_session_builder *builder;
+ logError(
+ group_session_builder_create(&builder, m_pStore, m_pContext),
+ "unable to create session builder");
+
+ sender_key_distribution_message *skmsg;
+ logError(
+ group_session_builder_create_session(builder, &skmsg, &senderKeyName),
+ "unable to create session");
+
+ group_cipher *cipher;
+ logError(
+ group_cipher_create(&cipher, m_pStore, &senderKeyName, m_pContext),
+ "unable to create group cipher");
+
+ ciphertext_message *encMessage;
+ logError(
+ group_cipher_encrypt(cipher, buf.data(), buf.length(), &encMessage),
+ "unable to encrypt group message");
+
+ MBinBuffer res;
+ auto *cipherText = ciphertext_message_get_serialized(encMessage);
+ res.assign(cipherText->data, cipherText->len);
+
+ auto *pKey = sender_key_distribution_message_get_signature_key(skmsg);
+ skmsgKey.assign(pKey->data, sizeof(pKey->data));
+
+ sender_key_distribution_message_destroy((signal_type_base*)skmsg);
+ group_cipher_free(cipher);
+ group_session_builder_free(builder);
+ return res;
+}
+
MBinBuffer MSignalStore::encryptSignalProto(const WAJid &to, const MBinBuffer &buf, int &type)
{
auto *pSession = createSession(to.user, to.device);
diff --git a/protocols/WhatsApp/src/utils.cpp b/protocols/WhatsApp/src/utils.cpp
index a28deda31d..4f11f5821c 100644
--- a/protocols/WhatsApp/src/utils.cpp
+++ b/protocols/WhatsApp/src/utils.cpp
@@ -120,8 +120,6 @@ WAUser* WhatsAppProto::AddUser(const char *szId, bool bTemporary)
if (pUser->bIsGroupChat) {
setByte(hContact, "ChatRoom", 1);
setString(hContact, "ChatRoomID", szId);
-
- GC_Init(pUser);
}
else {
setString(hContact, DBKEY_ID, szId);
@@ -418,6 +416,47 @@ CMStringA file2string(const wchar_t *pwszFileName)
return res;
}
+void WhatsAppProto::GetMessageContent(
+ CMStringA &txt,
+ const char *szType,
+ const char *szMimeType,
+ const char *szUrl,
+ const char *szDirectPath,
+ const ProtobufCBinaryData &pMediaKey,
+ const char *szCaption)
+{
+ if (szCaption) {
+ if (m_bUseBbcodes)
+ txt.Append("<b>");
+ txt.Append(szCaption);
+ if (m_bUseBbcodes)
+ txt.Append("</b>");
+ txt.Append("\n");
+ }
+
+ CMStringA url = szUrl;
+ int idx = url.ReverseFind('/');
+ if (idx != -1)
+ url.Delete(0, idx+1);
+ idx = url.ReverseFind('.');
+ if (idx != -1)
+ url.Truncate(idx);
+ if (szMimeType)
+ url.Append(_T2A(ProtoGetAvatarExtension(ProtoGetAvatarFormatByMimeType(szMimeType))));
+
+ char *szMediaType = NEWSTR_ALLOCA(szType);
+ szMediaType[0] = toupper(szMediaType[0]);
+
+ MBinBuffer buf = DownloadEncryptedFile(directPath2url(szDirectPath), pMediaKey, szMediaType);
+ if (buf.data()) {
+ CMStringW pwszFileName(GetTmpFileName(szType, url));
+ bin2file(buf, pwszFileName);
+
+ pwszFileName.Replace(L"\\", L"/");
+ txt.AppendFormat("file://%s", T2Utf(pwszFileName).get());
+ }
+}
+
CMStringA WhatsAppProto::GetMessageText(const Wa__Message *pMessage)
{
CMStringA szMessageText;
@@ -439,6 +478,15 @@ CMStringA WhatsAppProto::GetMessageText(const Wa__Message *pMessage)
if (pExt->text)
szMessageText.Append(pExt->text);
}
+ else if (auto *pAudio = pMessage->audiomessage) {
+ GetMessageContent(szMessageText, "audio", pAudio->url, pAudio->directpath, pAudio->mimetype, pAudio->mediakey);
+ }
+ else if (auto *pVideo = pMessage->videomessage) {
+ GetMessageContent(szMessageText, "video", pVideo->url, pVideo->directpath, pVideo->mimetype, pVideo->mediakey, pVideo->caption);
+ }
+ else if (auto *pImage = pMessage->imagemessage) {
+ GetMessageContent(szMessageText, "image", pImage->url, pImage->directpath, pImage->mimetype, pImage->mediakey, pImage->caption);
+ }
else if (mir_strlen(pMessage->conversation))
szMessageText = pMessage->conversation;
}