[Libreoffice-commits] online.git: Makefile.am wsd/LOOLWSD.cpp wsd/ProofKey.cpp wsd/ProofKey.hpp wsd/Storage.cpp

Mike Kaganski (via logerrit) logerrit at kemper.freedesktop.org
Thu Nov 21 11:57:11 UTC 2019


 Makefile.am      |    3 
 wsd/LOOLWSD.cpp  |   11 ++
 wsd/ProofKey.cpp |  220 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 wsd/ProofKey.hpp |   32 ++++++++
 wsd/Storage.cpp  |   38 +++++----
 5 files changed, 286 insertions(+), 18 deletions(-)

New commits:
commit a986aabeb1d899b123fa3486008e4ecb2b981642
Author:     Mike Kaganski <mike.kaganski at collabora.com>
AuthorDate: Thu Nov 7 18:24:37 2019 +0300
Commit:     Mike Kaganski <mike.kaganski at collabora.com>
CommitDate: Thu Nov 21 12:56:53 2019 +0100

    Initial implementation of proof-key
    
    Change-Id: I7ab79218ca2af268dd4573cb64c6353dc71b5f03
    Reviewed-on: https://gerrit.libreoffice.org/82232
    Reviewed-by: Mike Kaganski <mike.kaganski at collabora.com>
    Tested-by: Mike Kaganski <mike.kaganski at collabora.com>

diff --git a/Makefile.am b/Makefile.am
index 9bed60626..1035a22e2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -109,7 +109,8 @@ loolwsd_sources = common/Crypto.cpp \
                   wsd/ClientSession.cpp \
                   wsd/FileServer.cpp \
                   wsd/Storage.cpp \
-                  wsd/TileCache.cpp
+                  wsd/TileCache.cpp \
+                  wsd/ProofKey.cpp
 
 loolwsd_SOURCES = $(loolwsd_sources) \
                   $(shared_sources)
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index e7b4f7439..cfa03034a 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -10,6 +10,7 @@
 #include <config.h>
 
 #include "LOOLWSD.hpp"
+#include "ProofKey.hpp"
 
 /* Default host used in the start test URI */
 #define LOOLWSD_TEST_HOST "localhost"
@@ -3002,6 +3003,16 @@ private:
                 LOOLWSD::EditFileExtensions.insert(elem->getAttribute("ext"));
         }
 
+        const auto& proofAttribs = GetProofKeyAttributes();
+        if (!proofAttribs.empty())
+        {
+            // Add proof-key element to wopi-discovery root
+            AutoPtr<Element> keyElem = docXML->createElement("proof-key");
+            for (const auto& attrib : proofAttribs)
+                keyElem->setAttribute(attrib.first, attrib.second);
+            docXML->documentElement()->appendChild(keyElem);
+        }
+
         std::ostringstream ostrXML;
         DOMWriter writer;
         writer.writeNode(ostrXML, docXML);
