/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * 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 #include "ProofKey.hpp" #include "LOOLWSD.hpp" #include #include #include #include #include #include #include #include #include #include #include "Exceptions.hpp" #include #include namespace{ std::vector getBytesLE(const unsigned char* bytesInHostOrder, const size_t n) { std::vector 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.begin()); #else std::copy_n(bytesInHostOrder, n, ret.rbegin()); #endif return ret; } std::vector getBytesBE(const unsigned char* bytesInHostOrder, const size_t n) { std::vector 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) template std::vector ToLEBytes(const T& x) { return getBytesLE(reinterpret_cast(&x), sizeof(x)); } // Returns passed number as vector of bytes (network order = big-endian) template std::vector ToNetworkOrderBytes(const T& x) { return getBytesBE(reinterpret_cast(&x), sizeof(x)); } } // namespace std::string Proof::BytesToBase64(const std::vector& 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(); } std::vector Proof::Base64ToBytes(const std::string &str) { std::istringstream oss(str); Poco::Base64Decoder decoder(oss); char c = 0; std::vector vec; while (decoder.get(c)) vec.push_back(c); return vec; } void Proof::initialize() { if (m_pKey) { const auto m = m_pKey->modulus(); const auto e = m_pKey->encryptionExponent(); const auto capiBlob = RSA2CapiBlob(m, e); const auto sv = BytesToBase64(capiBlob); const auto sm = BytesToBase64(m); const auto se = BytesToBase64(e); m_aAttribs.emplace_back("value", sv); m_aAttribs.emplace_back("modulus", sm); m_aAttribs.emplace_back("exponent", se); // TODO: implement proper rotation; for now, just duplicate * to old* m_aAttribs.emplace_back("oldvalue", sv); m_aAttribs.emplace_back("oldmodulus", sm); m_aAttribs.emplace_back("oldexponent", se); } } Proof::Proof(Type) : m_pKey(new Poco::Crypto::RSAKey( Poco::Crypto::RSAKey::KeyLength::KL_2048, Poco::Crypto::RSAKey::Exponent::EXP_LARGE)) { initialize(); } Proof::Proof() : m_pKey([]() -> Poco::Crypto::RSAKey* { const auto keyPath = ProofKeyPath(); try { 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:" "\n loolwsd-generate-proof-key" "\nor if your config dir is not /etc, you can run ssh-keygen manually:" "\n ssh-keygen -t rsa -N \"\" -m PEM -f \"" + keyPath + "\"" "\nNote: the proof_key file must be readable by the loolwsd process."; LOG_WRN(msg); } catch (const Poco::Exception& e) { LOG_ERR("Could not open proof RSA key, Poco exception: " << e.displayText()); } catch (const std::exception& e) { LOG_ERR("Could not open proof RSA key, standard exception: " << e.what()); } catch (...) { LOG_ERR("Could not open proof RSA key: unknown exception"); } return nullptr; }()) { initialize(); } std::string Proof::ProofKeyPath() { static const std::string keyPath = #if ENABLE_DEBUG DEBUG_ABSSRCDIR #else LOOLWSD_CONFIGDIR #endif "/proof_key"; return keyPath; } // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb std::vector Proof::RSA2CapiBlob(const std::vector& modulus, const std::vector& exponent) { // Exponent might have arbitrary length in OpenSSL; we need exactly 4 if (exponent.size() > 4) throw ParseError("Proof key public exponent is longer than 4 bytes."); // make sure exponent length is correct; assume we are passed big-endian vectors std::vector exponent32LE(4); std::copy(exponent.rbegin(), exponent.rend(), exponent32LE.begin()); std::vector 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(modulus.size() * 8); capiBlob.reserve(capiBlob.size() + bitLen.size() + exponent32LE.size() + modulus.size()); std::copy(bitLen.begin(), bitLen.end(), std::back_inserter(capiBlob)); // exponent - 4 bytes (little-endian) std::copy(exponent32LE.begin(), exponent32LE.end(), std::back_inserter(capiBlob)); // modulus (passed big-endian, stored little-endian) std::copy(modulus.rbegin(), modulus.rend(), std::back_inserter(capiBlob)); return capiBlob; } 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(utc - aUnxEpoch); return duration_ns.count() / 100 + 621355968000000000; } std::vector Proof::GetProof(const std::string& access_token, const std::string& uri, int64_t ticks) { assert(access_token.size() <= static_cast(std::numeric_limits::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(std::numeric_limits::max())); const auto access_token_size = ToNetworkOrderBytes(access_token.size()); const auto uri_size = ToNetworkOrderBytes(uri_upper.size()); const auto ticks_bytes = ToNetworkOrderBytes(ticks); const auto ticks_size = ToNetworkOrderBytes(ticks_bytes.size()); const size_t size = access_token_size.size() + access_token.size() + uri_size.size() + uri_upper.size() + ticks_size.size() + ticks_bytes.size(); std::vector buf(size); auto pos = std::copy(access_token_size.begin(), access_token_size.end(), buf.begin()); pos = std::copy(access_token.begin(), access_token.end(), pos); pos = std::copy(uri_size.begin(), uri_size.end(), pos); pos = std::copy(uri_upper.begin(), uri_upper.end(), pos); 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& proof) const { assert(m_pKey); // One per DocumentBroker that uses this via WopiStorage static thread_local Poco::Crypto::RSADigestEngine digestEngine(*m_pKey, "SHA256"); digestEngine.reset(); digestEngine.update(proof.data(), proof.size()); return BytesToBase64(digestEngine.signature()); } 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)); const auto sProof = SignProof(GetProof(access_token, uri, ticks)); vec.emplace_back("X-WOPI-Proof", sProof); // TODO: implement proper rotation; for now, just duplicate X-WOPI-Proof to X-WOPI-ProofOld vec.emplace_back("X-WOPI-ProofOld", sProof); } 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: */