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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- PDF authentication accepts named crypt filters with an explicit or default
`None` method, and applies the 127-byte password limit for AES-256 revision 6.

- Spreadsheet recalculation recognizes STDEV.S, STDEV.P, VAR.S, and VAR.P,
including their Excel and LibreOffice compatibility prefixes.

Expand Down
50 changes: 37 additions & 13 deletions src/odr/internal/pdf/pdf_encryption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ std::optional<EncryptionMethod> cfm_method(const std::string &cfm) {
if (cfm == "AESV3") {
return EncryptionMethod::aes_v3;
}
if (cfm == "Identity") {
if (cfm == "None" || cfm == "Identity") {
return EncryptionMethod::none;
}
return std::nullopt;
Expand All @@ -216,12 +216,27 @@ std::optional<EncryptionMethod> resolve_crypt_filter(const Dictionary &encrypt,
return std::nullopt;
}
const Dictionary &filter = cf[name].as_dictionary();
// ISO 32000-1 Table 25: an omitted CFM defaults to None.
if (filter.get("CFM").is_null()) {
return EncryptionMethod::none;
}
if (!filter.get("CFM").is_name()) {
return std::nullopt;
}
return cfm_method(filter["CFM"].as_name());
}

/// The method `/StmF` or `/StrF` selects, or @p fallback if the entry is
/// absent.
std::optional<EncryptionMethod> filter_method(const Dictionary &encrypt,
const std::string &key,
const EncryptionMethod fallback) {
if (!encrypt.get(key).is_name()) {
return fallback;
}
return resolve_crypt_filter(encrypt, encrypt[key].as_name());
}

} // namespace

std::optional<Authenticator>
Expand Down Expand Up @@ -259,11 +274,20 @@ Authenticator::create(const Dictionary &encrypt, const std::string &file_id0) {
d.m_oe = encrypt["OE"].as_string();
d.m_ue = encrypt["UE"].as_string();
d.m_key_length = 32;
d.m_stream_method = EncryptionMethod::aes_v3;
d.m_string_method = EncryptionMethod::aes_v3;
if (d.m_r != 6) {
return std::nullopt; // R 5 (deprecated interim revision) is out of scope
}
// The spec defaults to Identity, but AES-256 writers that omit the crypt
// filters still encrypt everything.
const auto stream_method =
filter_method(encrypt, "StmF", EncryptionMethod::aes_v3);
const auto string_method =
filter_method(encrypt, "StrF", EncryptionMethod::aes_v3);
if (!stream_method || !string_method) {
return std::nullopt; // unsupported crypt filter
}
d.m_stream_method = *stream_method;
d.m_string_method = *string_method;
return d;
}

