2019-11-07 09:24:37 -06:00
|
|
|
/* -*- 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"
|
|
|
|
|
2020-01-21 09:35:01 -06:00
|
|
|
#include <algorithm>
|
2019-11-07 09:24:37 -06:00
|
|
|
#include <cassert>
|
|
|
|
#include <chrono>
|
|
|
|
#include <cstdlib>
|
|
|
|
#include <memory>
|
2020-01-21 09:35:01 -06:00
|
|
|
#include <vector>
|
2019-11-07 09:24:37 -06:00
|
|
|
|
|
|
|
#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{
|
|
|
|
|
2020-02-06 03:21:58 -06:00
|
|
|
std::vector<unsigned char> getBytesLE(const unsigned char* bytesInHostOrder, const size_t n)
|
2020-01-21 09:35:01 -06:00
|
|
|
{
|
|
|
|
std::vector<unsigned char> ret(n);
|
|
|
|
#if !defined __BYTE_ORDER__
|
|
|
|
static_assert(false, "Byte order is not detected on this platform!");
|
|
|
|
#elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
2020-02-06 03:21:58 -06:00
|
|
|
std::copy_n(bytesInHostOrder, n, ret.begin());
|
2020-01-21 09:35:01 -06:00
|
|
|
#else
|
2020-02-06 03:21:58 -06:00
|
|
|
std::copy_n(bytesInHostOrder, n, ret.rbegin());
|
2020-01-21 09:35:01 -06:00
|
|
|
#endif
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2020-02-06 03:21:58 -06:00
|
|
|
std::vector<unsigned char> getBytesBE(const unsigned char* bytesInHostOrder, const size_t n)
|
|
|
|
{
|
|
|
|
std::vector<unsigned char> ret(n);
|
|
|
|
#if !defined __BYTE_ORDER__
|
|
|
|
static_assert(false, "Byte order is not detected on this platform!");
|
|
|
|
#elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
|
|
|
std::copy_n(bytesInHostOrder, n, ret.rbegin());
|
|
|
|
#else
|
|
|
|
std::copy_n(bytesInHostOrder, n, ret.begin());
|
|
|
|
#endif
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns passed number as vector of bytes (little-endian)
|
2020-01-21 09:35:01 -06:00
|
|
|
template <typename T>
|
2020-02-06 03:21:58 -06:00
|
|
|
std::vector<unsigned char> ToLEBytes(const T& x)
|
2020-01-21 09:35:01 -06:00
|
|
|
{
|
|
|
|
return getBytesLE(reinterpret_cast<const unsigned char*>(&x), sizeof(x));
|
|
|
|
}
|
|
|
|
|
2020-02-06 03:21:58 -06:00
|
|
|
// Returns passed number as vector of bytes (network order = big-endian)
|
|
|
|
template <typename T>
|
|
|
|
std::vector<unsigned char> ToNetworkOrderBytes(const T& x)
|
2020-01-21 09:35:01 -06:00
|
|
|
{
|
2020-02-06 03:21:58 -06:00
|
|
|
return getBytesBE(reinterpret_cast<const unsigned char*>(&x), sizeof(x));
|
2020-01-21 09:35:01 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string BytesToBase64(const std::vector<unsigned char>& bytes)
|
|
|
|
{
|
|
|
|
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);
|
|
|
|
encoder << std::string(bytes.begin(), bytes.end());
|
|
|
|
encoder.close();
|
|
|
|
return oss.str();
|
|
|
|
}
|
|
|
|
|
2019-11-07 09:24:37 -06:00
|
|
|
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();
|
|
|
|
|
2020-02-06 03:21:58 -06:00
|
|
|
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb
|
|
|
|
static std::vector<unsigned char> RSA2CapiBlob(const std::vector<unsigned char>& modulus,
|
|
|
|
const std::vector<unsigned char>& exponent);
|
|
|
|
|
2019-11-07 09:24:37 -06:00
|
|
|
// 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);
|
2020-02-06 03:21:58 -06:00
|
|
|
// Returns bytes to sign and base64-encode
|
2019-11-07 09:24:37 -06:00
|
|
|
// See http://www.wictorwilen.se/sharepoint-2013-building-your-own-wopi-client-part-2
|
2020-02-06 03:21:58 -06:00
|
|
|
static std::vector<unsigned char> GetProof(const std::string& access_token,
|
|
|
|
const std::string& uri, int64_t ticks);
|
|
|
|
// Signs bytes and returns base64-encoded string
|
|
|
|
std::string SignProof(const std::vector<unsigned char>& proof) const;
|
2019-11-07 09:24:37 -06:00
|
|
|
|
|
|
|
const std::unique_ptr<const Poco::Crypto::RSAKey> m_pKey;
|
|
|
|
VecOfStringPairs m_aAttribs;
|
|
|
|
};
|
|
|
|
|
|
|
|
Proof::Proof()
|
|
|
|
: m_pKey([]() -> Poco::Crypto::RSAKey* {
|
2020-02-06 03:44:05 -06:00
|
|
|
const auto keyPath = ProofKeyPath();
|
2019-11-07 09:24:37 -06:00
|
|
|
try
|
|
|
|
{
|
2020-02-06 03:44:05 -06:00
|
|
|
return new Poco::Crypto::RSAKey("", keyPath);
|
|
|
|
}
|
|
|
|
catch (const Poco::FileNotFoundException& e)
|
|
|
|
{
|
|
|
|
std::string msg = e.displayText() +
|
|
|
|
"\nNo proof-key will be present in discovery."
|
|
|
|
"\nIf you need to use WOPI security, generate an RSA key using this command line:"
|
|
|
|
"\n ssh-keygen -t rsa -N \"\" -f \"" + keyPath + "\"";
|
|
|
|
LOG_WRN(msg);
|
2019-11-07 09:24:37 -06:00
|
|
|
}
|
|
|
|
catch (const Poco::Exception& e)
|
|
|
|
{
|
2020-02-06 03:44:05 -06:00
|
|
|
LOG_ERR("Could not open proof RSA key: " << e.displayText());
|
2019-11-07 09:24:37 -06:00
|
|
|
}
|
|
|
|
catch (const std::exception& e)
|
|
|
|
{
|
2020-02-06 03:44:05 -06:00
|
|
|
LOG_ERR("Could not open proof RSA key: " << e.what());
|
2019-11-07 09:24:37 -06:00
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
2020-02-06 03:44:05 -06:00
|
|
|
LOG_ERR("Could not open proof RSA key: unknown exception");
|
2019-11-07 09:24:37 -06:00
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}())
|
|
|
|
{
|
|
|
|
if (m_pKey)
|
|
|
|
{
|
2020-01-21 09:35:01 -06:00
|
|
|
const auto m = m_pKey->modulus();
|
|
|
|
const auto e = m_pKey->encryptionExponent();
|
|
|
|
const auto capiBlob = RSA2CapiBlob(m, e);
|
|
|
|
|
|
|
|
m_aAttribs.emplace_back("value", BytesToBase64(capiBlob));
|
|
|
|
m_aAttribs.emplace_back("modulus", BytesToBase64(m));
|
|
|
|
m_aAttribs.emplace_back("exponent", BytesToBase64(e));
|
2019-11-07 09:24:37 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string Proof::ProofKeyPath()
|
|
|
|
{
|
2020-02-14 03:00:42 -06:00
|
|
|
static const std::string keyPath =
|
|
|
|
#if ENABLE_DEBUG
|
|
|
|
DEBUG_ABSSRCDIR
|
|
|
|
#else
|
|
|
|
LOOLWSD_CONFIGDIR
|
|
|
|
#endif
|
|
|
|
"/proof_key";
|
2019-11-07 09:24:37 -06:00
|
|
|
return keyPath;
|
|
|
|
}
|
|
|
|
|
2020-02-06 03:21:58 -06:00
|
|
|
std::vector<unsigned char> Proof::RSA2CapiBlob(const std::vector<unsigned char>& modulus,
|
|
|
|
const std::vector<unsigned char>& exponent)
|
|
|
|
{
|
|
|
|
std::vector<unsigned char> capiBlob = {
|
|
|
|
0x06, 0x02, 0x00, 0x00,
|
|
|
|
0x00, 0xA4, 0x00, 0x00,
|
|
|
|
0x52, 0x53, 0x41, 0x31,
|
|
|
|
};
|
|
|
|
// modulus size in bits - 4 bytes (little-endian)
|
|
|
|
const auto bitLen = ToLEBytes<std::uint32_t>(modulus.size() * 8);
|
|
|
|
capiBlob.reserve(capiBlob.size() + bitLen.size() + exponent.size() + modulus.size());
|
|
|
|
std::copy(bitLen.begin(), bitLen.end(), std::back_inserter(capiBlob));
|
|
|
|
// exponent - 4 bytes (little-endian)
|
|
|
|
std::copy(exponent.rbegin(), exponent.rend(), std::back_inserter(capiBlob));
|
|
|
|
// modulus (little-endian)
|
|
|
|
std::copy(modulus.rbegin(), modulus.rend(), std::back_inserter(capiBlob));
|
|
|
|
return capiBlob;
|
|
|
|
}
|
|
|
|
|
2019-11-07 09:24:37 -06:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-02-06 03:21:58 -06:00
|
|
|
std::vector<unsigned char> Proof::GetProof(const std::string& access_token, const std::string& uri,
|
|
|
|
int64_t ticks)
|
2019-11-07 09:24:37 -06:00
|
|
|
{
|
2020-02-14 12:01:06 -06:00
|
|
|
assert(access_token.size() <= static_cast<size_t>(std::numeric_limits<int32_t>::max()));
|
|
|
|
std::string uri_upper = uri;
|
|
|
|
for (auto& c : uri_upper)
|
|
|
|
if (c >= 'a' && c <= 'z')
|
|
|
|
c -= 'a' - 'A';
|
|
|
|
assert(uri_upper.size() <= static_cast<size_t>(std::numeric_limits<int32_t>::max()));
|
|
|
|
const auto access_token_size = ToNetworkOrderBytes<int32_t>(access_token.size());
|
|
|
|
const auto uri_size = ToNetworkOrderBytes<int32_t>(uri_upper.size());
|
2020-02-06 03:21:58 -06:00
|
|
|
const auto ticks_bytes = ToNetworkOrderBytes(ticks);
|
|
|
|
const auto ticks_size = ToNetworkOrderBytes<int32_t>(ticks_bytes.size());
|
2020-02-14 12:01:06 -06:00
|
|
|
const size_t size = access_token_size.size() + access_token.size()
|
|
|
|
+ uri_size.size() + uri_upper.size() + ticks_size.size()
|
2020-02-06 03:21:58 -06:00
|
|
|
+ ticks_bytes.size();
|
|
|
|
std::vector<unsigned char> buf(size);
|
|
|
|
auto pos = std::copy(access_token_size.begin(), access_token_size.end(), buf.begin());
|
2020-02-14 12:01:06 -06:00
|
|
|
pos = std::copy(access_token.begin(), access_token.end(), pos);
|
2020-02-06 03:21:58 -06:00
|
|
|
pos = std::copy(uri_size.begin(), uri_size.end(), pos);
|
2020-02-14 12:01:06 -06:00
|
|
|
pos = std::copy(uri_upper.begin(), uri_upper.end(), pos);
|
2020-02-06 03:21:58 -06:00
|
|
|
pos = std::copy(ticks_size.begin(), ticks_size.end(), pos);
|
|
|
|
std::copy(ticks_bytes.begin(), ticks_bytes.end(), pos);
|
|
|
|
return buf;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string Proof::SignProof(const std::vector<unsigned char>& proof) const
|
2019-11-07 09:24:37 -06:00
|
|
|
{
|
|
|
|
assert(m_pKey);
|
|
|
|
static Poco::Crypto::RSADigestEngine digestEngine(*m_pKey, "SHA256");
|
2020-02-06 03:21:58 -06:00
|
|
|
digestEngine.update(proof.data(), proof.size());
|
|
|
|
return BytesToBase64(digestEngine.signature());
|
2019-11-07 09:24:37 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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: */
|