ucb/CppunitTest_ucb_webdav_core.mk | 14 + ucb/Library_ucpdav1.mk | 19 ++ ucb/source/ucp/webdav-curl/CurlSession.cxx | 13 + ucb/source/ucp/webdav-curl/CurlSession.hxx | 4 ucb/source/ucp/webdav-curl/ImportCookies.cxx | 236 +++++++++++++++++++++++++++ 5 files changed, 286 insertions(+)
New commits: commit e0a66ba5852d480c70507d9f389c17c308324d55 Author: Michael Stahl <[email protected]> AuthorDate: Fri Aug 26 20:55:46 2022 +0200 Commit: Michael Stahl <[email protected]> CommitDate: Fri Aug 26 20:58:37 2022 +0200 ucb: webdav-curl: import Sharepoint FedAuth cookie from Edge Change-Id: I1d22ff039f10304588820388e62701ea1f515a80 diff --git a/ucb/CppunitTest_ucb_webdav_core.mk b/ucb/CppunitTest_ucb_webdav_core.mk index 2d37401da28b..060968e96b61 100644 --- a/ucb/CppunitTest_ucb_webdav_core.mk +++ b/ucb/CppunitTest_ucb_webdav_core.mk @@ -29,6 +29,20 @@ $(eval $(call gb_CppunitTest_use_library_objects,ucb_webdav_core, \ ucpdav1 \ )) +ifeq ($(OS),WNT) +$(eval $(call gb_CppunitTest_add_libs,ucb_webdav_core,\ + $(call gb_UnpackedTarball_get_dir,nss)/dist/out/lib/sqlite3.lib \ +)) +$(eval $(call gb_CppunitTest_use_externals,ucb_webdav_core,\ + nss3 \ +)) +$(eval $(call gb_CppunitTest_use_system_win32_libs,ucb_webdav_core,\ + crypt32 \ + Ole32 \ + shell32 \ +)) +endif + $(eval $(call gb_CppunitTest_use_externals,ucb_webdav_core,\ boost_headers \ libxml2 \ diff --git a/ucb/Library_ucpdav1.mk b/ucb/Library_ucpdav1.mk index 7b8812563e87..91933763f0ec 100644 --- a/ucb/Library_ucpdav1.mk +++ b/ucb/Library_ucpdav1.mk @@ -36,6 +36,24 @@ $(eval $(call gb_Library_use_externals,ucpdav1,\ curl \ )) +ifeq ($(OS),WNT) +$(eval $(call gb_Library_set_include,ucpdav1,\ + $$(INCLUDE) \ + -I$(call gb_UnpackedTarball_get_dir,nss)/dist/private/nss \ +)) +$(eval $(call gb_Library_add_libs,ucpdav1,\ + $(call gb_UnpackedTarball_get_dir,nss)/dist/out/lib/sqlite3.lib \ +)) +$(eval $(call gb_Library_use_externals,ucpdav1,\ + nss3 \ +)) +$(eval $(call gb_Library_use_system_win32_libs,ucpdav1,\ + crypt32 \ + Ole32 \ + shell32 \ +)) +endif + $(eval $(call gb_Library_add_exception_objects,ucpdav1,\ ucb/source/ucp/webdav-curl/ContentProperties \ ucb/source/ucp/webdav-curl/CurlSession \ @@ -45,6 +63,7 @@ $(eval $(call gb_Library_add_exception_objects,ucpdav1,\ ucb/source/ucp/webdav-curl/DAVSessionFactory \ ucb/source/ucp/webdav-curl/DAVTypes \ ucb/source/ucp/webdav-curl/DateTimeHelper \ + ucb/source/ucp/webdav-curl/ImportCookies \ ucb/source/ucp/webdav-curl/PropfindCache \ ucb/source/ucp/webdav-curl/SerfLockStore \ ucb/source/ucp/webdav-curl/UCBDeadPropertyValue \ diff --git a/ucb/source/ucp/webdav-curl/CurlSession.cxx b/ucb/source/ucp/webdav-curl/CurlSession.cxx index c3fd76062e2c..c503b265fcb0 100644 --- a/ucb/source/ucp/webdav-curl/CurlSession.cxx +++ b/ucb/source/ucp/webdav-curl/CurlSession.cxx @@ -707,6 +707,19 @@ CurlSession::CurlSession(uno::Reference<uno::XComponentContext> const& xContext, rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_FORBID_REUSE, 1L); assert(rc == CURLE_OK); } +#ifdef _WIN32 + if (m_URI.GetScheme() == "https") + { + OString const cookies(TryImportCookies(m_xContext, m_URI.GetHost())); + if (!cookies.isEmpty()) + { + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_COOKIEFILE, ""); + assert(rc == CURLE_OK); + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_COOKIE, cookies.getStr()); + assert(rc == CURLE_OK); + } + } +#endif } CurlSession::~CurlSession() {} diff --git a/ucb/source/ucp/webdav-curl/CurlSession.hxx b/ucb/source/ucp/webdav-curl/CurlSession.hxx index 75ccf682692c..5428bb98b23f 100644 --- a/ucb/source/ucp/webdav-curl/CurlSession.hxx +++ b/ucb/source/ucp/webdav-curl/CurlSession.hxx @@ -140,6 +140,10 @@ public: auto NonInteractive_UNLOCK(OUString const& rURI) -> void; }; +OString TryImportCookies( + ::com::sun::star::uno::Reference<::com::sun::star::uno::XComponentContext> const& xContext, + OUString const& rHost); + } // namespace http_dav_ucp /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/ucb/source/ucp/webdav-curl/ImportCookies.cxx b/ucb/source/ucp/webdav-curl/ImportCookies.cxx new file mode 100644 index 000000000000..897299da3c0a --- /dev/null +++ b/ucb/source/ucp/webdav-curl/ImportCookies.cxx @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "CurlSession.hxx" +#include "CurlUri.hxx" + +#include <comphelper/base64.hxx> +#include <comphelper/scopeguard.hxx> +#include <o3tl/char16_t2wchar_t.hxx> + +#include <com/sun/star/xml/crypto/DigestID.hpp> +#include <com/sun/star/xml/crypto/NSSInitializer.hpp> +#include <com/sun/star/uno/Sequence.hxx> + +#include <osl/file.hxx> +#include <sal/log.hxx> +#include <rtl/string.hxx> +#include <rtl/ustring.hxx> + +#ifdef _WIN32 +#include <boost/property_tree/json_parser.hpp> + +#include <sqlite3.h> +#include <pk11pub.h> + +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> +#include <Shlobj.h> +#include <Knownfolders.h> +#include <dpapi.h> +#endif + +using namespace ::com::sun::star; + +namespace http_dav_ucp +{ +#ifdef _WIN32 +struct Value +{ + OString value; + OString encryptedValue; +}; + +static int callback(void* pArg, int argc, char** argv, char** ppColNames) +{ + Value* const pValue(static_cast<Value*>(pArg)); + assert(argc == 3); + assert(strcmp(ppColNames[0], "value") == 0); + assert(strcmp(ppColNames[2], "encrypted_value") == 0); + pValue->value = OString(argv[0]); // base64 has no embedded 0 + auto const len(OString(argv[1]).toInt32()); + assert(len >= 0); + pValue->encryptedValue = OString(argv[2], len); + return 0; +} +#endif + +OString TryImportCookies(uno::Reference<uno::XComponentContext> const& xContext[[maybe_unused]], + OUString const& rHost[[maybe_unused]]) +{ +#ifdef _WIN32 + // Sharepoint authentication: try to find a cookie from Microsoft Edge + PWSTR ladPath; + if (S_OK != SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &ladPath)) + { + SAL_INFO("ucb.ucp.webdav.curl", "ShGetKnownFolderPath failed"); + return OString(); + } + OUString const localAppDirPath(o3tl::toU(ladPath)); + CoTaskMemFree(ladPath); + OUString localAppDirUrl; + ::osl::File::getFileURLFromSystemPath(localAppDirPath, localAppDirUrl); + ::osl::DirectoryItem temp; + OUString dbUrlU = localAppDirUrl + "/Microsoft/Edge/User Data/Default/Network/Cookies"; + if (::osl::DirectoryItem::get(dbUrlU, temp) != osl_File_E_None) + { + dbUrlU = localAppDirUrl + "/Microsoft/Edge/User Data/Default/Cookies"; + if (::osl::DirectoryItem::get(dbUrlU, temp) != osl_File_E_None) + { + SAL_INFO("ucb.ucp.webdav.curl", "no Cookies file"); + return OString(); + } + } + OString const dbUrl(::rtl::OUStringToOString(dbUrlU, RTL_TEXTENCODING_UTF8)); + sqlite3* db; + int rc = sqlite3_open_v2(dbUrl.getStr(), &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, nullptr); + if (rc != SQLITE_OK) + { + SAL_INFO("ucb.ucp.webdav.curl", "sqlite3_open failed: " << sqlite3_errmsg(db)); + sqlite3_close(db); + } + char* err(nullptr); + Value value; + OString const statement("SELECT value, LENGTH(encrypted_value), encrypted_value FROM cookies " + "WHERE name = \"FedAuth\" and host_key = \"" + + ::rtl::OUStringToOString(rHost, RTL_TEXTENCODING_ASCII_US) + "\";"); + rc = sqlite3_exec(db, statement.getStr(), callback, &value, &err); + if (rc != SQLITE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "sqlite3_exec failed: " << err); + sqlite3_free(err); + } + sqlite3_close(db); + if (!value.value.isEmpty()) + { + return value.value; + } + if (value.encryptedValue.getLength() < 3 + 12 + 16) + { + SAL_INFO("ucb.ucp.webdav.curl", "encrypted_value too short"); + return OString(); + } + + OString const iv(value.encryptedValue.copy(3, 12)); + OString const encryptedValue( + value.encryptedValue.copy(3 + 12, value.encryptedValue.getLength() - 3 - 12 - 16)); + OString const tag(value.encryptedValue.copy(value.encryptedValue.getLength() - 16, 16)); + + OUString const stateUrl = localAppDirUrl + "/Microsoft/Edge/User Data/Local State"; + OUString statePathU; + ::osl::File::getSystemPathFromFileURL(stateUrl, statePathU); + OString statePath(::rtl::OUStringToOString(statePathU, RTL_TEXTENCODING_UTF8)); + ::std::string sEncryptedKey; + try + { + ::boost::property_tree::ptree localState; + ::boost::property_tree::read_json(::std::string(statePath.getStr()), localState); + sEncryptedKey = localState.get<std::string>("os_crypt.encrypted_key"); + } + catch (...) + { + SAL_INFO("ucb.ucp.webdav.curl", "failed to parse Local State"); + return OString(); + } + OUString const encodedEncryptedKey(sEncryptedKey.data(), sEncryptedKey.size(), + RTL_TEXTENCODING_UTF8); + uno::Sequence<sal_Int8> decodedEncryptedKey; + ::comphelper::Base64::decode(decodedEncryptedKey, encodedEncryptedKey); + DATA_BLOB protectedKey; + protectedKey.cbData = decodedEncryptedKey.getLength() - 5; + protectedKey.pbData + = reinterpret_cast<BYTE*>(const_cast<sal_Int8*>(decodedEncryptedKey.getConstArray())) + 5; + DATA_BLOB unprotectedKey; + if (CryptUnprotectData(&protectedKey, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &unprotectedKey) + == FALSE) + { + SAL_INFO("ucb.ucp.webdav.curl", "CryptUnprotectData failed"); + assert(false); + return OString(); + } + ::comphelper::ScopeGuard g([&]() { + SecureZeroMemory(unprotectedKey.pbData, unprotectedKey.cbData); + LocalFree(unprotectedKey.pbData); + }); + if (unprotectedKey.cbData < 16) + { + SAL_WARN("ucb.ucp.webdav.curl", "CryptUnprotectData result too small"); + return OString(); + } + + // first, init NSS - but can't do AES GCM via API so do it directly + uno::Reference<xml::crypto::XNSSInitializer> xNSS( + xml::crypto::NSSInitializer::create(xContext)); + xNSS->getDigestContext(xml::crypto::DigestID::SHA256, {}); + SECItem keyItem = { siBuffer, reinterpret_cast<unsigned char*>(unprotectedKey.pbData), + sal::static_int_cast<unsigned>(unprotectedKey.cbData) }; + ::std::unique_ptr<PK11SlotInfo, deleter_from_fn<PK11SlotInfo, PK11_FreeSlot>> pSlot( + PK11_GetBestSlot(CKM_AES_GCM, nullptr)); + if (!pSlot) + { + SAL_WARN("ucb.ucp.webdav.curl", "PK11_GetBestSlot failed"); + return OString(); + } + ::std::unique_ptr<PK11SymKey, deleter_from_fn<PK11SymKey, PK11_FreeSymKey>> pSymKey( + PK11_ImportSymKey(pSlot.get(), CKM_AES_GCM, PK11_OriginDerive, CKA_DECRYPT, &keyItem, + nullptr)); + if (!pSymKey) + { + SAL_WARN("ucb.ucp.webdav.curl", "PK11_ImportSymKey failed"); + return OString(); + } + SECItem dummy = { siBuffer, nullptr, 0 }; + struct ContextDeleter + { + void operator()(PK11Context* p) const { PK11_DestroyContext(p, PR_TRUE); } + }; + ::std::unique_ptr<PK11Context, ContextDeleter> pContext(PK11_CreateContextBySymKey( + CKM_AES_GCM, CKA_NSS_MESSAGE | CKA_DECRYPT, pSymKey.get(), &dummy)); + if (!pContext) + { + SAL_WARN("ucb.ucp.webdav.curl", "PK11_CreateContextBySymKey failed"); + return OString(); + } + + ::std::vector<unsigned char> unencryptedValue; + unencryptedValue.resize(encryptedValue.getLength()); + int outLength(0); + SECStatus rv = PK11_AEADOp( + pContext.get(), CKG_NO_GENERATE, 0, // only used for encryption + reinterpret_cast<unsigned char*>(const_cast<sal_Char*>(iv.getStr())), iv.getLength(), + nullptr, 0, // "additional data" not used + unencryptedValue.data(), &outLength, encryptedValue.getLength(), + reinterpret_cast<unsigned char*>(const_cast<sal_Char*>(tag.getStr())), tag.getLength(), + reinterpret_cast<const unsigned char*>(encryptedValue.getStr()), + encryptedValue.getLength()); + if (rv != SECSuccess) + { + SAL_WARN("ucb.ucp.webdav.curl", "PK11_AEADOp failed"); + return OString(); + } + if (outLength != encryptedValue.getLength()) + { + SAL_WARN("ucb.ucp.webdav.curl", "PK11_AEADOp unexpected output length"); + return OString(); + } + + return "FedAuth=" + + OString(reinterpret_cast<const char*>(unencryptedValue.data()), + unencryptedValue.size()) + + ";"; + +#else + return OString(); +#endif +} + +} // namespace http_dav_ucp + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
