comphelper/source/misc/docpasswordhelper.cxx |   57 +++++++++++++++++++++
 include/comphelper/docpasswordhelper.hxx     |   17 ++++++
 include/sfx2/objsh.hxx                       |    3 +
 sfx2/source/dialog/securitypage.cxx          |   26 ++++++++-
 sfx2/source/doc/objserv.cxx                  |   31 +++++++++++
 sw/qa/uitest/data/tdf128744.docx             |binary
 sw/qa/uitest/writer_tests7/tdf128744.py      |   71 +++++++++++++++++++++++++++
 sw/source/uibase/uiview/view2.cxx            |   57 +++++++++++++--------
 8 files changed, 237 insertions(+), 25 deletions(-)

New commits:
commit 28c31ba12567f66ccb6a334fd21af10880f4a33b
Author:     László Németh <[email protected]>
AuthorDate: Thu May 5 12:04:47 2022 +0200
Commit:     László Németh <[email protected]>
CommitDate: Fri May 6 12:59:11 2022 +0200

    tdf#128744 sw DOCX: unprotect change tracking with verification
    
    Unprotect change tracking only by password verification
    instead of 1) unprotecting without the password or 2) rejecting
    the correct password.
    
    I.e. now 1) clicking on Record changes icon of Track Changes
    toolbar or Edit->Track Changes->Record asks for a password, and
    2) Unprotect Record changes on Security page of
    File->Properties... accepts the correct password with disabling
    record changes.
    
    Show also "Invalid password!" dialog disabling Record Changes
    by its icon or menu option, like Properties... dialog window does,
    if the password is invalid.
    
    Note: Still allow to unprotect OpenDocument export of a
    protected OOXML import document, because that doesn't contain
    the original password info, only a dummy RedlinePassword.
    (OpenDocument exports protect Track Changes with the simple
    RedlineProtectionKey configuration setting, so it's not
    possible to map the OOXML password info to OpenDocument without
    extending this.)
    
    Follow-up to commit d416250f4f1766e2d596ea3feef6a94b7adf29f4
    "tdf#106843 DOCX: forbid disabling protected Record Changes".
    
    See also commit bfd7730f4cf002a79dc9c02c23286850fee3f12a
    "tdf#89383 DOCX import: fix permission for editing".
    
    Change-Id: Iafcf4a6b551a7e8485d4311aee889c2522526d71
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/133894
    Tested-by: Jenkins
    Reviewed-by: László Németh <[email protected]>

diff --git a/comphelper/source/misc/docpasswordhelper.cxx 
b/comphelper/source/misc/docpasswordhelper.cxx
index e894b0d77bb7..5edb3949c977 100644
--- a/comphelper/source/misc/docpasswordhelper.cxx
+++ b/comphelper/source/misc/docpasswordhelper.cxx
@@ -137,6 +137,63 @@ 
DocPasswordHelper::GenerateNewModifyPasswordInfoOOXML(std::u16string_view aPassw
 }
 
 