diff --git a/wsd/ProofKey.cpp b/wsd/ProofKey.cpp
new file mode 100644
index 000000000..d4587dc70
--- /dev/null
+++ b/wsd/ProofKey.cpp
@@ -0,0 +1,220 @@
+/* -*- 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 <config.h>
+
+#include "ProofKey.hpp"
+#include "LOOLWSD.hpp"
+
+#include <cassert>
+#include <chrono>
+#include <cstdlib>
+#include <memory>
+
+#include <Poco/Base64Decoder.h>
+#include <Poco/Base64Encoder.h>
+#include <Poco/BinaryWriter.h>
+#include <Poco/Crypto/RSADigestEngine.h>
+#include <Poco/Crypto/RSAKey.h>
+#include <Poco/Dynamic/Var.h>
+#include <Poco/JSON/Object.h>
+#include <Poco/JSON/Parser.h>
+#include <Poco/LineEndingConverter.h>
+#include <Poco/Net/HTTPClientSession.h>
+#include <Poco/Net/HTTPRequest.h>
+#include <Poco/Net/HTTPResponse.h>
+#include <Poco/Net/NetException.h>
+#include <Poco/StringTokenizer.h>
+#include <Poco/Timestamp.h>
+#include <Poco/URI.h>
+#include <Poco/Util/Application.h>
+
+#include <Log.hpp>
+#include <Util.hpp>
+
+namespace{
+
+class Proof {
+public:
+    Proof();
+    VecOfStringPairs GetProofHeaders(const std::string& access_token, const std::string& uri) const;
+    const VecOfStringPairs& GetProofKeyAttributes() const { return m_aAttribs; }
+private:
+    static std::string ProofKeyPath();
+
+    // Returns .Net tick (=100ns) count since 0001-01-01 00:00:00 Z
+    // See https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks
+    static int64_t DotNetTicks(const std::chrono::system_clock::time_point& utc);
+    // Returns string of bytes to sign and base64-encode
+    // See http://www.wictorwilen.se/sharepoint-2013-building-your-own-wopi-client-part-2
+    static std::string GetProof(const std::string& access_token, const std::string& uri, int64_t ticks);
+    // Signs string of bytes and returns base64-encoded string
+    std::string SignProof(const std::string& proof) const;
+
+    const std::unique_ptr<const Poco::Crypto::RSAKey> m_pKey;
+    VecOfStringPairs m_aAttribs;
+};
+
+Proof::Proof()
+    : m_pKey([]() -> Poco::Crypto::RSAKey* {
+        try
+        {
+            return new Poco::Crypto::RSAKey("", ProofKeyPath());
+        }
+        catch (const Poco::Exception& e)
+        {
+            LOG_ERR("Could not open proof RSA key: " << e.displayText());
+        }
+        catch (const std::exception& e)
+        {
+            LOG_ERR("Could not open proof RSA key: " << e.what());
+        }
+        catch (...)
+        {
+            LOG_ERR("Could not open proof RSA key: unknown exception");
+        }
+        return nullptr;
+    }())
+{
+    if (m_pKey)
+    {
+        {
+            // TODO: This is definitely not correct at the moment. The proof key must be
+            // base64-encoded blob in "unmanaged Microsoft Cryptographic API (CAPI)" format
+            // (as .Net's RSACryptoServiceProvider::ExportScpBlob returns).
+            std::ostringstream oss;
+            Poco::OutputLineEndingConverter lineEndingConv(oss, "");
+            m_pKey->save(&lineEndingConv);
+            std::string sKey = oss.str();
+            const std::string sBegin = "-----BEGIN RSA PUBLIC KEY-----";
+            const std::string sEnd = "-----END RSA PUBLIC KEY-----";
+            auto pos = sKey.find(sBegin);
+            if (pos != std::string::npos)
+                sKey = sKey.substr(pos + sBegin.length());
+            pos = sKey.find(sEnd);
+            if (pos != std::string::npos)
+                sKey = sKey.substr(0, pos);
+            m_aAttribs.emplace_back("value", sKey);
+        }
+        {
+            std::ostringstream oss;
+            // The signature generated contains CRLF line endings.
+            // Use a line ending converter to remove these CRLF
+            Poco::OutputLineEndingConverter lineEndingConv(oss, "");
+            Poco::Base64Encoder encoder(lineEndingConv);
+            const auto m = m_pKey->modulus();
+            encoder << std::string(m.begin(), m.end());
+            encoder.close();
+            m_aAttribs.emplace_back("modulus", oss.str());
+        }
+        {
+            std::ostringstream oss;
+            // The signature generated contains CRLF line endings.
+            // Use a line ending converter to remove these CRLF
+            Poco::OutputLineEndingConverter lineEndingConv(oss, "");
+            Poco::Base64Encoder encoder(lineEndingConv);
+            const auto e = m_pKey->encryptionExponent();
+            encoder << std::string(e.begin(), e.end());
+            encoder.close();
+            m_aAttribs.emplace_back("exponent", oss.str());
+        }
+    }
+}
+
+std::string Proof::ProofKeyPath()
+{
+    const std::string keyPath = LOOLWSD_CONFIGDIR "/proof_key";
+    if (!Poco::File(keyPath).exists())
+    {
+        std::string msg = "Could not find " + keyPath +
+            "\nNo proof-key will be present in discovery."
+            "\nGenerate an RSA key using this command line:"
+            "\n    ssh-keygen -t rsa -N \"\" -f \"" + keyPath + "\"";
+        fprintf(stderr, "%s\n", msg.c_str());
+        LOG_WRN(msg);
+    }
+
+    return keyPath;
+}
+
+int64_t Proof::DotNetTicks(const std::chrono::system_clock::time_point& utc)
+{
+    // Get time point for Unix epoch; unfortunately from_time_t isn't constexpr
+    const auto aUnxEpoch(std::chrono::system_clock::from_time_t(0));
+    const auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(utc - aUnxEpoch);
+    return duration_ns.count() / 100 + 621355968000000000;
+}
+
+std::string Proof::GetProof(const std::string& access_token, const std::string& uri, int64_t ticks)
+{
+    std::string decoded_access_token;
+    Poco::URI::decode(access_token, decoded_access_token);
+    assert(decoded_access_token.size() <= std::numeric_limits<int32_t>::max()
+           && uri.size() <= std::numeric_limits<int32_t>::max());
+    const size_t size = 4 + decoded_access_token.size() + 4 + uri.size() + 4 + 8;
+    Poco::Buffer<char> buffer(size); // allocate enough size
+    buffer.resize(0); // start from empty buffer
+    Poco::MemoryBinaryWriter writer(buffer, Poco::BinaryWriter::NETWORK_BYTE_ORDER);
+    writer << static_cast<int32_t>(decoded_access_token.size())
+           << decoded_access_token
+           << static_cast<int32_t>(uri.size())
+           << uri
+           << int32_t(8)
+           << ticks;
+    assert(buffer.size() == size);
+    return std::string(buffer.begin(), buffer.end());
+}
+
+std::string Proof::SignProof(const std::string& proof) const
+{
+    assert(m_pKey);
+    std::ostringstream ostr;
+    static Poco::Crypto::RSADigestEngine digestEngine(*m_pKey, "SHA256");
+    digestEngine.update(proof.c_str(), proof.length());
+    Poco::Crypto::DigestEngine::Digest digest = digestEngine.signature();
+    // The signature generated contains CRLF line endings.
+    // Use a line ending converter to remove these CRLF
+    Poco::OutputLineEndingConverter lineEndingConv(ostr, "");
+    Poco::Base64Encoder encoder(lineEndingConv);
+    encoder << std::string(digest.begin(), digest.end());
+    encoder.close();
+    return ostr.str();
+}
+
+VecOfStringPairs Proof::GetProofHeaders(const std::string& access_token, const std::string& uri) const
+{
+    VecOfStringPairs vec;
+    if (m_pKey)
+    {
+        int64_t ticks = DotNetTicks(std::chrono::system_clock::now());
+        vec.emplace_back("X-WOPI-TimeStamp", std::to_string(ticks));
+        vec.emplace_back("X-WOPI-Proof", SignProof(GetProof(access_token, uri, ticks)));
+    }
+    return vec;
+}
+
+const Proof& GetProof()
+{
+    static const Proof proof;
+    return proof;
+}
+
+}
+
+VecOfStringPairs GetProofHeaders(const std::string& access_token, const std::string& uri)
+{
+    return GetProof().GetProofHeaders(access_token, uri);
+}
+
+const VecOfStringPairs& GetProofKeyAttributes()
+{
+    return GetProof().GetProofKeyAttributes();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ProofKey.hpp b/wsd/ProofKey.hpp
new file mode 100644
index 000000000..e1d4b63a3
--- /dev/null
+++ b/wsd/ProofKey.hpp
@@ -0,0 +1,32 @@
+/* -*- 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/.
+ */
+
+// WOPI proof management
+#ifndef INCLUDED_PROOFKEY_HPP
+#define INCLUDED_PROOFKEY_HPP
+
+#include <string>
+#include <utility>
+#include <vector>
+
+typedef std::vector<std::pair<std::string, std::string>> VecOfStringPairs;
+
+// Returns pairs <header_name, header_value> to add to request
+// The headers returned are X-WOPI-TimeStamp, X-WOPI-Proof
+// If no proof key, returns empty vector
+// Both parameters are utf-8-encoded strings
+VecOfStringPairs GetProofHeaders(const std::string& access_token, const std::string& uri);
+
+// Returns pairs <attribute, value> to set in proof-key element in discovery xml.
+// If no proof key, returns empty vector
+const VecOfStringPairs& GetProofKeyAttributes();
+
+#endif // INCLUDED_PROOFKEY_HPP
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index 5569763ee..6b5b87daf 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -47,6 +47,7 @@
 #include <Log.hpp>
 #include <Unit.hpp>
 #include <Util.hpp>
