tdf#157028 vcl: use object streams in an encrypted file too
Object streams and a cross-reference stream shrink a tagged PDF, but
I1e27dc3a8f43aa8b6c75ae930615c8d4b4645627 ("tdf#157028 vcl: write the
structure tree into object streams") left an encrypted export out of
both, so a password kept the whole structure tree uncompressed. The
spreadsheet reported in tdf#166233, attachment 171182, exported tagged
at 2368642 bytes, and at 13465120 with a password. The protected export
is 2389620 bytes now, and what it has over the plain tagged one is what
encryption itself costs.
This writer encrypts a string as it builds the object. ISO 32000-2 7.5.7
forbids encrypting a string in an object stream separately: the stream
that carries it is encrypted instead. So the strings of a stream-bound
object go in plain now, and the object stream is encrypted as a whole,
with the cipher set up for the stream's own object number rather than
for the objects inside. The stream's /Length counts what the cipher
adds. Only the structure elements and the ID tree put strings in a
stream, so only those two builders ask getStringEncryptor for one, and
it returns nothing while the file uses object streams.
Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com>
Change-Id: Ia6a063244baf456d7c7368a6bba7a8cbb96e31b0
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/13370
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit 59193295323917bd065ea3a235f7653ad7f9b04b)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211655
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
committed by
Xisco Fauli
parent
c80b6acef5
commit
eec52071ef
@@ -1094,7 +1094,14 @@ private:
|
||||
bool emitTrailer();
|
||||
/// whether this file may carry object streams and a cross-reference stream
|
||||
bool useObjectStreams() const;
|
||||
/// writes a whole object, into an object stream where the file may have one
|
||||
/// what encrypts a string as an object is built, and nothing for an object bound for a stream
|
||||
std::shared_ptr<pdf::IPDFEncryptor> getStringEncryptor(sal_Int32 nObject);
|
||||
#ifndef NDEBUG
|
||||
/// objects whose strings went in plain, checked once the last object stream is flushed
|
||||
std::unordered_set<sal_Int32> m_aPlainStringObjects;
|
||||
#endif
|
||||
/// writes a whole object, into an object stream where the file may have one; a body
|
||||
/// holding strings takes its encryptor from getStringEncryptor
|
||||
bool writeObject(sal_Int32 nObject, std::string_view aBody);
|
||||
/// writes an object's body into the pending object stream rather than into the file
|
||||
bool writeCompressedObject(sal_Int32 nObject, std::string_view aBody);
|
||||
|
||||
@@ -12,16 +12,21 @@
|
||||
|
||||
#include <test/unoapi_test.hxx>
|
||||
#include <o3tl/numeric.hxx>
|
||||
#include <o3tl/string_view.hxx>
|
||||
|
||||
#include <comphelper/crypto/Crypto.hxx>
|
||||
#include <comphelper/hash.hxx>
|
||||
#include <comphelper/propertysequence.hxx>
|
||||
#include <comphelper/propertyvalue.hxx>
|
||||
#include <comphelper/sequenceashashmap.hxx>
|
||||
|
||||
#include <vcl/filter/PDFiumLibrary.hxx>
|
||||
|
||||
#include <com/sun/star/beans/XPropertySet.hpp>
|
||||
#include <com/sun/star/frame/XStorable.hpp>
|
||||
#include <com/sun/star/lang/XMultiServiceFactory.hpp>
|
||||
#include <com/sun/star/text/XText.hpp>
|
||||
#include <com/sun/star/text/XTextContent.hpp>
|
||||
#include <com/sun/star/text/XTextCursor.hpp>
|
||||
#include <com/sun/star/text/XTextDocument.hpp>
|
||||
|
||||
@@ -88,6 +93,79 @@ CPPUNIT_TEST_FIXTURE(PDFEncryptionTest, testEncryptionRoundtrip_PDF_1_7)
|
||||
CPPUNIT_ASSERT_EQUAL(17, nFileVersion);
|
||||
}
|
||||
|
||||
// encryption used to cost a tagged file its object streams, and so most of its size
|
||||
CPPUNIT_TEST_FIXTURE(PDFEncryptionTest, testEncryptedTaggedUsesObjectStreams)
|
||||
{
|
||||
loadFromURL(u"private:factory/swriter"_ustr);
|
||||
auto xText = mxComponent.queryThrow<text::XTextDocument>()->getText();
|
||||
xText->setString(u"Some tagged text"_ustr);
|
||||
// a footnote, because its element is the one this writer gives an /ID to
|
||||
auto xFactory = mxComponent.queryThrow<lang::XMultiServiceFactory>();
|
||||
auto xFootnote = xFactory->createInstance(u"com.sun.star.text.Footnote"_ustr)
|
||||
.queryThrow<text::XTextContent>();
|
||||
xText->insertTextContent(xText->getEnd(), xFootnote, false);
|
||||
xFootnote.queryThrow<text::XText>()->setString(u"The note this refers to"_ustr);
|
||||
|
||||
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true),
|
||||
comphelper::makePropertyValue(u"EncryptFile"_ustr, true),
|
||||
comphelper::makePropertyValue(u"DocumentOpenPassword"_ustr,
|
||||
u"secret"_ustr) };
|
||||
mxComponent.queryThrow<frame::XStorable>()->storeToURL(
|
||||
maTempFile.GetURL(),
|
||||
{ comphelper::makePropertyValue(u"FilterName"_ustr, u"writer_pdf_Export"_ustr),
|
||||
comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) });
|
||||
|
||||
std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument
|
||||
= parsePDFExport(vcl::pdf::PDFiumLibrary::get(), "secret"_ostr);
|
||||
|
||||
// the structure tree went into object streams, which is what the encryption used to cost.
|
||||
// Only strings and stream data are encrypted, so the stream's own dictionary reads plainly
|
||||
const std::string_view aFile(static_cast<const char*>(maMemory.GetData()), maMemory.GetSize());
|
||||
// an unencrypted export has object streams anyway, so the rest tests nothing without this
|
||||
CPPUNIT_ASSERT(aFile.find("/Encrypt") != std::string_view::npos);
|
||||
const size_t nObjStm = aFile.find("/ObjStm");
|
||||
CPPUNIT_ASSERT(nObjStm != std::string_view::npos);
|
||||
|
||||
// /Length has to count what the cipher adds, or a reader that trusts it stops mid-block.
|
||||
// pdfium does not: it scans for endstream, so only the byte count catches this
|
||||
// the writer puts exactly one newline here, and the cipher's own first byte may be another
|
||||
const size_t nData = aFile.find(">>\nstream\n", nObjStm);
|
||||
CPPUNIT_ASSERT(nData != std::string_view::npos);
|
||||
const size_t nLength = aFile.find("/Length ", nObjStm);
|
||||
CPPUNIT_ASSERT(nLength != std::string_view::npos);
|
||||
// inside this dictionary, so it is this stream's length and not a later object's
|
||||
CPPUNIT_ASSERT_LESS(nData, nLength);
|
||||
const sal_Int32 nDeclared = o3tl::toInt32(aFile.substr(nLength + 8, 12));
|
||||
CPPUNIT_ASSERT(nData + 10 + nDeclared + 10 <= aFile.size());
|
||||
CPPUNIT_ASSERT_EQUAL("\nendstream"_ostr, OString(aFile.substr(nData + 10 + nDeclared, 10)));
|
||||
|
||||
// reading the footnote element's /ID back through the password distinguishes a plain
|
||||
// string from one encrypted a second time
|
||||
CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
|
||||
auto pPage = pPdfDocument->openPage(0);
|
||||
CPPUNIT_ASSERT(pPage);
|
||||
auto pTree = pPage->getStructureTree();
|
||||
CPPUNIT_ASSERT(pTree);
|
||||
CPPUNIT_ASSERT_GREATER(0, pTree->getNumberOfChildren());
|
||||
|
||||
OUString aID;
|
||||
const auto aFindID = [&aID](auto& rSelf, vcl::pdf::PDFiumStructureElement& rElement) -> void {
|
||||
if (aID.isEmpty())
|
||||
aID = rElement.getID();
|
||||
for (int i = 0; i < rElement.getNumberOfChildren() && aID.isEmpty(); ++i)
|
||||
{
|
||||
if (const auto pKid = rElement.getChild(i))
|
||||
rSelf(rSelf, *pKid);
|
||||
}
|
||||
};
|
||||
for (int i = 0; i < pTree->getNumberOfChildren() && aID.isEmpty(); ++i)
|
||||
{
|
||||
if (const auto pChild = pTree->getChild(i))
|
||||
aFindID(aFindID, *pChild);
|
||||
}
|
||||
CPPUNIT_ASSERT(aID.startsWith("id"));
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(PDFEncryptionTest, testEncryptionRoundtrip_PDF_2_0)
|
||||
{
|
||||
loadFromURL(u"private:factory/swriter"_ustr);
|
||||
|
||||
@@ -844,15 +844,26 @@ bool PDFWriterImpl::useObjectStreams() const
|
||||
{
|
||||
if (m_aContext.Version < PDFWriter::PDFVersion::PDF_1_5)
|
||||
return false;
|
||||
// a string in an object stream shall not be separately encrypted, while this writer
|
||||
// encrypts every string as it builds the object
|
||||
if (m_aContext.Encryption.canEncrypt())
|
||||
return false;
|
||||
// sdext reads the embedded original document out of a trailer, and a cross-reference
|
||||
// stream leaves none
|
||||
return m_aDocumentAttachedFiles.empty();
|
||||
}
|
||||
|
||||
std::shared_ptr<pdf::IPDFEncryptor> PDFWriterImpl::getStringEncryptor(sal_Int32 nObject)
|
||||
{
|
||||
if (!useObjectStreams())
|
||||
return m_pPDFEncryptor;
|
||||
|
||||
#ifndef NDEBUG
|
||||
m_aPlainStringObjects.insert(nObject);
|
||||
#else
|
||||
(void)nObject;
|
||||
#endif
|
||||
// ISO 32000-2 7.5.7: a string in an object stream is not encrypted on its own, the
|
||||
// stream carrying it is
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool PDFWriterImpl::writeObject(sal_Int32 nObject, std::string_view aBody)
|
||||
{
|
||||
if (useObjectStreams())
|
||||
@@ -895,14 +906,24 @@ bool PDFWriterImpl::emitObjectStream()
|
||||
aLine.append(OString::number(nStreamObject) + " 0 obj\n<</Type/ObjStm/N "
|
||||
+ OString::number(static_cast<sal_Int32>(m_aCompressedObjects.size())) + "/First "
|
||||
+ OString::number(nFirst) + "/Length "
|
||||
+ OString::number(static_cast<sal_Int32>(aStream.size())));
|
||||
+ OString::number(calculateStreamSize(aStream.size())));
|
||||
if (!g_bDebugDisableCompression)
|
||||
aLine.append("/Filter/FlateDecode");
|
||||
aLine.append(">>\nstream\n");
|
||||
|
||||
if (!updateObject(nStreamObject) || !writeBuffer(aLine)
|
||||
|| !writeBufferBytes(aStream.data(), aStream.size())
|
||||
|| !writeBuffer("\nendstream\nendobj\n\n"))
|
||||
if (!updateObject(nStreamObject) || !writeBuffer(aLine))
|
||||
return false;
|
||||
|
||||
// ISO 32000-2 7.5.7: a string in here is not encrypted on its own, the stream holding
|
||||
// it is, with the cipher set up for the stream's own object number
|
||||
checkAndEnableStreamEncryption(nStreamObject);
|
||||
// the strings inside are covered by nothing else, so stream encryption must really be on
|
||||
assert((!m_aContext.Encryption.canEncrypt()
|
||||
|| (m_pPDFEncryptor && m_pPDFEncryptor->isStreamEncryptionEnabled()))
|
||||
&& "an object stream is going out unencrypted");
|
||||
const bool bBodies = writeBufferBytes(aStream.data(), aStream.size());
|
||||
disableStreamEncryption();
|
||||
if (!bBodies || !writeBuffer("\nendstream\nendobj\n\n"))
|
||||
return false;
|
||||
|
||||
// only now can an entry name the stream, which is in the file
|
||||
@@ -958,7 +979,7 @@ sal_Int32 PDFWriterImpl::emitStructIDTree(sal_Int32 const nObject)
|
||||
ids.emplace(GenerateID(n), n);
|
||||
}
|
||||
OStringBuffer buf;
|
||||
COSWriter aWriter(buf, m_aContext.Encryption.getParams(), m_pPDFEncryptor);
|
||||
COSWriter aWriter(buf, m_aContext.Encryption.getParams(), getStringEncryptor(nObject));
|
||||
buf.append("<</Names [\n");
|
||||
for (auto const& it : ids)
|
||||
{
|
||||
@@ -1274,7 +1295,7 @@ sal_Int32 PDFWriterImpl::emitStructure( PDFStructureElement& rEle )
|
||||
}
|
||||
|
||||
OStringBuffer aLine( 512 );
|
||||
COSWriter aWriter(aLine, m_aContext.Encryption.getParams(), m_pPDFEncryptor);
|
||||
COSWriter aWriter(aLine, m_aContext.Encryption.getParams(), getStringEncryptor(rEle.m_nObject));
|
||||
aLine.append("<</Type");
|
||||
sal_Int32 nParentTree = -1;
|
||||
sal_Int32 nIDTree = -1;
|
||||
@@ -4646,6 +4667,11 @@ bool PDFWriterImpl::emitCatalog()
|
||||
emitStructure( m_aStructure[ 0 ] );
|
||||
if (!emitObjectStream())
|
||||
return false;
|
||||
// whatever put its strings in plain has to have landed in an object stream; that the
|
||||
// stream is encrypted is asserted where it is written
|
||||
assert(std::ranges::all_of(m_aPlainStringObjects, [this](sal_Int32 nPlain) {
|
||||
return m_aObjects[nPlain - 1].isCompressed();
|
||||
}));
|
||||
}
|
||||
|
||||
// adjust tree node file offset
|
||||
|
||||
Reference in New Issue
Block a user