Expand All @@ -279,12 +303,10 @@ Authenticator::create(const Dictionary &encrypt, const std::string &file_id0) {
if (d.m_v == 4) {
// Crypt filters select the method for streams and strings; the defaults
// are Identity (Table 20).
const std::string stmf =
encrypt.get("StmF").is_name() ? encrypt["StmF"].as_name() : "Identity";
const std::string strf =
encrypt.get("StrF").is_name() ? encrypt["StrF"].as_name() : "Identity";
const auto stream_method = resolve_crypt_filter(encrypt, stmf);
const auto string_method = resolve_crypt_filter(encrypt, strf);
const auto stream_method =
filter_method(encrypt, "StmF", EncryptionMethod::none);
const auto string_method =
filter_method(encrypt, "StrF", EncryptionMethod::none);
if (!stream_method || !string_method) {
return std::nullopt; // unsupported crypt filter
}
Expand All @@ -307,15 +329,17 @@ Authenticator::authenticate(const std::string &password) const {
// ISO 32000-2 Algorithms 11/12: validate against the salts in /U and /O,
// then unwrap the file key from /UE or /OE with AES-256-CBC (zero IV).
const std::string zero_iv(aes_block, '\0');
// Algorithm 2.A: only the first 127 bytes of the UTF-8 password count.
const std::string truncated = password.substr(0, 127);

if (hash_r6(password, m_u.substr(32, 8), {}) == m_u.substr(0, 32)) {
const std::string ik = hash_r6(password, m_u.substr(40, 8), {});
if (hash_r6(truncated, m_u.substr(32, 8), {}) == m_u.substr(0, 32)) {
const std::string ik = hash_r6(truncated, m_u.substr(40, 8), {});
std::string key = crypto::util::decrypt_aes_cbc(ik, zero_iv, m_ue);
return Decryptor(std::move(key), m_stream_method, m_string_method);
}
const std::string u48 = m_u.substr(0, 48);
if (hash_r6(password, m_o.substr(32, 8), u48) == m_o.substr(0, 32)) {
const std::string ik = hash_r6(password, m_o.substr(40, 8), u48);
if (hash_r6(truncated, m_o.substr(32, 8), u48) == m_o.substr(0, 32)) {
const std::string ik = hash_r6(truncated, m_o.substr(40, 8), u48);
std::string key = crypto::util::decrypt_aes_cbc(ik, zero_iv, m_oe);
return Decryptor(std::move(key), m_stream_method, m_string_method);
}
Expand Down
86 changes: 86 additions & 0 deletions test/src/internal/pdf/pdf_encryption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,92 @@ TEST(PdfEncryption, qpdf_aes_128_r4) {
EXPECT_NE(content.find(kMarker), std::string::npos);
}

TEST(PdfEncryption, named_unencrypted_crypt_filter) {
const std::string owner(32, 'o');
const std::string key = standard_security::compute_key_r2_r4(
"user", owner, -4, "id", 4, 16, true);
const std::string user = standard_security::compute_u_r2_r4(key, "id", 4);
for (const bool explicit_method : {false, true}) {
SCOPED_TRACE(explicit_method);
Dictionary encrypt = standard_encrypt(4, 4, 128, owner, user);
Dictionary filter;
if (explicit_method) {
filter["CFM"] = Object(Name("None"));
}
Dictionary filters;
filters["Clear"] = Object(std::move(filter));
encrypt["CF"] = Object(std::move(filters));
encrypt["StmF"] = Object(Name("Clear"));
encrypt["StrF"] = Object(Name("Clear"));
const auto authenticator = Authenticator::create(encrypt, "id");
ASSERT_TRUE(authenticator.has_value());
const auto decryptor = authenticator->authenticate("user");
ASSERT_TRUE(decryptor.has_value());
EXPECT_EQ(decryptor->decrypt_stream(kContentRef, kMarker), kMarker);
EXPECT_EQ(decryptor->decrypt_string(kContentRef, kMarker), kMarker);
}
}

namespace {

// Frozen qpdf 12.3.2 output, user/owner passwords: 127 'u'/'o' bytes.
Dictionary qpdf_r6_encrypt() {
Dictionary encrypt = standard_encrypt(
5, 6, 256,
hex_decode(
"b6f62a02b186fa1bd0bf815c7b9374df14dd1e711287a4f5be1f623cdb2dae07"
"05fbe76667796505e0d1024c0b327eb9"),
hex_decode(
"7b27e3f32d74562fe3236910b01f794430c25b6227ccd22319eac7a773682fc7"
"e535e5435177c87a4ad8aa91ab2316a5"));
encrypt["OE"] = Object(StandardString(hex_decode(
"db29dc16413c275e33d0f297fde819f57173d601280cfefbe90f9c1899647f03")));
encrypt["UE"] = Object(StandardString(hex_decode(
"3d2aa5d75ee4f90aee3fdf6a28af4b504912d9fe8678063910b673600e0c3c23")));
return encrypt;
}

const std::string kQpdfR6Stream = hex_decode(
"477bf3f6b8aa0697656e2844d037d183218f77ad3f2a926118a08b694f07ea2e"
"1fec24dbe016dac6bbffeba92757f4ccf227c9d378efafdb8e703fe38b501a22");
constexpr const char *kQpdfR6Content = "BT /F1 12 Tf (KAT-MARKER-12345) Tj ET";

} // namespace

TEST(PdfEncryption, qpdf_r6_truncates_user_and_owner_passwords) {
const auto authenticator = Authenticator::create(qpdf_r6_encrypt(), "");
ASSERT_TRUE(authenticator.has_value());
for (const char character : {'u', 'o'}) {
SCOPED_TRACE(character);
const std::string password(127, character);
EXPECT_FALSE(authenticator->authenticate(password.substr(1)).has_value());
for (const std::string &candidate : {password, password + "ignored"}) {
const auto decryptor = authenticator->authenticate(candidate);
ASSERT_TRUE(decryptor.has_value());
EXPECT_EQ(decryptor->decrypt_stream(kContentRef, kQpdfR6Stream),
kQpdfR6Content);
}
}
}

TEST(PdfEncryption, r6_honors_named_crypt_filters) {
Dictionary encrypt = qpdf_r6_encrypt();
Dictionary filter;
filter["CFM"] = Object(Name("AESV3"));
Dictionary filters;
filters["StdCF"] = Object(std::move(filter));
encrypt["CF"] = Object(std::move(filters));
encrypt["StmF"] = Object(Name("StdCF"));
encrypt["StrF"] = Object(Name("Identity"));
const auto authenticator = Authenticator::create(encrypt, "");
ASSERT_TRUE(authenticator.has_value());
const auto decryptor = authenticator->authenticate(std::string(127, 'u'));
ASSERT_TRUE(decryptor.has_value());
EXPECT_EQ(decryptor->decrypt_stream(kContentRef, kQpdfR6Stream),
kQpdfR6Content);
EXPECT_EQ(decryptor->decrypt_string(kContentRef, kMarker), kMarker);
}

// A damaged file can end an AES string inside a block; the whole blocks before
// it still decrypt.
TEST(PdfEncryption, aes_drops_a_trailing_partial_block) {
Expand Down
Loading