+#include "ProofKey.hpp"
 #include <common/FileUtil.hpp>
 #include <common/JsonUtil.hpp>
 
@@ -445,18 +446,18 @@ static void addStorageReuseCookie(Poco::Net::HTTPRequest& request, const std::st
     }
 }
 
-std::string getReuseCookies(const Poco::URI &uriObject)
+void addWopiProof(Poco::Net::HTTPRequest& request, const std::string& access_token)
 {
-    std::string reuseStorageCookies;
-    for (const auto& param : uriObject.getQueryParameters())
-    {
-        if (param.first == "reuse_cookies")
-        {
-            reuseStorageCookies = param.second;
-            break;
-        }
-    }
-    return reuseStorageCookies;
+    for (const auto header : GetProofHeaders(access_token, request.getURI()))
+        request.set(header.first, header.second);
+}
+
+std::map<std::string, std::string> GetQueryParams(const Poco::URI& uri)
+{
+    std::map<std::string, std::string> result;
+    for (const auto& param : uri.getQueryParameters())
+        result.emplace(param);
+    return result;
 }
 
 } // anonymous namespace
@@ -467,7 +468,7 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au
     Poco::URI uriObject(getUri());
     auth.authorizeURI(uriObject);
     const std::string uriAnonym = LOOLWSD::anonymizeUrl(uriObject.toString());
