comphelper/source/misc/docpasswordhelper.cxx |   33 ++++++++++++++
 include/comphelper/docpasswordhelper.hxx     |    2 
 sfx2/source/dialog/filedlghelper.cxx         |   23 +++++++--
 sw/qa/uitest/writer_tests6/tdf144374.py      |   63 +++++++++++++++++++++++++++
 sw/source/filter/ww8/docxexport.cxx          |   61 +++++++++++++++++++++++++-
 5 files changed, 175 insertions(+), 7 deletions(-)

New commits:
commit 1b53c1dfc76f08ca7df40a0673aa50eca700d072
Author:     Tünde Tóth <toth.tu...@nisz.hu>
AuthorDate: Tue Oct 12 15:11:48 2021 +0200
Commit:     László Németh <nem...@numbertext.org>
CommitDate: Tue Oct 26 11:59:19 2021 +0200

    tdf#144374 DOCX: export the password for editing
    
    The password for editing wasn't exported in DOCX,
    resulting unprotected documents in Writer and
    in MSO 2019 or newer.
    
    Change-Id: Ieb8a339795635bc1c4223ccbe2a40ea85222cc2e
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/123543
    Tested-by: László Németh <nem...@numbertext.org>
    Reviewed-by: László Németh <nem...@numbertext.org>

diff --git a/comphelper/source/misc/docpasswordhelper.cxx 
b/comphelper/source/misc/docpasswordhelper.cxx
index 980faff14698..81ed4bcfed77 100644
--- a/comphelper/source/misc/docpasswordhelper.cxx
+++ b/comphelper/source/misc/docpasswordhelper.cxx
@@ -110,6 +110,39 @@ uno::Sequence< beans::PropertyValue > 
DocPasswordHelper::GenerateNewModifyPasswo
 }
 
 
