diff --git a/Src/Common/Common.vcxitems b/Src/Common/Common.vcxitems index 088dc66..877e49f 100644 --- a/Src/Common/Common.vcxitems +++ b/Src/Common/Common.vcxitems @@ -52,6 +52,7 @@ + diff --git a/Src/Common/Common.vcxitems.filters b/Src/Common/Common.vcxitems.filters index b56e386..a6a7e78 100644 --- a/Src/Common/Common.vcxitems.filters +++ b/Src/Common/Common.vcxitems.filters @@ -107,5 +107,6 @@ + \ No newline at end of file diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Office365.h b/Src/Common/FSecure/C3/Interfaces/Channels/Office365.h new file mode 100644 index 0000000..72a5d0a --- /dev/null +++ b/Src/Common/FSecure/C3/Interfaces/Channels/Office365.h @@ -0,0 +1,233 @@ +#pragma once + +#include "Common/FSecure/WinHttp/HttpClient.h" +#include "Common/FSecure/WinHttp/HttpRequest.h" +#include "Common/FSecure/WinHttp/WebProxy.h" +#include "Common/FSecure/WinHttp/Constants.h" +#include "Common/FSecure/Crypto/String.h" + +namespace FSecure::C3::Interfaces::Channels +{ + /// Abstract of using Office365 API + template + class Office365 + { + public: + /// Public constructor. + /// @param arguments factory arguments. + Office365(ByteView arguments) + : m_InboundDirectionName{ arguments.Read() } + , m_OutboundDirectionName{ arguments.Read() } + , m_Username{ arguments.Read() } + , m_Password{ arguments.Read() } + , m_ClientKey{ arguments.Read() } + { + FSecure::Utils::DisallowChars({ m_InboundDirectionName, m_OutboundDirectionName }, OBF(R"(;/?:@&=+$,)")); + + // Obtain proxy information and store it in the HTTP configuration. + if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty()) + this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy); + + RefreshAccessToken(); + } + + /// Get channel capability. + /// @returns ByteView view of channel capability. + static ByteView GetCapability(); + + protected: + /// Remove one file from server. + /// @param id of task. + void RemoveFile(std::string const& id) + { + auto webClient = HttpClient{ Convert(Derived::ItemEndpont.Decrypt() + SecureString{id}), m_ProxyConfig }; + auto request = CreateAuthRequest(Method::DEL); + auto resp = webClient.Request(request); + + if (resp.GetStatusCode() > 205) + throw std::runtime_error{ OBF("RemoveFile() Error. Task ") + id + OBF(" could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()) }; + } + + /// Removes all file from server. + /// @param ByteView unused. + /// @returns ByteVector empty vector. + void RemoveAllFiles() + { + auto fileList = ListData(); + for (auto& element : fileList.at(OBF("value"))) + RemoveFile(element.at(OBF("id")).get()); + } + + /// Requests a new access token using the refresh token + /// @throws std::exception if token cannot be refreshed. + void RefreshAccessToken() + { + try + { + //Token endpoint + auto webClient = HttpClient{ Convert(Derived::TokenEndpoit.Decrypt()), m_ProxyConfig }; + + auto request = HttpRequest{ Method::POST }; + request.SetHeader(Header::ContentType, OBF(L"application/x-www-form-urlencoded; charset=utf-16")); + + auto requestBody = SecureString{}; + requestBody += OBF("grant_type=password"); + requestBody += OBF("&scope="); + requestBody += Derived::Scope.Decrypt(); + requestBody += OBF("&username="); + requestBody += m_Username.Decrypt(); + requestBody += OBF("&password="); + requestBody += m_Password.Decrypt(); + requestBody += OBF("&client_id="); + requestBody += m_ClientKey.Decrypt(); + + request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() }); + auto resp = webClient.Request(request); + EvaluateResponse(resp, false); + + auto data = json::parse(resp.GetData()); + m_Token = data[OBF("access_token")].get(); + } + catch (std::exception & exception) + { + throw std::runtime_error{ OBF_STR("Cannot refresh token: ") + exception.what() }; + } + } + + /// Check if request was successful. + /// @throws std::exception describing incorrect response if occurred. + void EvaluateResponse(WinHttp::HttpResponse const& resp, bool tryRefreshingToken = true) + { + + if (resp.GetStatusCode() == StatusCode::OK || resp.GetStatusCode() == StatusCode::Created) + return; + + if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time. + { + auto retryAfterHeader = resp.GetHeader(Header::RetryAfter); + auto delay = !retryAfterHeader.empty() ? stoul(retryAfterHeader) : FSecure::Utils::GenerateRandomValue(10, 20); + s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ delay }; + throw std::runtime_error{ OBF("Too many requests") }; + } + + if (resp.GetStatusCode() == StatusCode::Unauthorized) + { + if (tryRefreshingToken) + RefreshAccessToken(); + throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; + } + + if (resp.GetStatusCode() == StatusCode::BadRequest) + throw std::runtime_error{ OBF("Bad Request") }; + + throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; + } + + /// Create request using internally stored token. + /// @param method, request type. + WinHttp::HttpRequest CreateAuthRequest(WinHttp::Method method = WinHttp::Method::GET) + { + auto request = HttpRequest{ method }; + request.SetHeader(Header::Authorization, Convert(OBF_SEC("Bearer ") + m_Token.Decrypt())); + return request; + } + + /// Server will reject request send before s_TimePoint. This method will await to this point plus extra random delay. + /// @param min lower limit of random delay. + /// @param max upper limit of random delay. + void RateLimitDelay(std::chrono::milliseconds min, std::chrono::milliseconds max) + { + if (s_TimePoint.load() > std::chrono::steady_clock::now()) + std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(min, max)); + } + + /// List files on server. + /// @param filter flags to filter data from server. + /// @return json server response. + json ListData(std::string_view filter = {}) + { + auto webClient = HttpClient{ Convert(Derived::ListEndpoint.Decrypt() + SecureString{ filter }), m_ProxyConfig }; + auto request = CreateAuthRequest(); + auto resp = webClient.Request(request); + EvaluateResponse(resp); + + return json::parse(resp.GetData()); + } + + /// In/Out names on the server. + std::string m_InboundDirectionName, m_OutboundDirectionName; + + /// Username, password, client key and token for authentication. + Crypto::String m_Username, m_Password, m_ClientKey, m_Token; + + /// Store any relevant proxy info + WinHttp::WebProxy m_ProxyConfig; + + /// Used to delay every channel instance in case of server rate limit. + /// Set using information from 429 Too Many Requests header. + static std::atomic s_TimePoint; + }; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +template +std::atomic FSecure::C3::Interfaces::Channels::Office365::s_TimePoint = std::chrono::steady_clock::now(); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +template +FSecure::ByteView FSecure::C3::Interfaces::Channels::Office365::GetCapability() +{ + return R"_( +{ + "create": + { + "arguments": + [ + [ + { + "type": "string", + "name": "Input ID", + "min": 4, + "randomize": true, + "description": "Used to distinguish packets for the channel" + }, + { + "type": "string", + "name": "Output ID", + "min": 4, + "randomize": true, + "description": "Used to distinguish packets from the channel" + } + ], + { + "type": "string", + "name": "username", + "min": 1, + "description": "The O365 user" + }, + { + "type": "string", + "name": "password", + "min": 1, + "description": "The user's password" + }, + { + "type": "string", + "name": "Client Key/ID", + "min": 1, + "description": "The GUID of the registered application." + } + ] + }, + "commands": + [ + { + "name": "Clear channel", + "id": 0, + "description": "Clearing old files from server. May increase bandwidth", + "arguments": [] + } + ] +} +)_"; +} diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp index 6ca3b9a..3d334cb 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp +++ b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp @@ -13,35 +13,22 @@ using json = nlohmann::json; using namespace FSecure::StringConversions; using namespace FSecure::WinHttp; -std::atomic FSecure::C3::Interfaces::Channels::OneDrive365RestFile::s_TimePoint = std::chrono::steady_clock::now(); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OneDrive365RestFile(ByteView arguments) - : m_InboundDirectionName{ arguments.Read() } - , m_OutboundDirectionName{ arguments.Read() } - , m_Username{ arguments.Read() } - , m_Password{ arguments.Read() } - , m_ClientKey{ arguments.Read() } -{ - FSecure::Utils::DisallowChars({ m_InboundDirectionName, m_OutboundDirectionName }, OBF(R"(;/?:@&=+$,)")); - - // Obtain proxy information and store it in the HTTP configuration. - if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty()) - this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy); - - RefreshAccessToken(); -} +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RootEndpoint = OBF("https://graph.microsoft.com/v1.0/me/drive/root:/"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::ItemEndpont = OBF("https://graph.microsoft.com/v1.0/me/drive/items/"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::ListEndpoint = OBF("https://graph.microsoft.com/v1.0/me/drive/root/children"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::TokenEndpoit = OBF("https://login.windows.net/organizations/oauth2/v2.0/token"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::Scope = OBF("files.readwrite.all"); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(ByteView data) { - if (s_TimePoint.load() > std::chrono::steady_clock::now()) - std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay)); + RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay); try { // Construct the HTTP request - auto URLwithFilename = OBF("https://graph.microsoft.com/v1.0/me/drive/root:/") + m_OutboundDirectionName + OBF("-") + FSecure::Utils::GenerateRandomString(20) + OBF(".json") + OBF(":/content"); + auto URLwithFilename = RootEndpoint.Decrypt().c_str() + m_OutboundDirectionName + OBF("-") + FSecure::Utils::GenerateRandomString(20) + OBF(".json") + OBF(":/content"); auto webClient = HttpClient{ Convert(URLwithFilename), m_ProxyConfig }; auto request = CreateAuthRequest(Method::PUT); @@ -52,7 +39,7 @@ size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(B fileData[OBF("data")] = cppcodec::base64_rfc4648::encode(&data.front(), chunkSize); auto body = fileData.dump(); - request.SetData(OBF(L"text/plain"), { body.begin(), body.end() }); + request.SetData(ContentType::TextPlain, { body.begin(), body.end() }); EvaluateResponse(webClient.Request(request)); return chunkSize; @@ -67,25 +54,20 @@ size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(B //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::vector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnReceiveFromChannel() { - if (s_TimePoint.load() > std::chrono::steady_clock::now()) - std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay)); + RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay); auto packets = std::vector{}; try { - auto webClient = HttpClient{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children?top=1000&filter=startswith(name,'") + Convert(m_InboundDirectionName) + OBF(L"')"), m_ProxyConfig }; - auto request = CreateAuthRequest(); HttpRequest{}; - auto resp = webClient.Request(request); - EvaluateResponse(resp); - - auto taskDataAsJSON = json::parse(resp.GetData()); + auto fileList = ListData(OBF("?top=1000&filter=startswith(name,'") + m_InboundDirectionName + OBF("')")); // First iterate over the json and populate an array of the files we want. auto elements = std::vector{}; - for (auto& element : taskDataAsJSON.at(OBF("value"))) + for (auto& element : fileList.at(OBF("value"))) { //download the file auto webClientFile = HttpClient{ Convert(element.at(OBF("@microsoft.graph.downloadUrl")).get()), m_ProxyConfig }; + auto request = CreateAuthRequest(); auto resp = webClientFile.Request(request); EvaluateResponse(resp); @@ -114,82 +96,6 @@ std::vector FSecure::C3::Interfaces::Channels::OneDrive365R return packets; } -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RemoveFile(std::string const& id) -{ - auto webClient = HttpClient{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/items/") + Convert(id), m_ProxyConfig }; - auto request = CreateAuthRequest(Method::DEL); - auto resp = webClient.Request(request); - - if (resp.GetStatusCode() > 205) - throw std::runtime_error{ OBF("RemoveFile() Error. Task ") + id + OBF(" could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()) }; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RemoveAllFiles(ByteView) -{ - auto webClient = HttpClient{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children"), m_ProxyConfig }; - auto request = CreateAuthRequest(); - auto resp = webClient.Request(request); - try - { - EvaluateResponse(resp); - } - catch (const std::exception & exception) - { - Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + exception.what(), LogMessage::Severity::Error }); - return {}; - } - - // For each task (under the "value" key), extract the ID, and send a request to delete the task. - auto taskDataAsJSON = nlohmann::json::parse(resp.GetData()); - for (auto& element : taskDataAsJSON.at(OBF("value"))) - try - { - RemoveFile(element.at(OBF("id")).get()); - } - catch (const std::exception& exception) - { - Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + exception.what(), LogMessage::Severity::Error }); - } - - return {}; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RefreshAccessToken() -{ - try - { - //Token endpoint - auto webClient = HttpClient{ OBF(L"https://login.windows.net/organizations/oauth2/v2.0/token"), m_ProxyConfig }; - - auto request = HttpRequest{ Method::POST }; - request.SetHeader(Header::ContentType, OBF(L"application/x-www-form-urlencoded; charset=utf-16")); - - auto requestBody = SecureString{}; - requestBody += OBF("grant_type=password"); - requestBody += OBF("&scope=files.readwrite.all"); - requestBody += OBF("&username="); - requestBody += m_Username.Decrypt(); - requestBody += OBF("&password="); - requestBody += m_Password.Decrypt(); - requestBody += OBF("&client_id="); - requestBody += m_ClientKey.Decrypt(); - - request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() }); - auto resp = webClient.Request(request); - EvaluateResponse(resp, false); - - auto data = json::parse(resp.GetData()); - m_Token = data[OBF("access_token")].get(); - } - catch (std::exception& exception) - { - throw std::runtime_error{ OBF_STR("Cannot refresh token: ") + exception.what() }; - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnRunCommand(ByteView command) { @@ -197,102 +103,16 @@ FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnRu switch (command.Read()) { case 0: - return RemoveAllFiles(command); + try + { + RemoveAllFiles(); + } + catch (std::exception const& e) + { + Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + e.what(), LogMessage::Severity::Error }); + } + return {}; default: return AbstractChannel::OnRunCommand(commandCopy); } } - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::EvaluateResponse(WinHttp::HttpResponse const& resp, bool tryRefreshingToken) -{ - - if (resp.GetStatusCode() == StatusCode::OK || resp.GetStatusCode() == StatusCode::Created) - return; - - if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time. - { - auto retryAfterHeader = resp.GetHeader(Header::RetryAfter); - auto delay = !retryAfterHeader.empty() ? stoul(retryAfterHeader) : FSecure::Utils::GenerateRandomValue(10, 20); - s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ delay }; - throw std::runtime_error{ OBF("Too many requests") }; - } - - if (resp.GetStatusCode() == StatusCode::Unauthorized) - { - if (tryRefreshingToken) - RefreshAccessToken(); - throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; - } - - if (resp.GetStatusCode() == StatusCode::BadRequest) - throw std::runtime_error{ OBF("Bad Request") }; - - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::WinHttp::HttpRequest FSecure::C3::Interfaces::Channels::OneDrive365RestFile::CreateAuthRequest(WinHttp::Method method) -{ - auto request = HttpRequest{ method }; - request.SetHeader(Header::Authorization, Convert(OBF_SEC("Bearer ") + m_Token.Decrypt())); - return request; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::ByteView FSecure::C3::Interfaces::Channels::OneDrive365RestFile::GetCapability() -{ - return R"_( -{ - "create": - { - "arguments": - [ - [ - { - "type": "string", - "name": "Input ID", - "min": 4, - "randomize": true, - "description": "Used to distinguish packets for the channel" - }, - { - "type": "string", - "name": "Output ID", - "min": 4, - "randomize": true, - "description": "Used to distinguish packets from the channel" - } - ], - { - "type": "string", - "name": "username", - "min": 1, - "description": "The O365 user" - }, - { - "type": "string", - "name": "password", - "min": 1, - "description": "The user's password" - }, - { - "type": "string", - "name": "Client Key/ID", - "min": 1, - "description": "The GUID of the registered application." - } - ] - }, - "commands": - [ - { - "name": "Clear channel", - "id": 0, - "description": "Clearing old files from server. May increase bandwidth", - "arguments": [] - } - ] -} -)_"; -} diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h index 1f79bbe..938b53d 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h +++ b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h @@ -1,20 +1,14 @@ #pragma once -#include "Common/FSecure/WinHttp/HttpClient.h" -#include "Common/FSecure/WinHttp/HttpRequest.h" -#include "Common/FSecure/WinHttp/WebProxy.h" -#include "Common/FSecure/WinHttp/Constants.h" -#include "Common/FSecure/Crypto/String.h" +#include "Office365.h" namespace FSecure::C3::Interfaces::Channels { - /// Implementation of the OneDrive 365 REST file Channel. - class OneDrive365RestFile : public Channel + class OneDrive365RestFile : public Channel, public Office365 { public: - /// Public constructor. - /// @param arguments factory arguments. - OneDrive365RestFile(ByteView arguments); + /// Use Office365 constructor. + using Office365::Office365; /// OnSend callback implementation. /// @param blob data to send to Channel. @@ -30,48 +24,17 @@ namespace FSecure::C3::Interfaces::Channels /// @return command result. ByteVector OnRunCommand(ByteView command) override; - /// Get channel capability. - /// @returns ByteView view of channel capability. - static ByteView GetCapability(); - /// Values used as default for channel jitter. 30 ms if unset. Current jitter value can be changed at runtime. /// Set long delay otherwise O365 rate limit will heavily impact channel. constexpr static std::chrono::milliseconds s_MinUpdateDelay = 1000ms, s_MaxUpdateDelay = 1000ms; - protected: - /// Removes all file from server. - /// @param ByteView unused. - /// @returns ByteVector empty vector. - ByteVector RemoveAllFiles(ByteView); + /// Endpoint used to add file to OneDrive + static Crypto::String RootEndpoint; - /// Remove one file from server. - /// @param id of task. - void RemoveFile(std::string const& id); - - /// Requests a new access token using the refresh token - /// @throws std::exception if token cannot be refreshed. - void RefreshAccessToken(); - - /// Check if request was successful. - /// @throws std::exception describing incorrect response if occurred. - void EvaluateResponse(WinHttp::HttpResponse const& resp, bool tryRefreshingToken = true); - - /// Create request using internally stored token. - /// @param method, request type. - WinHttp::HttpRequest CreateAuthRequest(WinHttp::Method method = WinHttp::Method::GET); - - /// In/Out names on the server. - std::string m_InboundDirectionName, m_OutboundDirectionName; - - /// Username, password, client key and token for authentication. - Crypto::String m_Username, m_Password, m_ClientKey, m_Token; - - /// Store any relevant proxy info - WinHttp::WebProxy m_ProxyConfig; - - - /// Used to delay every channel instance in case of server rate limit. - /// Set using information from 429 Too Many Requests header. - static std::atomic s_TimePoint; + /// Endpoints used by Office365 methods. + static Crypto::String ItemEndpont; + static Crypto::String ListEndpoint; + static Crypto::String TokenEndpoit; + static Crypto::String Scope; }; } diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp index db0ef17..527cc7f 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp +++ b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp @@ -8,184 +8,62 @@ #include "Common/FSecure/WinHttp/Constants.h" #include "Common/FSecure/WinHttp/Uri.h" -// Namespaces. +// Namespaces using json = nlohmann::json; using namespace FSecure::StringConversions; using namespace FSecure::WinHttp; -std::atomic FSecure::C3::Interfaces::Channels::Outlook365RestTask::s_TimePoint = std::chrono::steady_clock::now(); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::C3::Interfaces::Channels::Outlook365RestTask::Outlook365RestTask(ByteView arguments) - : m_InboundDirectionName{ arguments.Read() } - , m_OutboundDirectionName{ arguments.Read() } - , m_Username{ arguments.Read() } - , m_Password{ arguments.Read() } - , m_ClientKey{ arguments.Read() } -{ - // Obtain proxy information and store it in the HTTP configuration. - if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty()) - this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy); - - RefreshAccessToken(); -} +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::ItemEndpont = OBF("https://outlook.office.com/api/v2.0/me/tasks/"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::ListEndpoint = OBF("https://outlook.office.com/api/v2.0/me/tasks"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::TokenEndpoit = OBF("https://login.windows.net/organizations/oauth2/v2.0/token/"); +FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::Scope = OBF("https://outlook.office365.com/.default"); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// size_t FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnSendToChannel(ByteView data) { - if (s_TimePoint.load() > std::chrono::steady_clock::now()) - std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay)); + RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay); try { // Construct the HTTP request - HttpClient webClient(OBF(L"https://outlook.office.com/api/v2.0/me/tasks"), m_ProxyConfig); + auto webClient = HttpClient{ Convert(ItemEndpont.Decrypt()), m_ProxyConfig }; + auto request = CreateAuthRequest(Method::POST); - HttpRequest request; // default request is GET - std::string auth = "Bearer " + m_Token; - request.SetHeader(Header::Authorization, Convert(auth)); - request.m_Method = Method::POST; + auto chunkSize = std::min(data.size(), 3 * 1024 * 1024); // Send max 4 MB. base64 will expand data by 4/3. + auto fileData = json(); + fileData[OBF("Subject")] = m_OutboundDirectionName; + fileData[OBF("Body")][OBF("Content")] = cppcodec::base64_rfc4648::encode(&data.front(), data.size()); + fileData[OBF("Body")][OBF("ContentType")] = OBF("Text"); - // For the JSON body, take a simple approach and use only the required fields. - json jsonBody; - jsonBody[OBF("Subject")] = m_OutboundDirectionName; - jsonBody[OBF("Body")][OBF("Content")] = cppcodec::base64_rfc4648::encode(&data.front(), data.size()); - jsonBody[OBF("Body")][OBF("ContentType")] = OBF("Text"); - std::string body = jsonBody.dump(); + auto body = fileData.dump(); request.SetData(ContentType::ApplicationJson, { body.begin(), body.end() }); - request.m_ContentType = L"application/json"; + EvaluateResponse(webClient.Request(request)); - auto resp = webClient.Request(request); - - if (resp.GetStatusCode() != StatusCode::OK && resp.GetStatusCode() != StatusCode::Created) - { - if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time. - { - s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s); - throw std::runtime_error{ OBF("Too many requests") }; - } - if (resp.GetStatusCode() == StatusCode::Unauthorized) - { - RefreshAccessToken(); - throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; - - } - if (resp.GetStatusCode() == StatusCode::BadRequest) - { - RefreshAccessToken(); - throw std::runtime_error{ OBF("Bad Request") }; - } - - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; - } - - return data.size(); + return chunkSize; } - catch (std::exception& exception) + catch (std::exception & exception) { - Log({ OBF_SEC("Caught a std::exception when running OnSend(): ") + exception.what(), LogMessage::Severity::Warning }); + Log({ OBF_SEC("Caught a std::exception when running OnSend(): ") + exception.what(), LogMessage::Severity::Error }); return 0u; } - - return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::vector FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnReceiveFromChannel() { - if (s_TimePoint.load() > std::chrono::steady_clock::now()) - std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay)); + RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay); - std::vector packets; + auto packets = std::vector{}; try { - // Construct request to get tasks. - // Filtered by subjects that start with m_InboundDirectionName, order by oldest first, and fetch 1000 tasks. - // Example: https://outlook.office.com/api/v2.0/me/tasks?top=1000&filter=startswith(Subject,'C2S')&orderby=CreatedDateTime - std::string URLwithInboundDirection = OBF("https://outlook.office.com/api/v2.0/me/tasks?top="); - URLwithInboundDirection += OBF("1000"); // number of tasks to fetch - URLwithInboundDirection += OBF("&filter=startswith(Subject,'"); // filter by subject - URLwithInboundDirection += m_InboundDirectionName; // subject should contain m_InboundDirectionName - URLwithInboundDirection += OBF("')&orderby=CreatedDateTime"); // order by creation date (oldest first) + auto fileList = ListData(OBF("?top=1000&filter=startswith(Subject,'") + m_InboundDirectionName + OBF("')&orderby=CreatedDateTime")); - HttpClient webClient(Convert(URLwithInboundDirection), m_ProxyConfig); + for (auto& element : fileList.at(OBF("value"))) + packets.emplace_back(cppcodec::base64_rfc4648::decode(element.at(OBF("Body")).at(OBF("Content")).get())); - HttpRequest request; // default request is GET - std::string auth = "Bearer " + m_Token; - request.SetHeader(Header::Authorization, Convert(auth)); - auto resp = webClient.Request(request); - - if (resp.GetStatusCode() != StatusCode::OK) - { - if (resp.GetStatusCode() == StatusCode::TooManyRequests) - { - s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s); - throw std::runtime_error{ OBF("Too many requests") }; - } - - if (resp.GetStatusCode() == StatusCode::Unauthorized) - { - RefreshAccessToken(); - throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; - - } - - if (resp.GetStatusCode() == StatusCode::BadRequest) - { - RefreshAccessToken(); - throw std::runtime_error{ OBF("Bad Request") }; - } - - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; - } - - auto responseData = resp.GetData(); - // Gracefully handle situation where there's an empty JSON value (e.g., a failed request) - if (responseData.size() == 0) - return {}; - - // Convert response (as string_t to utf8) and parse. - json taskDataAsJSON; - try - { - taskDataAsJSON = json::parse(responseData); - } - catch (json::parse_error&) - { - Log({ OBF("Failed to parse the list of received tasks."), LogMessage::Severity::Error }); - return {}; - } - - for (auto& element : taskDataAsJSON.at(OBF("value"))) - { - // Obtain subject and task ID. - std::string subject = element.at(OBF("Subject")).get(); - std::string id = element.at(OBF("Id")).get(); - - // Verify that the full subject and ID were obtained. If not, ignore. - if (subject.empty() || id.empty()) - continue; - - // Check the direction component is at the start of subject. - if (subject.find(m_InboundDirectionName)) - continue; - - try - { - // Send the (decoded) message's body. - ByteVector packet = cppcodec::base64_rfc4648::decode(element.at(OBF("Body")).at(OBF("Content")).get()); - packets.emplace_back(packet); - SCOPE_GUARD(RemoveTask(id); ); - } - catch (const cppcodec::parse_error& exception) - { - Log({ OBF("Error decoding task #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error }); - } - catch (std::exception& exception) - { - Log({ OBF("Caught a std::exception when processing task #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error }); - } - } + for (auto& element : fileList.at(OBF("value"))) + RemoveFile(element.at(OBF("Id"))); } catch (std::exception& exception) { @@ -195,170 +73,23 @@ std::vector FSecure::C3::Interfaces::Channels::Outlook365Re return packets; } -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::ByteVector FSecure::C3::Interfaces::Channels::Outlook365RestTask::RemoveAllTasks(ByteView) -{ - - try - { - // Construct request. One minor limitation of this is that it will remove 1000 tasks only (the maximum of "top"). This could be paged. - HttpClient webClient(OBF(L"https://outlook.office.com/api/v2.0/me/tasks?$top=1000"), m_ProxyConfig); - - HttpRequest request; // default request is GET - std::string auth = "Bearer " + m_Token; - request.SetHeader(Header::Authorization, Convert(auth)); - auto resp = webClient.Request(request); - - - if (resp.GetStatusCode() != StatusCode::OK) - { - Log({ OBF("RemoveAllFiles() Error. Files could not be deleted. Confirm access and refresh tokens are correct."), LogMessage::Severity::Error }); - return {}; - } - - // Parse response (list of tasks) - auto taskDataAsJSON = nlohmann::json::parse(resp.GetData()); - - // For each task (under the "value" key), extract the ID, and send a request to delete the task. - for (auto& element : taskDataAsJSON.at(OBF("value"))) - RemoveTask(element.at(OBF("id")).get()); - } - catch (std::exception& exception) - { - Log({ OBF_SEC("Caught a std::exception when running RemoveAllTasks(): ") + exception.what(), LogMessage::Severity::Warning }); - } - - return {}; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void FSecure::C3::Interfaces::Channels::Outlook365RestTask::RemoveTask(std::string const& id) -{ - - // There is a minor logic flaw in this part of the code, as it assumes the access token is still valid, which may not be the case. - auto URLwithID = OBF("https://outlook.office.com/api/v2.0/me/tasks('") + id + OBF("')"); - HttpClient webClient(Convert(URLwithID), m_ProxyConfig); - - HttpRequest request; // default request is GET - std::string auth = "Bearer " + m_Token; - request.SetHeader(Header::Authorization, Convert(auth)); - request.m_Method = Method::DEL; - auto resp = webClient.Request(request); - - if (resp.GetStatusCode() > 205) - Log({ OBF("RemoveTask() Error. Task could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()), LogMessage::Severity::Error }); - - return; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void FSecure::C3::Interfaces::Channels::Outlook365RestTask::RefreshAccessToken() -{ - try - { - //Token endpoint - HttpClient webClient(Convert("https://login.windows.net/organizations/oauth2/v2.0/token"), m_ProxyConfig); - - HttpRequest request; // default request is GET - request.m_Method = Method::POST; - request.SetHeader(Header::ContentType, L"application/x-www-form-urlencoded; charset=utf-16");//ContentType::ApplicationXWwwFormUrlencoded); - json data; - - auto requestBody = ""s; - requestBody += OBF("grant_type=password"); - requestBody += OBF("&scope=https://outlook.office365.com/.default"); - requestBody += OBF("&username="); - requestBody += m_Username; - requestBody += OBF("&password="); - requestBody += m_Password; - requestBody += OBF("&client_id="); - requestBody += m_ClientKey; - - request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() }); - FSecure::Utils::SecureMemzero(requestBody.data(), requestBody.size()); - auto resp = webClient.Request(request); - - if (resp.GetStatusCode() == StatusCode::OK) - data = json::parse(resp.GetData()); - else - throw std::runtime_error{ OBF("Refresh access token request - non-200 status code was received: ") + std::to_string(resp.GetStatusCode()) }; - - m_Token = data["access_token"].get(); - } - catch (std::exception& exception) - { - throw std::runtime_error{ OBF_STR("Cannot refresh token: ") + exception.what() }; - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FSecure::ByteVector FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnRunCommand(ByteView command) { - auto commandCopy = command; //each read moves ByteView. CommandCoppy is needed for default. + auto commandCopy = command; // Each read moves ByteView. CommandCoppy is needed for default. switch (command.Read()) { case 0: - return RemoveAllTasks(command); + try + { + RemoveAllFiles(); + } + catch (std::exception const& e) + { + Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + e.what(), LogMessage::Severity::Error }); + } + return {}; default: return AbstractChannel::OnRunCommand(commandCopy); } } - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::ByteView FSecure::C3::Interfaces::Channels::Outlook365RestTask::GetCapability() -{ - return R"_( -{ - "create": - { - "arguments": - [ - [ - { - "type": "string", - "name": "Input ID", - "min": 4, - "randomize": true, - "description": "Used to distinguish packets for the channel" - }, - { - "type": "string", - "name": "Output ID", - "min": 4, - "randomize": true, - "description": "Used to distinguish packets from the channel" - } - ], - { - "type": "string", - "name": "Username", - "min": 0, - "description": "User with Office 365 subscription." - }, - { - "type": "string", - "name": "Password", - "min": 0, - "description": "User password." - }, - { - "type": "string", - "name": "Client key", - "min": 1, - "description": "Identifies the application (e.g. a GUID). User, or user admin must give consent for application to work in user context." - } - ] - }, - "commands": - [ - { - "name": "Remove all tasks", - "id": 0, - "description": "Clearing old tasks from server may increase bandwidth", - "arguments": [] - } - ] -} -)_"; -} - diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h index a8912c3..31713c4 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h +++ b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h @@ -1,17 +1,15 @@ #pragma once -#include "Common/FSecure/WinHttp/WebProxy.h" -#include "Common/FSecure/WinHttp/Constants.h" //< For CppRestSdk. +#include "Office365.h" namespace FSecure::C3::Interfaces::Channels { /// Implementation of the Outlook365 REST tasks Device. - class Outlook365RestTask : public Channel + class Outlook365RestTask : public Channel, public Office365 { public: - /// Public constructor. - /// @param arguments factory arguments. - Outlook365RestTask(ByteView arguments); + /// Use Office365 constructor. + using Office365::Office365; /// OnSend callback implementation. /// @param blob data to send to Channel. @@ -27,40 +25,15 @@ namespace FSecure::C3::Interfaces::Channels /// @return command result. ByteVector OnRunCommand(ByteView command) override; - /// Get channel capability. - /// @returns ByteView view of channel capability. - static ByteView GetCapability(); - /// Values used as default for channel jitter. 30 ms if unset. Current jitter value can be changed at runtime. /// Set long delay otherwise O365 rate limit will heavily impact channel. constexpr static std::chrono::milliseconds s_MinUpdateDelay = 1000ms, s_MaxUpdateDelay = 1000ms; - protected: - - /// Removes all tasks from server. - /// @param ByteView unused. - /// @returns ByteVector empty vector. - ByteVector RemoveAllTasks(ByteView); - - /// Remove one task from server. - /// @param id of task. - void RemoveTask(std::string const& id); - - /// Requests a new access token using the refresh token - /// @throws std::exception if token cannot be refreshed. - void RefreshAccessToken(); - - /// In/Out names on the server. - std::string m_InboundDirectionName, m_OutboundDirectionName; - - std::string m_Username, m_Password, m_ClientKey, m_Token; - - /// Stores HTTP configuration (proxy, OAuth, etc). - WinHttp::WebProxy m_ProxyConfig; - - /// Used to delay every channel instance in case of server rate limit. - /// Set using information from 429 Too Many Requests header. - static std::atomic s_TimePoint; + /// Endpoints used by Office365 methods. + static Crypto::String ItemEndpont; + static Crypto::String ListEndpoint; + static Crypto::String TokenEndpoit; + static Crypto::String Scope; }; } diff --git a/Src/Common/FSecure/CppTools/StringConversions.h b/Src/Common/FSecure/CppTools/StringConversions.h index 0bec5ef..047b1ac 100644 --- a/Src/Common/FSecure/CppTools/StringConversions.h +++ b/Src/Common/FSecure/CppTools/StringConversions.h @@ -34,7 +34,7 @@ namespace FSecure::StringConversions { /// Detect character type stored by container. template - using CharT = std::remove_reference_t()[0])>; + using CharT = std::remove_const_t()[0])>>; /// Detect view type corresponding to container. template