Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/odr/exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,8 @@ UnsupportedFileEncoding::UnsupportedFileEncoding(const std::string &message)
FileEncryptedError::FileEncryptedError()
: std::runtime_error("file encrypted error") {}

UnauthenticatedReadError::UnauthenticatedReadError()
: std::runtime_error(
"cannot read encrypted object without authentication") {}

} // namespace odr
5 changes: 5 additions & 0 deletions src/odr/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,9 @@ struct FileEncryptedError final : std::runtime_error {
explicit FileEncryptedError();
};

/// @brief Read attempted on an encrypted file that has not been authenticated
struct UnauthenticatedReadError final : std::runtime_error {
explicit UnauthenticatedReadError();
};

} // namespace odr
22 changes: 18 additions & 4 deletions src/odr/internal/pdf/pdf_document_parser.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <odr/internal/pdf/pdf_document_parser.hpp>

#include <odr/exceptions.hpp>
#include <odr/logger.hpp>

#include <odr/internal/pdf/pdf_cmap_parser.hpp>
Expand Down Expand Up @@ -263,10 +264,7 @@ DocumentParser::DocumentParser(std::unique_ptr<std::istream> in,

if (m_trailer.has_key("Encrypt")) {
// Build an `Authenticator` from the trailer `/Encrypt` and `/ID`
// (ISO 32000-1 7.6). `m_is_encrypted` records that the file declares
// encryption regardless of whether we can authenticate it, so an
// unsupported handler still reports as encrypted.
m_is_encrypted = true;
// (ISO 32000-1 7.6).

// The `/Encrypt` dictionary's own `/O`,`/U`,… strings are never encrypted
// (7.6.2), and need no explicit self-skip guard: it is resolved here while
Expand All @@ -290,6 +288,12 @@ DocumentParser::DocumentParser(std::unique_ptr<std::istream> in,
}
}

// Set only after resolving `/Encrypt` above: `read_object` throws on an
// encrypted-but-unauthenticated read, and that resolution runs before any
// decryptor exists. `m_is_encrypted` records that the file declares
// encryption regardless of whether we can authenticate it, so an
// unsupported handler still reports as encrypted.
m_is_encrypted = true;
m_authenticator = Authenticator::create(encrypt.as_dictionary(), id0);
}

Expand All @@ -310,6 +314,10 @@ const Dictionary &DocumentParser::trailer() const { return m_trailer; }

bool DocumentParser::is_encrypted() const { return m_is_encrypted; }

bool DocumentParser::is_authenticated() const {
return m_is_encrypted && m_decryptor.has_value();
}

const std::optional<Authenticator> &DocumentParser::authenticator() const {
return m_authenticator;
}
Expand Down Expand Up @@ -350,6 +358,9 @@ DocumentParser::read_object(const ObjectReference &reference) {
// case here: it is read and cached during construction before the
// decryptor is installed, so its un-decrypted /O,/U,… strings are served
// from the cache and never reach this path.
if (is_encrypted() && !is_authenticated()) {
throw UnauthenticatedReadError();
}
if (m_decryptor.has_value()) {
decrypt_strings(object.object, object.reference);
}
Expand Down Expand Up @@ -422,6 +433,9 @@ std::string DocumentParser::read_object_stream(const IndirectObject &object) {
// during the trailer-chain walk, before the decryptor exists, so they are
// never decrypted (7.5.8.2); object streams are decrypted here as a whole,
// leaving their members' plaintext.
if (is_encrypted() && !is_authenticated()) {
throw UnauthenticatedReadError();
}
if (m_decryptor.has_value()) {
raw = m_decryptor->decrypt_stream(object.reference, std::move(raw));
}
Expand Down
3 changes: 3 additions & 0 deletions src/odr/internal/pdf/pdf_document_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class DocumentParser {

/// Whether the file declares an `/Encrypt` dictionary.
[[nodiscard]] bool is_encrypted() const;
/// Whether the file is encrypted and a decryptor is installed, so reads can
/// decrypt. False for an encrypted file that has not been unlocked.
[[nodiscard]] bool is_authenticated() const;
/// The authenticator for an encrypted file (validates passwords and produces
/// a `Decryptor`), or `nullopt` if the file is not encrypted.
[[nodiscard]] const std::optional<Authenticator> &authenticator() const;
Expand Down
15 changes: 15 additions & 0 deletions test/src/internal/pdf/pdf_document_parser.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <odr/exceptions.hpp>

#include <odr/internal/common/file.hpp>
#include <odr/internal/pdf/pdf_document.hpp>
#include <odr/internal/pdf/pdf_document_element.hpp>
Expand Down Expand Up @@ -147,6 +149,19 @@ TEST(DocumentParser, reopen_with_decryptor) {
}
}

// Reading an encrypted file without authenticating must throw rather than serve
// undecrypted bytes. The file reports as encrypted but not authenticated.
TEST(DocumentParser, read_without_authentication_throws) {
const auto file = std::make_shared<DiskFile>(
TestData::test_file_path("odr-public/pdf/Casio_WVA-M650-7AJF.pdf"));

DocumentParser parser(file->stream());
EXPECT_TRUE(parser.is_encrypted());
EXPECT_FALSE(parser.is_authenticated());

EXPECT_THROW((void)parser.parse_document(), odr::UnauthenticatedReadError);
}

TEST(DocumentParser, inherited_page_attributes) {
PdfFileBuilder builder;
builder.object("<< /Type /Catalog /Pages 2 0 R >>")
Expand Down
Loading