+uno::Sequence<beans::PropertyValue>
+DocPasswordHelper::GenerateNewModifyPasswordInfoOOXML(std::u16string_view 
aPassword)
+{
+    uno::Sequence<beans::PropertyValue> aResult;
+
+    uno::Sequence<sal_Int8> aSalt = GenerateRandomByteSequence(16);
+    OUStringBuffer aBuffer;
+    comphelper::Base64::encode(aBuffer, aSalt);
+    OUString sSalt = aBuffer.toString();
+
+    sal_Int32 const nIterationCount = 100000;
+    OUString sAlgorithm("SHA-512");
+
+    const OUString sHash(GetOoxHashAsBase64(OUString(aPassword), sSalt, 
nIterationCount,
+                                            
comphelper::Hash::IterCount::APPEND, sAlgorithm));
+
+    if (!sHash.isEmpty())
+    {
+        aResult.realloc(4);
+        aResult[0].Name = "algorithm-name";
+        aResult[0].Value <<= sAlgorithm;
+        aResult[1].Name = "salt";
+        aResult[1].Value <<= sSalt;
+        aResult[2].Name = "iteration-count";
+        aResult[2].Value <<= nIterationCount;
+        aResult[3].Name = "hash";
+        aResult[3].Value <<= 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 bf36cb9250aa..fc40739184bc 100644
--- a/include/comphelper/docpasswordhelper.hxx
+++ b/include/comphelper/docpasswordhelper.hxx
@@ -113,6 +113,8 @@ public:
     static css::uno::Sequence< css::beans::PropertyValue >
         GenerateNewModifyPasswordInfo( std::u16string_view aPassword );
 
+    static css::uno::Sequence<css::beans::PropertyValue>
+    GenerateNewModifyPasswordInfoOOXML(std::u16string_view aPassword);
 
     /** This helper function allows to check whether
         the "Password to modify" provided by user is the correct one.
diff --git a/sfx2/source/dialog/filedlghelper.cxx 
b/sfx2/source/dialog/filedlghelper.cxx
index 99e0de639070..35fc5d166e11 100644
--- a/sfx2/source/dialog/filedlghelper.cxx
+++ b/sfx2/source/dialog/filedlghelper.cxx
@@ -2944,11 +2944,24 @@ ErrCode RequestPassword(const std::shared_ptr<const 
SfxFilter>& pCurrentFilter,
 
         if ( bMSType )
         {
-            // the empty password has 0 as Hash
-            sal_Int32 nHash = SfxMedium::CreatePasswordToModifyHash( 
pPasswordRequest->getPasswordToModify(),
-                                                                     
pCurrentFilter->GetServiceName() == "com.sun.star.text.TextDocument" );
-            if ( nHash )
-                pSet->Put( SfxUnoAnyItem( SID_MODIFYPASSWORDINFO, 
uno::makeAny( nHash ) ) );
+            if (bOOXML)
+            {
+                uno::Sequence<beans::PropertyValue> aModifyPasswordInfo
+                    = 
::comphelper::DocPasswordHelper::GenerateNewModifyPasswordInfoOOXML(
+                        pPasswordRequest->getPasswordToModify());
+                if (aModifyPasswordInfo.hasElements())
+                    pSet->Put(
+                        SfxUnoAnyItem(SID_MODIFYPASSWORDINFO, 
uno::makeAny(aModifyPasswordInfo)));
+            }
+            else
+            {
+                // the empty password has 0 as Hash
+                sal_Int32 nHash = SfxMedium::CreatePasswordToModifyHash(
+                    pPasswordRequest->getPasswordToModify(),
+                    pCurrentFilter->GetServiceName() == 
"com.sun.star.text.TextDocument");
+                if (nHash)
+                    pSet->Put(SfxUnoAnyItem(SID_MODIFYPASSWORDINFO, 
uno::makeAny(nHash)));
+            }
         }
         else
         {
diff --git a/sw/qa/uitest/writer_tests6/tdf144374.py 
b/sw/qa/uitest/writer_tests6/tdf144374.py
new file mode 100644
index 000000000000..c484b79b54cd
--- /dev/null
+++ b/sw/qa/uitest/writer_tests6/tdf144374.py
@@ -0,0 +1,63 @@
+# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
+#
+# 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 org.libreoffice.unotest import systemPathToFileUrl
+from uitest.uihelper.common import select_by_text
+from tempfile import TemporaryDirectory
+import os.path
+
+#Bug 144374 - Writer: FILESAVE to DOCX as read-only with additional password 
protection for editing not working
+
+class tdf144374(UITestCase):
+
+   def test_tdf144374_DOCX(self):
+        with self.ui_test.create_doc_in_start_center("writer"):
+            with TemporaryDirectory() as tempdir:
+                xFilePath = os.path.join(tempdir, "tdf144374-tmp.docx")
+
+                # Save the document
+                with self.ui_test.execute_dialog_through_command(".uno:Save", 
close_button="") as xSaveDialog:
+                    xFileName = xSaveDialog.getChild("file_name")
+                    xFileName.executeAction("TYPE", 
mkPropertyValues({"KEYCODE":"CTRL+A"}))
+                    xFileName.executeAction("TYPE", 
mkPropertyValues({"KEYCODE":"BACKSPACE"}))
+                    xFileName.executeAction("TYPE", mkPropertyValues({"TEXT": 
xFilePath}))
+                    xFileTypeCombo = xSaveDialog.getChild("file_type")
+                    select_by_text(xFileTypeCombo, "Office Open XML Text 
(Transitional) (.docx)")
+                    xPasswordCheckButton = xSaveDialog.getChild("password")
+                    xPasswordCheckButton.executeAction("CLICK", tuple())
+                    xOpen = xSaveDialog.getChild("open")
+
+                    with self.ui_test.execute_dialog_through_action(xOpen, 
"CLICK") as xPasswordDialog:
+                        xReadonly = xPasswordDialog.getChild("readonly")
+                        xReadonly.executeAction("CLICK", tuple())
+                        xNewPassword = 
xPasswordDialog.getChild("newpassroEntry")
+                        xNewPassword.executeAction("TYPE", 
mkPropertyValues({"TEXT": "password"}))
+                        xConfirmPassword = 
xPasswordDialog.getChild("confirmropassEntry")
+                        xConfirmPassword.executeAction("TYPE", 
mkPropertyValues({"TEXT": "password"}))
+
+                # DOCX confirmation dialog is displayed
+                xWarnDialog = self.xUITest.getTopFocusWindow()
+                xSave = xWarnDialog.getChild("save")
+                self.ui_test.close_dialog_through_button(xSave)
+
+                self.ui_test.close_doc()
+
+                with self.ui_test.load_file(systemPathToFileUrl(xFilePath)):
+                    xWriterEdit = 
self.xUITest.getTopFocusWindow().getChild("writer_edit")
+                    document = self.ui_test.get_component()
+
+                    self.assertTrue(document.isReadonly())
+
+                    #Without the fix in place, this dialog wouldn't have been 
displayed
+                    with 
self.ui_test.execute_dialog_through_action(xWriterEdit, "TYPE", 
mkPropertyValues({"KEYCODE": "CTRL+SHIFT+M"})) as xDialog:
+                        xPassword = xDialog.getChild("newpassEntry")
+                        xPassword.executeAction("TYPE", 
mkPropertyValues({"TEXT": "password"}))
+
+                    self.assertFalse(document.isReadonly())
+
+# vim: set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/sw/source/filter/ww8/docxexport.cxx 
b/sw/source/filter/ww8/docxexport.cxx
index b90fc8c74a02..a1ead25a3b7e 100644
--- a/sw/source/filter/ww8/docxexport.cxx
+++ b/sw/source/filter/ww8/docxexport.cxx
@@ -987,6 +987,8 @@ void DocxExport::WriteSettings()
     if( !pViewShell && !m_aSettings.hasData() && 
!m_pAttrOutput->HasFootnotes() && !m_pAttrOutput->HasEndnotes())
         return;
 
+    SwDocShell* pDocShell = m_rDoc.GetDocShell();
+
     m_rFilter.addRelation( m_pDocumentFS->getOutputStream(),
             oox::getRelationship(Relationship::SETTINGS),
             u"settings.xml" );
@@ -998,6 +1000,61 @@ void DocxExport::WriteSettings()
     pFS->startElementNS( XML_w, XML_settings,
             FSNS( XML_xmlns, XML_w ), m_rFilter.getNamespaceURL(OOX_NS(doc)) );
 
+    // Write protection
+    const uno::Sequence<beans::PropertyValue> aInfo = 
pDocShell->GetModifyPasswordInfo();
+    if (aInfo.hasElements())
+    {
+        OUString sAlgorithm, sSalt, sHash;
+        sal_Int32 nCount = 0;
+        for (const auto& prop : aInfo)
+        {
+            if (prop.Name == "algorithm-name")
+                prop.Value >>= sAlgorithm;
+            else if (prop.Name == "salt")
+                prop.Value >>= sSalt;
+            else if (prop.Name == "iteration-count")
+                prop.Value >>= nCount;
+            else if (prop.Name == "hash")
+                prop.Value >>= sHash;
+        }
+        if (!sAlgorithm.isEmpty() && !sSalt.isEmpty() && !sHash.isEmpty())
+        {
+            sal_Int32 nAlgorithmSid = 0;
+            if (sAlgorithm == "MD2")
+                nAlgorithmSid = 1;
+            else if (sAlgorithm == "MD4")
+                nAlgorithmSid = 2;
+            else if (sAlgorithm == "MD5")
+                nAlgorithmSid = 3;
+            else if (sAlgorithm == "SHA-1")
+                nAlgorithmSid = 4;
+            else if (sAlgorithm == "MAC")
+                nAlgorithmSid = 5;
+            else if (sAlgorithm == "RIPEMD")
+                nAlgorithmSid = 6;
+            else if (sAlgorithm == "RIPEMD-160")
+                nAlgorithmSid = 7;
+            else if (sAlgorithm == "HMAC")
+                nAlgorithmSid = 9;
+            else if (sAlgorithm == "SHA-256")
+                nAlgorithmSid = 12;
+            else if (sAlgorithm == "SHA-384")
+                nAlgorithmSid = 13;
+            else if (sAlgorithm == "SHA-512")
+                nAlgorithmSid = 14;
+
+            if (nAlgorithmSid != 0)
+                pFS->singleElementNS(XML_w, XML_writeProtection,
+                    FSNS(XML_w, XML_cryptProviderType), "rsaAES",
+                    FSNS(XML_w, XML_cryptAlgorithmClass), "hash",
+                    FSNS(XML_w, XML_cryptAlgorithmType), "typeAny",
+                    FSNS(XML_w, XML_cryptAlgorithmSid), 
OString::number(nAlgorithmSid).getStr(),
+                    FSNS(XML_w, XML_cryptSpinCount), 
OString::number(nCount).getStr(),
+                    FSNS(XML_w, XML_hash), sHash.toUtf8().getStr(),
+                    FSNS(XML_w, XML_salt), sSalt.toUtf8().getStr());
+        }
+    }
+
     // View
     if (pViewShell && pViewShell->GetViewOptions()->getBrowseMode())
     {
@@ -1114,7 +1171,7 @@ void DocxExport::WriteSettings()
         DocxAttributeOutput::WriteFootnoteEndnotePr( pFS, XML_endnotePr, 
m_rDoc.GetEndNoteInfo(), XML_endnote );
 
     // Has themeFontLang information
-    uno::Reference< beans::XPropertySet > xPropSet( 
m_rDoc.GetDocShell()->GetBaseModel(), uno::UNO_QUERY_THROW );
+    uno::Reference< beans::XPropertySet > xPropSet( pDocShell->GetBaseModel(), 
uno::UNO_QUERY_THROW );
 
     bool bUseGrabBagProtection = false;
     bool bWriterWantsToProtect = false;
@@ -1274,7 +1331,7 @@ void DocxExport::WriteSettings()
                             else if ( nToken == XML_edit && sValue == 
"readOnly" )
                             {
                                 // Ignore the case where read-only was not 
enforced, but now is. That is handled by _MarkAsFinal
-                                bReadOnlyStatusUnchanged = 
m_rDoc.GetDocShell()->IsSecurityOptOpenReadOnly();
+                                bReadOnlyStatusUnchanged = 
pDocShell->IsSecurityOptOpenReadOnly();
                             }
                             else if ( nToken == XML_enforcement )
                                 bEnforced = sValue.toBoolean();

Reply via email to