-    std::string reuseStorageCookies = getReuseCookies(uriObject);
+    std::map<std::string, std::string> params = GetQueryParams(uriObject);
 
     LOG_DBG("Getting info for wopi uri [" << uriAnonym << "].");
 
@@ -480,7 +481,8 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au
         auth.authorizeRequest(request);
         addStorageDebugCookie(request);
         if (_reuseCookies)
-            addStorageReuseCookie(request, reuseStorageCookies);
+            addStorageReuseCookie(request, params["reuse_cookies"]);
+        addWopiProof(request, params["access_token"]);
         const auto startTime = std::chrono::steady_clock::now();
 
         std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject));
@@ -676,7 +678,7 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const
     uriObject.setPath(uriObject.getPath() + "/contents");
     auth.authorizeURI(uriObject);
 
-    std::string reuseStorageCookies = getReuseCookies(uriObject);
+    std::map<std::string, std::string> params = GetQueryParams(uriObject);
 
     Poco::URI uriObjectAnonym(getUri());
     uriObjectAnonym.setPath(LOOLWSD::anonymizeUrl(uriObjectAnonym.getPath()) + "/contents");
@@ -705,7 +707,8 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const
         auth.authorizeRequest(request);
         addStorageDebugCookie(request);
         if (_reuseCookies)
-            addStorageReuseCookie(request, reuseStorageCookies);
+            addStorageReuseCookie(request, params["reuse_cookies"]);
+        addWopiProof(request, params["access_token"]);
         psession->sendRequest(request);
 
         Poco::Net::HTTPResponse response;
@@ -772,7 +775,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
     uriObject.setPath(isSaveAs || isRename? uriObject.getPath(): uriObject.getPath() + "/contents");
     auth.authorizeURI(uriObject);
 
-    std::string reuseStorageCookies = getReuseCookies(uriObject);
+    std::map<std::string, std::string> params = GetQueryParams(uriObject);
 
     const std::string uriAnonym = LOOLWSD::anonymizeUrl(uriObject.toString());
 
@@ -854,7 +857,8 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         request.setContentLength(size);
         addStorageDebugCookie(request);
         if (_reuseCookies)
-            addStorageReuseCookie(request, reuseStorageCookies);
+            addStorageReuseCookie(request, params["reuse_cookies"]);
+        addWopiProof(request, params["access_token"]);
         std::ostream& os = psession->sendRequest(request);
 
         std::ifstream ifs(filePath);


More information about the Libreoffice-commits mailing list