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; } }
