/* WhatsApp plugin for Miranda NG Copyright © 2019-24 George Hazan */ #include "stdafx.h" WAJid::WAJid(const char *pszUser, const char *pszServer, int iDevice, int iAgent) : user(pszUser ? pszUser : ""), server(pszServer ? pszServer : ""), device(iDevice), agent(iAgent) {} WAJid::WAJid(const char *pszJid, int _id) { if (pszJid == nullptr) pszJid = ""; auto *tmp = NEWSTR_ALLOCA(pszJid); auto *p = strrchr(tmp, '@'); if (p) { *p = 0; server = p + 1; } if (p = strrchr(tmp, ':')) { *p = 0; device = atoi(p + 1); } else device = _id; if (p = strrchr(tmp, '_')) { *p = 0; agent = atoi(p + 1); } else agent = 0; user = tmp; } bool WAJid::isUser() const { return server == "s.whatsapp.net"; } bool WAJid::isGroup() const { return server == "g.us"; } bool WAJid::isBroadcast() const { return server == "broadcast"; } bool WAJid::isStatusBroadcast() const { return isBroadcast() && user == "status"; } CMStringA WAJid::toString() const { CMStringA ret(user); if (agent > 0) ret.AppendFormat("_%d", agent); if (device > 0) ret.AppendFormat(":%d", device); ret.AppendFormat("@%s", server.c_str()); return ret; } ///////////////////////////////////////////////////////////////////////////////////////// static uint8_t sttLtHashInfo[] = "WhatsApp Patch Integrity"; void LT_HASH::add(const void *pData, size_t len) { uint16_t tmp[_countof(hash)]; HKDF(EVP_sha256(), (BYTE *)"", 0, (BYTE*)pData, len, sttLtHashInfo, sizeof(sttLtHashInfo) - 1, (BYTE *)tmp, sizeof(tmp)); for (int i = 0; i < _countof(hash); i++) hash[i] += tmp[i]; } void LT_HASH::sub(const void *pData, size_t len) { uint16_t tmp[_countof(hash)]; HKDF(EVP_sha256(), (BYTE *)"", 0, (BYTE *)pData, len, sttLtHashInfo, sizeof(sttLtHashInfo) - 1, (BYTE *)tmp, sizeof(tmp)); for (int i = 0; i < _countof(hash); i++) hash[i] -= tmp[i]; } ///////////////////////////////////////////////////////////////////////////////////////// WAUser* WhatsAppProto::FindUser(const char *szId) { if (szId == nullptr) return nullptr; mir_cslock lck(m_csUsers); auto *tmp = (WAUser *)_alloca(sizeof(WAUser)); tmp->szId = (char*)szId; return m_arUsers.find(tmp); } WAUser* WhatsAppProto::AddUser(const char *szId, bool bTemporary) { auto *pUser = FindUser(szId); if (pUser != nullptr) return pUser; MCONTACT hContact = db_add_contact(); Proto_AddToContact(hContact, m_szModuleName); setString(hContact, DBKEY_ID, szId); pUser = new WAUser(hContact, mir_strdup(szId)); pUser->bIsGroupChat = WAJid(szId).isGroup(); if (pUser->bIsGroupChat) { setByte(hContact, "ChatRoom", 1); } else if (m_wszDefaultGroup) Clist_SetGroup(hContact, m_wszDefaultGroup); if (bTemporary) Contact::RemoveFromList(hContact); mir_cslock lck(m_csUsers); m_arUsers.insert(pUser); return pUser; } ///////////////////////////////////////////////////////////////////////////////////////// WA_PKT_HANDLER WhatsAppProto::FindPersistentHandler(const WANode &pNode) { auto *pChild = pNode.getFirstChild(); CMStringA szChild = (pChild) ? pChild->title : ""; CMStringA szTitle = pNode.title; CMStringA szType = pNode.getAttr("type"); CMStringA szXmlns = pNode.getAttr("xmlns"); for (auto &it : m_arPersistent) { if (it->pszTitle && szTitle != it->pszTitle) continue; if (it->pszType && szType != it->pszType) continue; if (it->pszXmlns && szXmlns != it->pszXmlns) continue; if (it->pszChild && szChild != it->pszChild) continue; return it->pHandler; } return nullptr; } ///////////////////////////////////////////////////////////////////////////////////////// CMStringA WhatsAppProto::GenerateMessageId() { return CMStringA(FORMAT, "%d.%d-%d", m_wMsgPrefix[0], m_wMsgPrefix[1], m_iPacketId++); } ///////////////////////////////////////////////////////////////////////////////////////// // sends a piece of JSON to a server via a websocket, masked int WhatsAppProto::WSSend(const ProtobufCMessage &msg) { if (m_ws == nullptr) return -1; MBinBuffer buf(proto::Serialize(&msg)); // Netlib_Dump(m_hServerConn, buf.data(), buf.length(), true, 0); MBinBuffer payload = m_noise->encodeFrame(buf.data(), buf.length()); m_ws->sendBinary(payload.data(), payload.length()); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// int WhatsAppProto::WSSendNode(WANode &node) { if (m_ws == nullptr) return 0; CMStringA szText; node.print(szText); debugLogA("Sending binary node:\n%s", szText.c_str()); WAWriter writer; writer.writeNode(&node); MBinBuffer encData = m_noise->encrypt(writer.body.data(), writer.body.length()); MBinBuffer payload = m_noise->encodeFrame(encData.data(), encData.length()); m_ws->sendBinary(payload.data(), payload.length()); return 1; } int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER pHandler) { if (m_ws == nullptr) return 0; CMStringA id(GenerateMessageId()); node.addAttr("id", id); { mir_cslock lck(m_csPacketQueue); m_arPacketQueue.insert(new WARequestSimple(id, pHandler)); } return WSSendNode(node); } int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER_FULL pHandler, void *pUserInfo) { if (m_ws == nullptr) return 0; CMStringA id(GenerateMessageId()); node.addAttr("id", id); { mir_cslock lck(m_csPacketQueue); m_arPacketQueue.insert(new WARequestParam(id, pHandler, pUserInfo)); } return WSSendNode(node); } ///////////////////////////////////////////////////////////////////////////////////////// std::string decodeBinStr(const std::string &buf) { size_t cbLen; void *pData = mir_base64_decode(buf.c_str(), &cbLen); if (pData == nullptr) return ""; std::string res((char *)pData, cbLen); mir_free(pData); return res; } MBinBuffer decodeBufStr(const std::string &buf) { MBinBuffer res; size_t cbLen; void *pData = mir_base64_decode(buf.c_str(), &cbLen); if (pData == nullptr) return res; res.assign(pData, cbLen); mir_free(pData); return res; } uint32_t decodeBigEndian(const uint8_t *buf, size_t len) { uint32_t ret = 0; for (size_t i = 0; i < len; i++) { ret <<= 8; ret += buf[i]; } return ret; } std::string encodeBigEndian(uint32_t num, size_t len) { std::string res; for (size_t i = 0; i < len; i++) { char c = num & 0xFF; res = c + res; num >>= 8; } return res; } void generateIV(uint8_t *iv, uint32_t &pVar) { auto counter = encodeBigEndian(pVar); memset(iv, 0, 8); memcpy(iv + 8, counter.c_str(), sizeof(uint32_t)); pVar++; } ///////////////////////////////////////////////////////////////////////////////////////// // Padding void padBuffer16(MBinBuffer &buf) { uint8_t c = 16 - buf.length() % 16; for (uint8_t i = 0; i < c; i++) buf.append(&c, 1); } MBinBuffer unpadBuffer16(const MBinBuffer &buf) { size_t len = buf.length(); auto p = buf.data() + len - 1; if (*p <= 0x10) { MBinBuffer res; res.assign(buf.data(), len - *p); return res; } return buf; } ///////////////////////////////////////////////////////////////////////////////////////// // Popups void WhatsAppProto::InitPopups(void) { g_plugin.addPopupOption(CMStringW(FORMAT, TranslateT("%s error notifications"), m_tszUserName), m_bUsePopups); char name[256]; mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); wchar_t desc[256]; mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Errors")); POPUPCLASS ppc = {}; ppc.flags = PCF_UNICODE; ppc.pszName = name; ppc.pszDescription.w = desc; ppc.hIcon = IcoLib_GetIconByHandle(m_hProtoIcon); ppc.colorBack = RGB(191, 0, 0); //Red ppc.colorText = RGB(255, 245, 225); //Yellow ppc.iSeconds = 60; m_hPopupClass = Popup_RegisterClass(&ppc); IcoLib_ReleaseIcon(ppc.hIcon); } void WhatsAppProto::Popup(MCONTACT hContact, const wchar_t *szMsg, const wchar_t *szTitle) { if (!m_bUsePopups) return; char name[256]; mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); CMStringW wszTitle(szTitle); if (hContact == 0) { wszTitle.Insert(0, L": "); wszTitle.Insert(0, m_tszUserName); } POPUPDATACLASS ppd = {}; ppd.szTitle.w = wszTitle; ppd.szText.w = szMsg; ppd.pszClassName = name; ppd.hContact = hContact; Popup_AddClass(&ppd); } ///////////////////////////////////////////////////////////////////////////////////////// void bin2file(const MBinBuffer &buf, const wchar_t *pwszFileName) { int fileId = _wopen(pwszFileName, _O_WRONLY | _O_TRUNC | _O_BINARY | _O_CREAT, _S_IREAD | _S_IWRITE); if (fileId != -1) { write(fileId, buf.data(), (unsigned)buf.length()); close(fileId); } } void string2file(const std::string &str, const wchar_t *pwszFileName) { int fileId = _wopen(pwszFileName, _O_WRONLY | _O_TRUNC | _O_BINARY | _O_CREAT, _S_IREAD | _S_IWRITE); if (fileId != -1) { write(fileId, str.c_str(), (unsigned)str.size()); close(fileId); } } CMStringA file2string(const wchar_t *pwszFileName) { CMStringA res; int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD | _S_IWRITE); if (fileId != -1) { res.Truncate(filelength(fileId)); read(fileId, res.GetBuffer(), res.GetLength()); close(fileId); } 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.isEmpty()) { 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; if (pMessage) { if (auto *pExt = pMessage->extendedtextmessage) { if (pExt->title) { if (m_bUseBbcodes) szMessageText.Append("[b]"); szMessageText.Append(pExt->title); if (m_bUseBbcodes) szMessageText.Append("[/b]"); szMessageText.Append("\n"); } if (pExt->contextinfo && pExt->contextinfo->quotedmessage) szMessageText.AppendFormat("> %s\n\n", pExt->contextinfo->quotedmessage->conversation); 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; } return szMessageText; } ///////////////////////////////////////////////////////////////////////////////////////// void proto::CleanBinary(ProtobufCBinaryData &field) { if (field.data) { free(field.data); field.data = nullptr; } field.len = 0; } ProtobufCBinaryData proto::SetBinary(const void *pData, size_t len) { ProtobufCBinaryData res; if (pData == nullptr) { res.data = nullptr; res.len = 0; } else { res.data = (uint8_t *)malloc(res.len = len); memcpy(res.data, pData, len); } return res; } MBinBuffer proto::Serialize(const ProtobufCMessage *msg) { MBinBuffer res(protobuf_c_message_get_packed_size(msg)); protobuf_c_message_pack(msg, res.data()); return res; } ///////////////////////////////////////////////////////////////////////////////////////// CMStringA directPath2url(const char *pszDirectPath) { return CMStringA("https://mmg.whatsapp.net") + pszDirectPath; } WAMediaKeys::WAMediaKeys(const uint8_t *pKey, size_t keyLen, const char *pszMediaType) { CMStringA pszHkdfString(FORMAT, "WhatsApp %s Keys", pszMediaType); HKDF(EVP_sha256(), (BYTE *)"", 0, pKey, (int)keyLen, (BYTE *)pszHkdfString.c_str(), pszHkdfString.GetLength(), (BYTE *)this, sizeof(*this)); } MBinBuffer WhatsAppProto::DownloadEncryptedFile(const char *url, const ProtobufCBinaryData &mediaKeys, const char *pszMediaType) { MHttpRequest req(REQUEST_GET); req.m_szUrl = url; req.AddHeader("Origin", "https://web.whatsapp.com"); MBinBuffer ret; auto *pResp = Netlib_HttpTransaction(m_hNetlibUser, &req); if (pResp) { if (pResp->resultCode == 200) { WAMediaKeys out(mediaKeys.data, mediaKeys.len, pszMediaType); ret = aesDecrypt(EVP_aes_256_cbc(), out.cipherKey, out.iv, pResp->body, pResp->body.GetLength()); } } return ret; } CMStringW WhatsAppProto::GetTmpFileName(const char *pszClass, const char *pszAddition) { CMStringW ret(VARSW(L"%miranda_userdata%")); ret.AppendFormat(L"/%S/%S_%S", m_szModuleName, pszClass, pszAddition); return ret; }