sw: sign paragraphs and validate

Change-Id: I917ad1460c89183eec38d50de8a0de2d76239ea6
Reviewed-on: https://gerrit.libreoffice.org/41592
Tested-by: Jenkins <ci@libreoffice.org>
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
diff --git a/sw/inc/editsh.hxx b/sw/inc/editsh.hxx
index 411c8ee..f8d40dd 100644
--- a/sw/inc/editsh.hxx
+++ b/sw/inc/editsh.hxx
@@ -374,6 +374,9 @@ public:
    /// Sign the paragraph at the cursor.
    void SignParagraph(SwPaM* pPaM);

    /// Validate paragraph signatures, if any, at the cursor.
    void ValidateParagraphSignatures(bool updateDontRemove);

    void Insert2(SwField const &, const bool bForceExpandHints);

    void UpdateFields( SwField & );   ///< One single field.
@@ -969,6 +972,7 @@ private:
     * the existing nb-space will be removed. Bear this in mind if that problem
     * arises. */
    bool m_bNbspRunNext;    ///< NO-BREAK SPACE state flag passed to and maintained by SvxAutoCorrect::DoAutoCorrect()
    bool m_bIsValidatingParagraphSignature; ///< Prevent nested calls of ValidateParagraphSignatures.
};

inline const sfx2::LinkManager& SwEditShell::GetLinkManager() const
diff --git a/sw/inc/strings.hrc b/sw/inc/strings.hrc
index 9c42670..9e77c73 100644
--- a/sw/inc/strings.hrc
+++ b/sw/inc/strings.hrc
@@ -1337,6 +1337,13 @@
#define STR_MENU_UP                             NC_("STR_MENU_UP", "~Upwards")
#define STR_MENU_DOWN                           NC_("STR_MENU_DOWN", "Do~wnwards")

/*--------------------------------------------------------------------
    Description: Paragraph Signature
 --------------------------------------------------------------------*/
#define STR_VALID                               NC_("STR_VALID", "Valid")
#define STR_INVALID                             NC_("STR_INVALID", "Invalid")
#define STR_SIGNED_BY                           NC_("STR_SIGNED_BY", "Signed-by")

#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/edit/edfcol.cxx b/sw/source/core/edit/edfcol.cxx
index 456d06b..5dd18ba 100644
--- a/sw/source/core/edit/edfcol.cxx
+++ b/sw/source/core/edit/edfcol.cxx
@@ -32,12 +32,18 @@
#include <com/sun/star/text/TextContentAnchorType.hpp>
#include <com/sun/star/text/VertOrientation.hpp>
#include <com/sun/star/text/WrapTextMode.hpp>
#include <com/sun/star/text/XTextField.hpp>
#include <com/sun/star/text/XTextRange.hpp>
#include <com/sun/star/xml/crypto/SEInitializer.hpp>
#include <com/sun/star/rdf/XMetadatable.hpp>

#include <basegfx/matrix/b2dhommatrix.hxx>
#include <comphelper/propertysequence.hxx>
#include <comphelper/propertyvalue.hxx>
#include <comphelper/processfactory.hxx>
#include <comphelper/sequence.hxx>
#include <comphelper/scopeguard.hxx>
#include <comphelper/string.hxx>
#include <editeng/formatbreakitem.hxx>
#include <editeng/unoprnms.hxx>
#include <sfx2/classificationhelper.hxx>
@@ -64,7 +70,11 @@
#include <sfx2/watermarkitem.hxx>
#include <DocumentDrawModelManager.hxx>

#include <unoparagraph.hxx>
#include <unotextrange.hxx>
#include <cppuhelper/bootstrap.hxx>
#include <modeltoviewhelper.hxx>
#include <strings.hrc>

#define WATERMARK_NAME "PowerPlusWaterMarkObject"

@@ -169,6 +179,103 @@ uno::Reference<drawing::XShape> lcl_getWatermark(const uno::Reference<text::XTex
    return uno::Reference<drawing::XShape>();
}

