diff options
-rw-r--r-- | comphelper/source/misc/docpasswordhelper.cxx | 33 | ||||
-rw-r--r-- | include/comphelper/docpasswordhelper.hxx | 2 | ||||
-rw-r--r-- | sfx2/source/dialog/filedlghelper.cxx | 23 | ||||
-rw-r--r-- | sw/qa/uitest/writer_tests6/tdf144374.py | 63 | ||||
-rw-r--r-- | sw/source/filter/ww8/docxexport.cxx | 61 |
5 files changed, 175 insertions, 7 deletions
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(); |