+uno::Sequence< beans::PropertyValue > DocPasswordHelper::ConvertPasswordInfo( 
const uno::Sequence< beans::PropertyValue >& aInfo )
+{
+    uno::Sequence< beans::PropertyValue > aResult;
+    OUString sAlgorithm, sHash, sSalt, sCount;
+    sal_Int32 nAlgorithm = 0;
+
+    for ( const auto & prop : aInfo )
+    {
+        if ( prop.Name == "cryptAlgorithmSid" )
+        {
+            prop.Value >>= sAlgorithm;
+            nAlgorithm = sAlgorithm.toInt32();
+        }
+        else if ( prop.Name == "salt" )
+            prop.Value >>= sSalt;
+        else if ( prop.Name == "cryptSpinCount" )
+            prop.Value >>= sCount;
+        else if ( prop.Name == "hash" )
+            prop.Value >>= sHash;
+    }
+
+    if (nAlgorithm == 1)
+        sAlgorithm = "MD2";
+    else if (nAlgorithm == 2)
+        sAlgorithm = "MD4";
+    else if (nAlgorithm == 3)
+        sAlgorithm = "MD5";
+    else if (nAlgorithm == 4)
+        sAlgorithm = "SHA-1";
+    else if (nAlgorithm == 5)
+        sAlgorithm = "MAC";
+    else if (nAlgorithm == 6)
+        sAlgorithm = "RIPEMD";
+    else if (nAlgorithm == 7)
+        sAlgorithm = "RIPEMD-160";
+    else if (nAlgorithm == 9)
+        sAlgorithm = "HMAC";
+    else if (nAlgorithm == 12)
+        sAlgorithm = "SHA-256";
+    else if (nAlgorithm == 13)
+        sAlgorithm = "SHA-384";
+    else if (nAlgorithm == 14)
+        sAlgorithm = "SHA-512";
+
+    if ( !sCount.isEmpty() )
+    {
+        sal_Int32 nCount = sCount.toInt32();
+        aResult = { comphelper::makePropertyValue("algorithm-name", 
sAlgorithm),
+                    comphelper::makePropertyValue("salt", sSalt),
+                    comphelper::makePropertyValue("iteration-count", nCount),
+                    comphelper::makePropertyValue("hash", sHash) };
+    }
+
+    return aResult;
+}
+
+
 bool DocPasswordHelper::IsModifyPasswordCorrect( std::u16string_view 
aPassword, const uno::Sequence< beans::PropertyValue >& aInfo )
 {
     bool bResult = false;
diff --git a/include/comphelper/docpasswordhelper.hxx 
b/include/comphelper/docpasswordhelper.hxx
index 0b9e646bd2b8..64d7ba9782ec 100644
--- a/include/comphelper/docpasswordhelper.hxx
+++ b/include/comphelper/docpasswordhelper.hxx
@@ -113,6 +113,23 @@ public:
     static css::uno::Sequence< css::beans::PropertyValue >
         GenerateNewModifyPasswordInfo( std::u16string_view aPassword );
 
+    /** This helper function converts a grab-bagged password, e.g. the
+        trackChanges password which has no complete inner equivalent to
+        the inner format. The result sequence contains the hash and the
+        algorithm-related info to use e.g. in IsModifyPasswordCorrect().
+
+        @param aInfo
+            The sequence containing the hash and the algorithm-related info
+            according to the OOXML origin, used by grab-bagging.
+
+        @return
+            The sequence containing the hash and the algorithm-related info
+            in the inner format.
+      */
+
+    static css::uno::Sequence< css::beans::PropertyValue > ConvertPasswordInfo(
+        const css::uno::Sequence< css::beans::PropertyValue >& aInfo );
+
     static css::uno::Sequence<css::beans::PropertyValue>
     GenerateNewModifyPasswordInfoOOXML(std::u16string_view aPassword);
 
diff --git a/include/sfx2/objsh.hxx b/include/sfx2/objsh.hxx
index b7f9e1fa0688..428995bff493 100644
--- a/include/sfx2/objsh.hxx
+++ b/include/sfx2/objsh.hxx
@@ -781,6 +781,9 @@ public:
     /// Gets the certificate that is already picked by the user but not yet 
used for signing.
     css::uno::Reference<css::security::XCertificate> GetSignPDFCertificate() 
const;
 
+    /// Gets grab-bagged password info to unprotect change tracking with 
verification
+    css::uno::Sequence< css::beans::PropertyValue > 
GetDocumentProtectionFromGrabBag() const;
+
     // Lock all unlocked views, and returns a guard object which unlocks those 
views when destructed
     virtual std::unique_ptr<LockAllViewsGuard> LockAllViews()
     {
diff --git a/sfx2/source/dialog/securitypage.cxx 
b/sfx2/source/dialog/securitypage.cxx
index 0ff73b44f6f9..39bdc7cb1eba 100644
--- a/sfx2/source/dialog/securitypage.cxx
+++ b/sfx2/source/dialog/securitypage.cxx
@@ -33,6 +33,7 @@
 #include <svl/poolitem.hxx>
 #include <svl/intitem.hxx>
 #include <svl/PasswordHelper.hxx>
+#include <comphelper/docpasswordhelper.hxx>
 
 #include <sfx2/strings.hrc>
 
@@ -114,11 +115,28 @@ static bool lcl_IsPasswordCorrect( std::u16string_view 
rPassword )
     pCurDocShell->GetProtectionHash( aPasswordHash );
 
     // check if supplied password was correct
-    uno::Sequence< sal_Int8 > aNewPasswd( aPasswordHash );
-    SvPasswordHelper::GetHashPassword( aNewPasswd, rPassword );
-    if (SvPasswordHelper::CompareHashPassword( aPasswordHash, rPassword ))
-        bRes = true;    // password was correct
+    if (aPasswordHash.getLength() == 1 && aPasswordHash[0] == 1)
+    {
+        // dummy RedlinePassword from OOXML import: get real password info
+        // from the grab-bag to verify the password
+        const css::uno::Sequence< css::beans::PropertyValue > 
aDocumentProtection =
+                                                
pCurDocShell->GetDocumentProtectionFromGrabBag();
+        bRes =
+            // password is ok, if there is no DocumentProtection in the 
GrabBag,
+            // i.e. the dummy RedlinePassword imported from an OpenDocument 
file
+            !aDocumentProtection.hasElements() ||
+            // verify password with the password info imported from OOXML
+            ::comphelper::DocPasswordHelper::IsModifyPasswordCorrect( 
rPassword,
+                    ::comphelper::DocPasswordHelper::ConvertPasswordInfo ( 
aDocumentProtection ) );
+    }
     else
+    {
+        uno::Sequence< sal_Int8 > aNewPasswd( aPasswordHash );
+        SvPasswordHelper::GetHashPassword( aNewPasswd, rPassword );
+        bRes = SvPasswordHelper::CompareHashPassword( aPasswordHash, rPassword 
);
+    }
+
+    if ( !bRes )
     {
         std::unique_ptr<weld::MessageDialog> 
xInfoBox(Application::CreateMessageDialog(nullptr,
                                                       VclMessageType::Info, 
VclButtonsType::Ok,
diff --git a/sfx2/source/doc/objserv.cxx b/sfx2/source/doc/objserv.cxx
index 452d059d0c16..3a6a0ceea2a8 100644
--- a/sfx2/source/doc/objserv.cxx
+++ b/sfx2/source/doc/objserv.cxx
@@ -104,6 +104,7 @@
 #include <unotools/ucbstreamhelper.hxx>
 #include <unotools/streamwrap.hxx>
 #include <comphelper/sequenceashashmap.hxx>
+#include <editeng/unoprnms.hxx>
 
 #include <autoredactdialog.hxx>
 
@@ -2097,4 +2098,34 @@ const uno::Sequence<sal_Int8>& 
SfxObjectShell::getUnoTunnelId()
     return theSfxObjectShellUnoTunnelId.getSeq();
 }
 
+uno::Sequence< beans::PropertyValue > 
SfxObjectShell::GetDocumentProtectionFromGrabBag() const
+{
+    uno::Reference<frame::XModel> xModel = GetBaseModel();
+
+    if (!xModel.is())
+    {
+        return uno::Sequence< beans::PropertyValue>();
+    }
+
+    uno::Reference< beans::XPropertySet > xPropSet( xModel, 
uno::UNO_QUERY_THROW );
+    uno::Reference< beans::XPropertySetInfo > xPropSetInfo = 
xPropSet->getPropertySetInfo();
+    const OUString aGrabBagName = UNO_NAME_MISC_OBJ_INTEROPGRABBAG;
+    if ( xPropSetInfo->hasPropertyByName( aGrabBagName ) )
+    {
+        uno::Sequence< beans::PropertyValue > propList;
+        xPropSet->getPropertyValue( aGrabBagName ) >>= propList;
+        for( const auto& rProp : std::as_const(propList) )
+        {
+            if (rProp.Name == "DocumentProtection")
+            {
+                uno::Sequence< beans::PropertyValue > rAttributeList;
+                rProp.Value >>= rAttributeList;
+                return rAttributeList;
+            }
+        }
+    }
+
+    return uno::Sequence< beans::PropertyValue>();
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/qa/uitest/data/tdf128744.docx b/sw/qa/uitest/data/tdf128744.docx
new file mode 100644
index 000000000000..b03bef21baa4
Binary files /dev/null and b/sw/qa/uitest/data/tdf128744.docx differ
diff --git a/sw/qa/uitest/writer_tests7/tdf128744.py 
b/sw/qa/uitest/writer_tests7/tdf128744.py
new file mode 100644
index 000000000000..34201e858675
--- /dev/null
+++ b/sw/qa/uitest/writer_tests7/tdf128744.py
@@ -0,0 +1,71 @@
+# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
+#
+# 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/.
+#
+
+from uitest.framework import UITestCase
+from libreoffice.uno.propertyvalue import mkPropertyValues
+from uitest.uihelper.common import get_state_as_dict
+from uitest.uihelper.common import select_pos
+from uitest.uihelper.common import get_url_for_data_file
+
+class tdf128744(UITestCase):
+
+   def test_tdf128744(self):
+        # load the sample file
+        with self.ui_test.load_file(get_url_for_data_file("tdf128744.docx")):
+
+            # first try to unprotect Record Changes with an invalid password
+
+            with 
self.ui_test.execute_dialog_through_command(".uno:SetDocumentProperties") as 
xDialog:
+                xRecordChangesCheckbox = xDialog.getChild("recordchanges")
+                
self.assertEqual(get_state_as_dict(xRecordChangesCheckbox)["Selected"], "true")
+                xTabs = xDialog.getChild("tabcontrol")
+                select_pos(xTabs, "3")     #tab Security
+                xProtectBtn = xDialog.getChild("protect")
+
+                # No close_button: click on the "Ok" inside to check the 
"Invalid password" infobox
+                with 
self.ui_test.execute_blocking_action(xProtectBtn.executeAction, args=('CLICK', 
()), close_button="") as xPasswordDialog:
+                    
self.assertEqual(get_state_as_dict(xPasswordDialog)["DisplayText"], "Enter 
Password")
+                    xPassword = xPasswordDialog.getChild("pass1ed")
+                    xPassword.executeAction("TYPE", mkPropertyValues({"TEXT": 
"bad password"}))
+                    print(xPasswordDialog.getChildren())
+                    xOkBtn = xPasswordDialog.getChild("ok")
+                    with 
self.ui_test.execute_blocking_action(xOkBtn.executeAction, args=('CLICK', ())) 
as xInfoBox:
+                        # "Invalid password" infobox
+                        
self.assertEqual(get_state_as_dict(xInfoBox)["DisplayText"], 'Information')
+
+            # now open the dialog again and read the properties, Record 
Changes checkbox is still enabled
+            with 
self.ui_test.execute_dialog_through_command(".uno:SetDocumentProperties", 
close_button="cancel") as xDialog:
+                xRecordChangesCheckbox = xDialog.getChild("recordchanges")
+                
self.assertEqual(get_state_as_dict(xRecordChangesCheckbox)["Selected"], "true")
+                xResetBtn = xDialog.getChild("reset")
+                xResetBtn.executeAction("CLICK", tuple())
+
+            # unprotect Record Changes with the valid password "test"
+
+            with 
self.ui_test.execute_dialog_through_command(".uno:SetDocumentProperties") as 
xDialog:
+                xRecordChangesCheckbox = xDialog.getChild("recordchanges")
+                
self.assertEqual(get_state_as_dict(xRecordChangesCheckbox)["Selected"], "true")
+                xTabs = xDialog.getChild("tabcontrol")
+                select_pos(xTabs, "3")     #tab Security
+                xProtectBtn = xDialog.getChild("protect")
+
+                with 
self.ui_test.execute_blocking_action(xProtectBtn.executeAction, args=('CLICK', 
())) as xPasswordDialog:
+                    
self.assertEqual(get_state_as_dict(xPasswordDialog)["DisplayText"], "Enter 
Password")
+                    xPassword = xPasswordDialog.getChild("pass1ed")
+                    # give the correct password
+                    xPassword.executeAction("TYPE", mkPropertyValues({"TEXT": 
"test"}))
+
+            # now open the dialog again and read the properties, Record 
Changes checkbox is disabled now
+            with 
self.ui_test.execute_dialog_through_command(".uno:SetDocumentProperties", 
close_button="cancel") as xDialog:
+                xRecordChangesCheckbox = xDialog.getChild("recordchanges")
+                
self.assertEqual(get_state_as_dict(xRecordChangesCheckbox)["Selected"], "false")
+                xResetBtn = xDialog.getChild("reset")
+                xResetBtn.executeAction("CLICK", tuple())
+
+# vim: set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/sw/source/uibase/uiview/view2.cxx 
b/sw/source/uibase/uiview/view2.cxx
index 2cb38325ba67..970064c6c164 100644
--- a/sw/source/uibase/uiview/view2.cxx
+++ b/sw/source/uibase/uiview/view2.cxx
@@ -117,6 +117,7 @@
 #include <reffld.hxx>
 #include <comphelper/lok.hxx>
 #include <comphelper/string.hxx>
+#include <comphelper/docpasswordhelper.hxx>
 
 #include <PostItMgr.hxx>
 
@@ -682,34 +683,48 @@ void SwView::Execute(SfxRequest &rReq)
                 {
                     OSL_ENSURE( !static_cast<const 
SfxBoolItem*>(pItem)->GetValue(), "SwView::Execute(): password set and 
redlining off doesn't match!" );
 
-                    // dummy password from OOXML import: only confirmation 
dialog
+                    // xmlsec05:    new password dialog
+                    SfxPasswordDialog aPasswdDlg(GetFrameWeld());
+                    aPasswdDlg.SetMinLen(1);
+                    //#i69751# the result of Execute() can be ignored
+                    (void)aPasswdDlg.run();
+                    OUString sNewPasswd(aPasswdDlg.GetPassword());
+
+                    // password verification
+                    bool bPasswordOk = false;
                     if (aPasswd.getLength() == 1 && aPasswd[0] == 1)
                     {
-                        std::unique_ptr<weld::MessageDialog> 
xWarn(Application::CreateMessageDialog(m_pWrtShell->GetView().GetFrameWeld(),
-                                                   VclMessageType::Warning, 
VclButtonsType::YesNo,
-                                                   
SfxResId(RID_SVXSTR_END_REDLINING_WARNING)));
-                        xWarn->set_default_response(RET_NO);
-                        if (xWarn->run() == RET_YES)
-                            rIDRA.SetRedlinePassword(Sequence <sal_Int8> ());
-                        else
-                            break;
+                        // dummy RedlinePassword from OOXML import: get real 
password info
+                        // from the grab-bag to verify the password
+                        const css::uno::Sequence< css::beans::PropertyValue > 
aDocumentProtection =
+                            static_cast<SfxObjectShell*>(GetDocShell())->
+                                                   
GetDocumentProtectionFromGrabBag();
+
+                        bPasswordOk =
+                            // password is ok, if there is no 
DocumentProtection in the GrabBag,
+                            // i.e. the dummy RedlinePassword imported from an 
OpenDocument file
+                            !aDocumentProtection.hasElements() ||
+                            // verify password with the password info imported 
from OOXML
+                            
::comphelper::DocPasswordHelper::IsModifyPasswordCorrect(sNewPasswd,
+                                
::comphelper::DocPasswordHelper::ConvertPasswordInfo ( aDocumentProtection ) );
                     }
                     else
                     {
-                        // xmlsec05:    new password dialog
-                        SfxPasswordDialog aPasswdDlg(GetFrameWeld());
-                        aPasswdDlg.SetMinLen(1);
-                        //#i69751# the result of Execute() can be ignored
-                        (void)aPasswdDlg.run();
-                        OUString sNewPasswd(aPasswdDlg.GetPassword());
+                        // the simplified RedlinePassword
                         Sequence <sal_Int8> aNewPasswd = 
rIDRA.GetRedlinePassword();
                         SvPasswordHelper::GetHashPassword( aNewPasswd, 
sNewPasswd );
-                        if(SvPasswordHelper::CompareHashPassword(aPasswd, 
sNewPasswd))
-                            rIDRA.SetRedlinePassword(Sequence <sal_Int8> ());
-                        else
-                        {   // xmlsec05: message box for wrong password
-                            break;
-                        }
+                        bPasswordOk = 
SvPasswordHelper::CompareHashPassword(aPasswd, sNewPasswd);
+                    }
+
+                    if (bPasswordOk)
+                        rIDRA.SetRedlinePassword(Sequence <sal_Int8> ());
+                    else
+                    {   // xmlsec05: message box for wrong password
+                        std::unique_ptr<weld::MessageDialog> 
xInfoBox(Application::CreateMessageDialog(nullptr,
+                                                      VclMessageType::Info, 
VclButtonsType::Ok,
+                                                      
SfxResId(RID_SVXSTR_INCORRECT_PASSWORD)));
+                        xInfoBox->run();
+                        break;
                     }
                 }
 

Reply via email to