/// Extract the text of the paragraph without any of the fields.
/// TODO: Consider moving to SwTextNode, or extend ModelToViewHelper.
OString lcl_getParagraphBodyText(const uno::Reference<text::XTextContent>& xText)
{
    OUStringBuffer strBuf;
    uno::Reference<container::XEnumerationAccess> xTextPortionEnumerationAccess(xText, uno::UNO_QUERY);
    if (!xTextPortionEnumerationAccess.is())
        return OString();

    uno::Reference<container::XEnumeration> xTextPortions = xTextPortionEnumerationAccess->createEnumeration();
    while (xTextPortions->hasMoreElements())
    {
        uno::Any elem = xTextPortions->nextElement();

        //TODO: Consider including hidden and conditional texts/portions.
        OUString aTextPortionType;
        uno::Reference<beans::XPropertySet> xPropertySet(elem, uno::UNO_QUERY);
        xPropertySet->getPropertyValue(UNO_NAME_TEXT_PORTION_TYPE) >>= aTextPortionType;
        if (aTextPortionType == "Text")
        {
            uno::Reference<text::XTextRange> xTextRange(elem, uno::UNO_QUERY);
            if (xTextRange.is())
                strBuf.append(xTextRange->getString());
        }
    }

    // Cleanup the dummy characters added by fields (which we exclude).
    comphelper::string::remove(strBuf, CH_TXT_ATR_INPUTFIELDSTART);
    comphelper::string::remove(strBuf, CH_TXT_ATR_INPUTFIELDEND);
    comphelper::string::remove(strBuf, CH_TXTATR_BREAKWORD);

    return strBuf.makeStringAndClear().trim().toUtf8();
}

/// Validate and return validation result and signature field display text.
std::pair<bool, OUString>
lcl_MakeParagraphSignatureFieldText(const uno::Reference<frame::XModel>& xModel,
                                    const uno::Reference<css::text::XTextField>& xField,
                                    const OString& utf8Text)
{
    static const OUString metaNS("urn:bails");

    OUString msg = "Invalid Signature";
    bool valid = false;

    const css::uno::Reference<css::rdf::XResource> xSubject(xField, uno::UNO_QUERY);
    std::map<OUString, OUString> aStatements = SwRDFHelper::getStatements(xModel, metaNS, xSubject);
    const auto it = aStatements.find("loext:signature:signature");
    if (it != aStatements.end())
    {
        const sal_Char* pData = utf8Text.getStr();
        const std::vector<unsigned char> data(pData, pData + utf8Text.getLength());

        OString encSignature;
        if (it->second.convertToString(&encSignature, RTL_TEXTENCODING_UTF8, 0))
        {
            const std::vector<unsigned char> sig(svl::crypto::DecodeHexString(encSignature));
            SignatureInformation aInfo(0);
            valid = svl::crypto::Signing::Verify(data, true, sig, aInfo);
            valid = valid && aInfo.nStatus == css::xml::crypto::SecurityOperationStatus_OPERATION_SUCCEEDED;

            msg = SwResId(STR_SIGNED_BY) + ": " + aInfo.ouSubject + ", " + aInfo.ouDateTime + ": ";
            if (valid)
                msg += SwResId(STR_VALID);
            else
                msg += SwResId(STR_INVALID);
        }
    }

    return std::make_pair(valid, msg);
}

/// Updates the signature field text if changed and returns true only iff updated.
bool lcl_UpdateParagraphSignatureField(const uno::Reference<frame::XModel>& xModel,
                                       const uno::Reference<css::text::XTextField>& xField,
                                       const OString& utf8Text)
{
    const std::pair<bool, OUString> res = lcl_MakeParagraphSignatureFieldText(xModel, xField, utf8Text);
    uno::Reference<css::text::XTextRange> xText(xField, uno::UNO_QUERY);
    const OUString curText = xText->getString();
    if (curText != res.second)
    {
        xText->setString(res.second);
        return true;
    }

    return false;
}

void lcl_RemoveParagraphSignatureField(const uno::Reference<css::text::XTextField>& xField)
{
    uno::Reference<css::text::XTextContent> xFieldTextContent(xField, uno::UNO_QUERY);
    uno::Reference<css::text::XTextRange> xParagraph(xFieldTextContent->getAnchor());
    uno::Reference<css::text::XText> xParagraphText(xParagraph->getText(), uno::UNO_QUERY);
    xParagraphText->removeTextContent(xFieldTextContent);
}

} // anonymous namespace

