diff options
author | George Hazan <ghazan@miranda.im> | 2022-10-21 13:51:21 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2022-10-21 13:51:21 +0300 |
commit | f565cc4efeace9a9f1c034f3956bd7192382bea7 (patch) | |
tree | 437d14bc2433b5453ac1f63c07f1a4a9b6ecbbde /protocols/WhatsApp/src | |
parent | 51037c1032b29ac1090cf4ecb78f3dce1bda554a (diff) |
WhatsApp: app state sync
Diffstat (limited to 'protocols/WhatsApp/src')
-rw-r--r-- | protocols/WhatsApp/src/appsync.cpp | 235 | ||||
-rw-r--r-- | protocols/WhatsApp/src/iq.cpp | 165 | ||||
-rw-r--r-- | protocols/WhatsApp/src/proto.cpp | 23 | ||||
-rw-r--r-- | protocols/WhatsApp/src/proto.h | 7 | ||||
-rw-r--r-- | protocols/WhatsApp/src/utils.cpp | 49 | ||||
-rw-r--r-- | protocols/WhatsApp/src/utils.h | 30 |
6 files changed, 321 insertions, 188 deletions
diff --git a/protocols/WhatsApp/src/appsync.cpp b/protocols/WhatsApp/src/appsync.cpp new file mode 100644 index 0000000000..9728eae48c --- /dev/null +++ b/protocols/WhatsApp/src/appsync.cpp @@ -0,0 +1,235 @@ +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-22 George Hazan + +*/ + +#include "stdafx.h" + +////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::InitSync() +{ + m_arCollections.insert(new WACollection("regular")); + m_arCollections.insert(new WACollection("regular_high")); + m_arCollections.insert(new WACollection("regular_low")); + m_arCollections.insert(new WACollection("critical_block")); + m_arCollections.insert(new WACollection("critical_unblock_low")); + + for (auto &it : m_arCollections) { + CMStringW wszPath(GetTmpFileName("collection", it->szName)); + wszPath.Append(L".json"); + if (_waccess(wszPath, 0)) + continue; + + JSONNode root = JSONNode::parse(file2string(wszPath)); + it->version = root["version"].as_int(); + + auto szHash = decodeBinStr(root["hash"].as_string()); + if (szHash.size() == sizeof(it->hash.hash)) + memcpy(it->hash.hash, szHash.c_str(), sizeof(it->hash.hash)); + + for (auto &val : root["indexValueMap"]) + it->indexValueMap[decodeBinStr(val.name())] = decodeBinStr(val.as_string()); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnServerSync(const WANode &node) +{ + OBJLIST<WACollection> task(1); + + for (auto &it : node.getChildren()) + if (it->title == "collection") + task.insert(new WACollection(it->getAttr("name"), it->getAttrInt("version"))); + + ResyncServer(task); +} + +void WhatsAppProto::ResyncAll() +{ + setDword("lastResyncTime", time(0)); + ResyncServer(m_arCollections); +} + +void WhatsAppProto::ResyncServer(const OBJLIST<WACollection> &task) +{ + WANodeIq iq(IQ::SET, "w:sync:app:state"); + + auto *pList = iq.addChild("sync"); + for (auto &it : task) { + auto *pCollection = m_arCollections.find(it); + if (pCollection == nullptr) + m_arCollections.insert(pCollection = new WACollection(it->szName, 0)); + + if (!pCollection->version || pCollection->version < it->version) { + auto *pNode = pList->addChild("collection"); + *pNode << CHAR_PARAM("name", it->szName) << INT_PARAM("version", pCollection->version) + << CHAR_PARAM("return_snapshot", (!pCollection->version) ? "true" : "false"); + } + } + + if (pList->getFirstChild() != nullptr) + WSSendNode(iq, &WhatsAppProto::OnIqServerSync); +} + +void WhatsAppProto::OnIqServerSync(const WANode &node) +{ + for (auto &coll : node.getChild("sync")->getChildren()) { + if (coll->title != "collection") + continue; + + auto *pszName = coll->getAttr("name"); + + auto *pCollection = FindCollection(pszName); + if (pCollection == nullptr) { + pCollection = new WACollection(pszName, 0); + m_arCollections.insert(pCollection); + } + + int dwVersion = 0; + + CMStringW wszSnapshotPath(GetTmpFileName("collection", pszName)); + if (auto *pSnapshot = coll->getChild("snapshot")) { + proto::ExternalBlobReference body; + body << pSnapshot->content; + if (!body.has_directpath() || !body.has_mediakey()) { + debugLogA("Invalid snapshot data, skipping"); + continue; + } + + MBinBuffer buf = DownloadEncryptedFile(directPath2url(body.directpath().c_str()), body.mediakey(), "App State"); + if (!buf.data()) { + debugLogA("Invalid downloaded snapshot data, skipping"); + continue; + } + + proto::SyncdSnapshot snapshot; + snapshot << buf; + + dwVersion = snapshot.version().version(); + if (dwVersion > pCollection->version) { + pCollection->hash.init(); + debugLogA("%s: applying snapshot of version %d", pCollection->szName.get(), dwVersion); + for (auto &it : snapshot.records()) + ParsePatch(pCollection, it, true); + } + else debugLogA("%s: skipping snapshot of version %d", pCollection->szName.get(), dwVersion); + } + + if (auto *pPatchList = coll->getChild("patches")) { + for (auto &it : pPatchList->getChildren()) { + proto::SyncdPatch patch; + patch << it->content; + + dwVersion = patch.version().version(); + if (dwVersion > pCollection->version) { + debugLogA("%s: applying patch of version %d", pCollection->szName.get(), dwVersion); + for (auto &jt : patch.mutations()) + ParsePatch(pCollection, jt.record(), jt.operation() == proto::SyncdMutation_SyncdOperation::SyncdMutation_SyncdOperation_SET); + } + else debugLogA("%s: skipping patch of version %d", pCollection->szName.get(), dwVersion); + } + } + + CMStringA szSetting(FORMAT, "Collection_%s", pszName); + // setDword(szSetting, dwVersion); + + JSONNode jsonRoot, jsonMap; + for (auto &it : pCollection->indexValueMap) + jsonMap << CHAR_PARAM(ptrA(mir_base64_encode(it.first.c_str(), it.first.size())), ptrA(mir_base64_encode(it.second.c_str(), it.second.size()))); + jsonRoot << INT_PARAM("version", dwVersion) << CHAR_PARAM("hash", ptrA(mir_base64_encode(pCollection->hash.hash, sizeof(pCollection->hash.hash)))) + << JSON_PARAM("indexValueMap", jsonMap); + + string2file(jsonRoot.write(), GetTmpFileName("collection", CMStringA(pszName) + ".json")); + } +} + +static uint8_t sttMutationInfo[] = "WhatsApp Mutation Keys"; + +void WhatsAppProto::ParsePatch(WACollection *pColl, const ::proto::SyncdRecord &rec, bool bSet) +{ + int id = decodeBigEndian(rec.keyid().id()); + auto &index = rec.index().blob(); + auto &value = rec.value().blob(); + auto &macValue = value.substr(value.size() - 32, value.size()); + + MBinBuffer key(getBlob(CMStringA(FORMAT, "AppSyncKey%d", id))); + if (!key.data()) { + debugLogA("No key with id=%d to decode a patch"); + return; + } + + struct + { + uint8_t indexKey[32]; + uint8_t encKey[32]; + uint8_t macKey[32]; + uint8_t snapshotMacKey[32]; + uint8_t patchMacKey[32]; + + } mutationKeys; + + HKDF(EVP_sha256(), (BYTE *)"", 0, key.data(), key.length(), sttMutationInfo, sizeof(sttMutationInfo) - 1, (BYTE *)&mutationKeys, sizeof(mutationKeys)); + + MBinBuffer decoded = aesDecrypt(EVP_aes_256_cbc(), mutationKeys.encKey, (uint8_t *)value.c_str(), value.c_str() + 16, value.size() - 32); + if (!decoded.data()) { + debugLogA("Unable to decode patch with key id=%d", id); + return; + } + + proto::SyncActionData data; + data << decoded; + + debugLogA("Applying patch for %s{%d}: %s", pColl->szName.get(), data.version(), data.Utf8DebugString().c_str()); + + if (bSet) { + JSONNode jsonRoot = JSONNode::parse(data.index().c_str()); + ApplyPatch(jsonRoot, data.value()); + + pColl->hash.add(macValue.c_str(), macValue.size()); + pColl->indexValueMap[index] = macValue; + } + else { + auto &prevVal = pColl->indexValueMap.find(index); + if (prevVal != pColl->indexValueMap.end()) { + pColl->hash.sub(prevVal->second.c_str(), prevVal->second.size()); + pColl->indexValueMap.erase(prevVal); + } + } +} + +void WhatsAppProto::ApplyPatch(const JSONNode &index, const proto::SyncActionValue &data) +{ + auto title = index.at((json_index_t)0).as_string(); + + if (title == "contact" && data.has_contactaction()) { + WAJid jid(index.at(1).as_string().c_str()); + auto *pUser = AddUser(jid.toString(), false, jid.isGroup()); + + auto &pAction = data.contactaction(); + auto &fullName = pAction.fullname(); + if (!fullName.empty()) + setUString(pUser->hContact, "Nick", fullName.c_str()); + + if (pAction.has_firstname()) { + CMStringA str(pAction.firstname().c_str()); + str.TrimRight(); + setUString(pUser->hContact, "FirstName", str.c_str()); + setUString(pUser->hContact, "LastName", fullName.c_str() + str.GetLength() + 1); + } + else { + size_t idx = fullName.rfind(' '); + if (idx != fullName.npos) { + setUString(pUser->hContact, "FirstName", fullName.substr(0, idx).c_str()); + setUString(pUser->hContact, "LastName", fullName.substr(idx+1, fullName.size()).c_str()); + } + else { + setUString(pUser->hContact, "FirstName", ""); + setUString(pUser->hContact, "LastName", fullName.c_str()); + } + } + } +} diff --git a/protocols/WhatsApp/src/iq.cpp b/protocols/WhatsApp/src/iq.cpp index 2f42dcf2a4..c0a2c414ac 100644 --- a/protocols/WhatsApp/src/iq.cpp +++ b/protocols/WhatsApp/src/iq.cpp @@ -93,7 +93,8 @@ void WhatsAppProto::OnReceiveInfo(const WANode &node) if (auto *pChild = node.getFirstChild()) { if (pChild->title == "offline") { debugLogA("Processed %d offline events", pChild->getAttrInt("count")); - if (m_arCollections.getCount() == 0) { + + if (getDword("lastResyncTime") == 0) { m_impl.m_resyncApp.Stop(); m_impl.m_resyncApp.Start(1000); } @@ -417,7 +418,6 @@ void WhatsAppProto::OnIqPairSuccess(const WANode &node) MBinBuffer accountEnc(account.ByteSize()); account.SerializeToArray(accountEnc.data(), (int)accountEnc.length()); - db_set_blob(0, m_szModuleName, "WAAccount", accountEnc.data(), (int)accountEnc.length()); proto::ADVDeviceIdentity deviceIdentity; deviceIdentity.ParseFromString(deviceDetails); @@ -565,167 +565,6 @@ LBL_Error: ///////////////////////////////////////////////////////////////////////////////////////// -void WhatsAppProto::OnServerSync(const WANode &node) -{ - OBJLIST<WACollection> task(1); - - for (auto &it : node.getChildren()) - if (it->title == "collection") - task.insert(new WACollection(it->getAttr("name"), it->getAttrInt("version"))); - - ResyncServer(task); -} - -void WhatsAppProto::ResyncAll() -{ - OBJLIST<WACollection> task(1); - task.insert(new WACollection("regular", 11)); - task.insert(new WACollection("regular_high", 9)); - task.insert(new WACollection("regular_low", 11)); - task.insert(new WACollection("critical_block", 11)); - task.insert(new WACollection("critical_unblock_low", 11)); - ResyncServer(task); -} - -void WhatsAppProto::ResyncServer(const OBJLIST<WACollection> &task) -{ - WANodeIq iq(IQ::SET, "w:sync:app:state"); - - auto *pList = iq.addChild("sync"); - for (auto &it : task) { - auto *pCollection = m_arCollections.find(it); - if (pCollection == nullptr) - m_arCollections.insert(pCollection = new WACollection(it->szName, 0)); - - if (pCollection->version < it->version) { - auto *pNode = pList->addChild("collection"); - *pNode << CHAR_PARAM("name", it->szName) << INT_PARAM("version", pCollection->version) - << CHAR_PARAM("return_snapshot", (!pCollection->version) ? "true" : "false"); - } - } - - if (pList->getFirstChild() != nullptr) - WSSendNode(iq, &WhatsAppProto::OnIqServerSync); -} - -void WhatsAppProto::OnIqServerSync(const WANode &node) -{ - for (auto &coll : node.getChild("sync")->getChildren()) { - if (coll->title != "collection") - continue; - - auto *pszName = coll->getAttr("name"); - - auto *pCollection = FindCollection(pszName); - if (pCollection == nullptr) { - pCollection = new WACollection(pszName, 0); - m_arCollections.insert(pCollection); - } - - int dwVersion = 0; - - CMStringW wszSnapshotPath(GetTmpFileName("collection", pszName)); - if (auto *pSnapshot = coll->getChild("snapshot")) { - proto::ExternalBlobReference body; - body << pSnapshot->content; - if (!body.has_directpath() || !body.has_mediakey()) { - debugLogA("Invalid snapshot data, skipping"); - continue; - } - - MBinBuffer buf = DownloadEncryptedFile(directPath2url(body.directpath().c_str()), body.mediakey(), "App State"); - if (!buf.data()) { - debugLogA("Invalid downloaded snapshot data, skipping"); - continue; - } - - proto::SyncdSnapshot snapshot; - snapshot << buf; - - dwVersion = snapshot.version().version(); - if (dwVersion > pCollection->version) { - auto &hash = snapshot.mac(); - pCollection->hash.assign(hash.c_str(), hash.size()); - - for (auto &it : snapshot.records()) - ParsePatch(pCollection, it, true); - } - } - - if (auto *pPatchList = coll->getChild("patches")) { - for (auto &it : pPatchList->getChildren()) { - proto::SyncdPatch patch; - patch << it->content; - - dwVersion = patch.version().version(); - if (dwVersion > pCollection->version) - for (auto &jt : patch.mutations()) - ParsePatch(pCollection, jt.record(), jt.operation() == proto::SyncdMutation_SyncdOperation::SyncdMutation_SyncdOperation_SET); - } - } - - CMStringA szSetting(FORMAT, "Collection_%s", pszName); - // setDword(szSetting, dwVersion); - - JSONNode jsonRoot, jsonMap; - for (auto &it : pCollection->indexValueMap) - jsonMap << CHAR_PARAM(ptrA(mir_base64_encode(it.first.c_str(), it.first.size())), ptrA(mir_base64_encode(it.second.c_str(), it.second.size()))); - jsonRoot << INT_PARAM("version", dwVersion) << JSON_PARAM("indexValueMap", jsonMap); - - string2file(jsonRoot.write(), GetTmpFileName("collection", CMStringA(pszName) + ".json")); - } -} - -static char sttMutationInfo[] = "WhatsApp Mutation Keys"; - -void WhatsAppProto::ParsePatch(WACollection *pColl, const ::proto::SyncdRecord &rec, bool bSet) -{ - int id = decodeBigEndian(rec.keyid().id()); - auto &index = rec.index().blob(); - auto &value = rec.value().blob(); - - MBinBuffer key(getBlob(CMStringA(FORMAT, "AppSyncKey%d", id))); - if (!key.data()) { - debugLogA("No key with id=%d to decode a patch"); - return; - } - - struct - { - uint8_t indexKey[32]; - uint8_t encKey[32]; - uint8_t macKey[32]; - uint8_t snapshotMacKey[32]; - uint8_t patchMacKey[32]; - - } mutationKeys; - - HKDF(EVP_sha256(), (BYTE *)"", 0, key.data(), key.length(), (BYTE *)sttMutationInfo, sizeof(sttMutationInfo) - 1, (BYTE*)&mutationKeys, sizeof(mutationKeys)); - - MBinBuffer decoded = aesDecrypt(EVP_aes_256_cbc(), mutationKeys.encKey, (uint8_t *)value.c_str(), value.c_str() + 16, value.size() - 32); - if (!decoded.data()) { - debugLogA("Unable to decode patch with key id=%d", id); - return; - } - - proto::SyncActionData data; - data << decoded; - - debugLogA("Applying patch for %s: %d -> %d", pColl->szName.get(), pColl->version, data.version()); - - if (bSet) { - auto &patchIndex = data.index(); - auto &patchVal = data.value(); - debugLogA("Got patch: %s", patchVal.Utf8DebugString().c_str()); - pColl->indexValueMap[index] = value.substr(0, value.size() - 32); - } - else pColl->indexValueMap.erase(index); - - pColl->version = data.version(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - void WhatsAppProto::OnSuccess(const WANode &) { OnLoggedIn(); diff --git a/protocols/WhatsApp/src/proto.cpp b/protocols/WhatsApp/src/proto.cpp index 75a562c067..701bf8f3a7 100644 --- a/protocols/WhatsApp/src/proto.cpp +++ b/protocols/WhatsApp/src/proto.cpp @@ -61,7 +61,7 @@ WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : HookProtoEvent(ME_OPT_INITIALISE, &WhatsAppProto::OnOptionsInit); - InitCollections(); + InitSync(); InitPopups(); InitPersistentHandlers(); @@ -310,24 +310,3 @@ HANDLE WhatsAppProto::SearchBasic(const wchar_t* id) ForkThread(&WhatsAppProto::SearchAckThread, param); return (HANDLE)param->id; } - -////////////////////////////////////////////////////////////////////////////// - -static int enumCollections(const char *szSetting, void *param) -{ - auto *pList = (LIST<char> *)param; - if (!memcmp(szSetting, "Collection_", 11)) - pList->insert(mir_strdup(szSetting)); - return 0; -} - -void WhatsAppProto::InitCollections() -{ - LIST<char> settings(10); - db_enum_settings(0, enumCollections, m_szModuleName, &settings); - - for (auto &it : settings) { - m_arCollections.insert(new WACollection(it + 11, getDword(it))); - mir_free(it); - } -} diff --git a/protocols/WhatsApp/src/proto.h b/protocols/WhatsApp/src/proto.h index 38d697f5ee..5814aa7538 100644 --- a/protocols/WhatsApp/src/proto.h +++ b/protocols/WhatsApp/src/proto.h @@ -110,7 +110,7 @@ struct WAOwnMessage struct WACollection { - WACollection(const char *_1, int _2) : + WACollection(const char *_1, int _2 = 0) : szName(mir_strdup(_1)), version(_2) {} @@ -118,7 +118,7 @@ struct WACollection ptrA szName; int version; - MBinBuffer hash; + LT_HASH hash; std::map<std::string, std::string> indexValueMap; }; @@ -257,7 +257,8 @@ class WhatsAppProto : public PROTO<WhatsAppProto> // App state management OBJLIST<WACollection> m_arCollections; - void InitCollections(void); + void InitSync(void); + void ApplyPatch(const JSONNode &index, const proto::SyncActionValue &data); void ParsePatch(WACollection *pColl, const proto::SyncdRecord &rec, bool bSet); void ResyncServer(const OBJLIST<WACollection> &task); void ResyncAll(void); diff --git a/protocols/WhatsApp/src/utils.cpp b/protocols/WhatsApp/src/utils.cpp index 455428bd7b..9bbea8a88b 100644 --- a/protocols/WhatsApp/src/utils.cpp +++ b/protocols/WhatsApp/src/utils.cpp @@ -72,6 +72,28 @@ CMStringA WAJid::toString() const ///////////////////////////////////////////////////////////////////////////////////////// +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) { mir_cslock lck(m_csUsers); @@ -193,6 +215,18 @@ int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER pHandler) ///////////////////////////////////////////////////////////////////////////////////////// +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; +} + uint32_t decodeBigEndian(const std::string &buf) { uint32_t ret = 0; @@ -328,6 +362,21 @@ void string2file(const std::string &str, const wchar_t *pwszFileName) } } +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; +} + +///////////////////////////////////////////////////////////////////////////////////////// + CMStringA directPath2url(const char *pszDirectPath) { return CMStringA("https://mmg.whatsapp.net") + pszDirectPath; diff --git a/protocols/WhatsApp/src/utils.h b/protocols/WhatsApp/src/utils.h index 778732e312..4b1d61a69e 100644 --- a/protocols/WhatsApp/src/utils.h +++ b/protocols/WhatsApp/src/utils.h @@ -153,6 +153,7 @@ public: }; ///////////////////////////////////////////////////////////////////////////////////////// +// WAJid struct WAJid { @@ -171,11 +172,38 @@ struct WAJid }; ///////////////////////////////////////////////////////////////////////////////////////// +// LT_HASH + +struct LT_HASH +{ + LT_HASH() + { + init(); + }; + + uint16_t hash[64]; + + void add(const void *pData, size_t len); + void sub(const void *pData, size_t len); + + void init() + { + SecureZeroMemory(hash, sizeof(hash)); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// functions void bin2file(const MBinBuffer &buf, const wchar_t *pwszFileName); + void string2file(const std::string &str, const wchar_t *pwszFileName); +CMStringA file2string(const wchar_t *pwszFileName); + CMStringA directPath2url(const char *pszDirectPath); +std::string decodeBinStr(const std::string &buf); + MBinBuffer aesDecrypt( const EVP_CIPHER *cipher, const uint8_t *key, @@ -186,6 +214,8 @@ MBinBuffer aesDecrypt( uint32_t decodeBigEndian(const std::string &buf); std::string encodeBigEndian(uint32_t num, size_t len = sizeof(uint32_t)); +void rtrim(std::string &str); + void generateIV(uint8_t *iv, int &pVar); __forceinline bool operator<<(MessageLite &msg, const MBinBuffer &buf) |