SwTextFormatColl& SwEditShell::GetDfltTextFormatColl() const
@@ -216,7 +323,7 @@ void SwEditShell::SetClassification(const OUString& rName, SfxClassificationPoli
    for (const OUString& rPageStyleName : aUsedPageStyles)
    {
        uno::Reference<beans::XPropertySet> xPageStyle(xStyleFamily->getByName(rPageStyleName), uno::UNO_QUERY);
        OUString aServiceName = "com.sun.star.text.TextField.DocInfo.Custom";
        const OUString aServiceName = "com.sun.star.text.TextField.DocInfo.Custom";
        uno::Reference<lang::XMultiServiceFactory> xMultiServiceFactory(xModel, uno::UNO_QUERY);

        if (bHeaderIsNeeded || bWatermarkIsNeeded || bHadWatermark)
@@ -572,67 +679,119 @@ void SwEditShell::SignParagraph(SwPaM* pPaM)
{
    if (!pPaM)
        return;

    SwDocShell* pDocShell = GetDoc()->GetDocShell();
    if (!pDocShell)
        return;
    SwWrtShell* pCurShell = pDocShell->GetWrtShell();
    if (!pCurShell)
    const SwPosition* pPosStart = pPaM->Start();
    if (!pPosStart)
        return;
    SwTextNode* pNode = pPosStart->nNode.GetNode().GetTextNode();
    if (!pNode)
        return;

    // 1. Get the text (without fields).
    const uno::Reference<text::XTextContent> xParent = SwXParagraph::CreateXParagraph(*pNode->GetDoc(), pNode);
    const OString utf8Text = lcl_getParagraphBodyText(xParent);
    if (utf8Text.isEmpty())
        return;

    // 2. Get certificate and SignatureInformation (needed to show signer name).
    //FIXME: Temporary until the Paragraph Signing Dialog is available.
    uno::Reference<uno::XComponentContext> xComponentContext = cppu::defaultBootstrap_InitialComponentContext();
    uno::Reference<xml::crypto::XSEInitializer> xSEInitializer = xml::crypto::SEInitializer::create(xComponentContext);
    uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext = xSEInitializer->createSecurityContext(OUString());
    uno::Reference<xml::crypto::XSecurityEnvironment> xSecurityEnvironment = xSecurityContext->getSecurityEnvironment();
    uno::Sequence<uno::Reference<security::XCertificate>> aCertificates = xSecurityEnvironment->getPersonalCertificates();
    if (!aCertificates.hasElements())
        return;

    uno::Reference<security::XCertificate> xCert = aCertificates[0];
    if (!xCert.is())
        return;

    // 3. Sign it.
    svl::crypto::Signing signing(xCert);
    signing.AddDataRange(utf8Text.getStr(), utf8Text.getLength());
    OStringBuffer sigBuf;
    if (!signing.Sign(sigBuf))
        return;

    const OString signature = sigBuf.makeStringAndClear();

    // 4. Add metadata
    static const OUString metaNS("urn:bails");
    static const OUString metaFile("bails.rdf");

    uno::Reference<frame::XModel> xModel = pDocShell->GetBaseModel();
    uno::Reference<lang::XMultiServiceFactory> xMultiServiceFactory(xModel, uno::UNO_QUERY);
    uno::Reference<css::text::XTextField> xField(xMultiServiceFactory->createInstance("com.sun.star.text.textfield.MetadataField"), uno::UNO_QUERY);

    uno::Reference<text::XTextContent> xContent(xField, uno::UNO_QUERY);
    xContent->attach(xParent->getAnchor()->getEnd());

    uno::Reference<rdf::XResource> xRes(xField, uno::UNO_QUERY);
    const OUString name = "loext:signature:signature";
    SwRDFHelper::addStatement(xModel, metaNS, metaFile, xRes, name, OStringToOUString(signature, RTL_TEXTENCODING_UTF8, 0));

    const std::pair<bool, OUString> res = lcl_MakeParagraphSignatureFieldText(xModel, xField, utf8Text);
    uno::Reference<css::text::XTextRange> xText(xField, uno::UNO_QUERY);
    xText->setString(res.second);
}

void SwEditShell::ValidateParagraphSignatures(bool updateDontRemove)
{
    SwDocShell* pDocShell = GetDoc()->GetDocShell();
    if (!pDocShell || m_bIsValidatingParagraphSignature)
        return;

    SwPaM* pPaM = GetCursor();
    const SwPosition* pPosStart = pPaM->Start();
    SwTextNode* pNode = pPosStart->nNode.GetNode().GetTextNode();
    if (pNode)
    if (!pNode)
        return;

    const uno::Reference<text::XTextContent> xParent = SwXParagraph::CreateXParagraph(*pNode->GetDoc(), pNode);

    // 1. Get the text (without fields).
    const OString utf8Text = lcl_getParagraphBodyText(xParent);
    if (utf8Text.isEmpty())
        return;

    // 2. For each signature field, update it.
    uno::Reference<container::XEnumerationAccess> xTextPortionEnumerationAccess(xParent, uno::UNO_QUERY);
    if (!xTextPortionEnumerationAccess.is())
        return;

    uno::Reference<frame::XModel> xModel = pDocShell->GetBaseModel();
    uno::Reference<container::XEnumeration> xTextPortions = xTextPortionEnumerationAccess->createEnumeration();
    m_bIsValidatingParagraphSignature = true;
    comphelper::ScopeGuard const g([this] () {
            m_bIsValidatingParagraphSignature = false;
        });

    while (xTextPortions->hasMoreElements())
    {
        // 1. Get the text (without fields).
        const OUString text = pNode->GetText();
        if (text.isEmpty())
            return;
        uno::Reference<beans::XPropertySet> xTextPortion(xTextPortions->nextElement(), uno::UNO_QUERY);
        OUString aTextPortionType;
        xTextPortion->getPropertyValue(UNO_NAME_TEXT_PORTION_TYPE) >>= aTextPortionType;
        if (aTextPortionType != UNO_NAME_TEXT_FIELD)
            continue;

        // 2. Get certificate and SignatureInformation (needed to show signer name).
        //FIXME: Temporary until the Paragraph Signing Dialog is available.
        uno::Reference<uno::XComponentContext> xComponentContext = cppu::defaultBootstrap_InitialComponentContext();
        uno::Reference<xml::crypto::XSEInitializer> xSEInitializer = xml::crypto::SEInitializer::create(xComponentContext);
        uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext = xSEInitializer->createSecurityContext(OUString());
        uno::Reference<xml::crypto::XSecurityEnvironment> xSecurityEnvironment = xSecurityContext->getSecurityEnvironment();
        uno::Sequence<uno::Reference<security::XCertificate>> aCertificates = xSecurityEnvironment->getPersonalCertificates();
        if (!aCertificates.hasElements())
            return;
        uno::Reference<lang::XServiceInfo> xTextField;
        xTextPortion->getPropertyValue(UNO_NAME_TEXT_FIELD) >>= xTextField;
        if (!xTextField->supportsService("com.sun.star.text.textfield.MetadataField"))
            continue;

        SignatureInformation aInfo(0);
        uno::Reference<security::XCertificate> xCert = aCertificates[0];
        if (!xCert.is())
            return;
        uno::Reference<text::XTextField> xContent(xTextField, uno::UNO_QUERY);

        // 3. Sign it.
        svl::crypto::Signing signing(xCert);
        signing.AddDataRange(text.getStr(), text.getLength());
        OStringBuffer sigBuf;
        if (!signing.Sign(sigBuf))
            return;
        const bool isUndoEnabled = GetDoc()->GetIDocumentUndoRedo().DoesUndo();
        GetDoc()->GetIDocumentUndoRedo().DoUndo(false);
        if (updateDontRemove)
            lcl_UpdateParagraphSignatureField(xModel, xContent, utf8Text);
        else if (!lcl_MakeParagraphSignatureFieldText(xModel, xContent, utf8Text).first)
            lcl_RemoveParagraphSignatureField(xContent);

        const OString signature = sigBuf.makeStringAndClear();
        const auto pData = reinterpret_cast<const unsigned char*>(text.getStr());
        const std::vector<unsigned char> data(pData, pData + text.getLength());
        const std::vector<unsigned char> sig(svl::crypto::DecodeHexString(signature));
        if (!svl::crypto::Signing::Verify(data, true, sig, aInfo))
            return;

        // 4. Add metadata
        static const OUString metaNS("urn:bails");
        static const OUString metaFile("bails.rdf");

        std::map<OUString, OUString> aStatements = SwRDFHelper::getTextNodeStatements(metaNS, *pNode);
        OUString name = "loext:signature:index";
        const OUString indexOld = aStatements[name];

        // Update the index
        const OUString index = OUString::number(indexOld.isEmpty() ? 1 : (indexOld.toInt32() + 1));
        SwRDFHelper::updateTextNodeStatement(metaNS, metaFile, *pNode, name, indexOld, index);

        // Add the signature
        name = "loext:signature:signature" + index;
        SwRDFHelper::addTextNodeStatement(metaNS, metaFile, *pNode, name, OStringToOUString(signature, RTL_TEXTENCODING_UTF8, 0));
        GetDoc()->GetIDocumentUndoRedo().DoUndo(isUndoEnabled);
    }
}

diff --git a/sw/source/core/edit/edws.cxx b/sw/source/core/edit/edws.cxx
index 3a719b1..4eb1ece 100644
--- a/sw/source/core/edit/edws.cxx
+++ b/sw/source/core/edit/edws.cxx
@@ -35,13 +35,16 @@

// masqueraded copy constructor
SwEditShell::SwEditShell( SwEditShell& rEdSH, vcl::Window *pWindow )
    : SwCursorShell( rEdSH, pWindow ),
    m_bNbspRunNext(false)   // TODO: would copying that make sense? only if editing continues
    : SwCursorShell( rEdSH, pWindow )
    , m_bNbspRunNext(false)   // TODO: would copying that make sense? only if editing continues
    , m_bIsValidatingParagraphSignature(false)
{
}

SwEditShell::SwEditShell( SwDoc& rDoc, vcl::Window *pWindow, const SwViewOption *pOptions )
    : SwCursorShell( rDoc, pWindow, pOptions ), m_bNbspRunNext(false)
    : SwCursorShell( rDoc, pWindow, pOptions )
    , m_bNbspRunNext(false)
    , m_bIsValidatingParagraphSignature(false)
{
    if (0 < officecfg::Office::Common::Undo::Steps::get())
    {