From e25910b59ab84c7aada0b651e7dbb47f6830ad3d Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sat, 17 Oct 2009 23:37:55 +0000 Subject: reading crypt filters is largely implemented but not fully tested git-svn-id: svn+q:///qpdf/trunk@812 71b93d88-0707-0410-a8cf-f5a4172ac649 --- include/qpdf/QPDF.hh | 7 +- libqpdf/Pl_AES_PDF.cc | 2 +- libqpdf/QPDF.cc | 3 + libqpdf/QPDF_Stream.cc | 14 +++- libqpdf/QPDF_encryption.cc | 189 ++++++++++++++++++++++++++++++++++++++++----- libqpdf/qpdf/Pl_AES_PDF.hh | 2 +- qpdf/qpdf.testcov | 4 +- 7 files changed, 195 insertions(+), 26 deletions(-) diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index 7df995b1..44d069f4 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -87,6 +87,7 @@ class DLL_EXPORT QPDF // Encryption support + enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes }; struct EncryptionData { // This class holds data read from the encryption dictionary. @@ -397,6 +398,7 @@ class DLL_EXPORT QPDF std::vector& result); // methods to support encryption -- implemented in QPDF_encryption.cc + encryption_method_e interpretCF(QPDFObjectHandle); void initializeEncryption(); std::string getKeyForObject(int objid, int generation, bool use_aes); void decryptString(std::string&, int objid, int generation); @@ -739,7 +741,10 @@ class DLL_EXPORT QPDF bool attempt_recovery; int encryption_V; bool encrypt_metadata; - QPDFObjectHandle encryption_dictionary; + std::map crypt_filters; + encryption_method_e cf_stream; + encryption_method_e cf_string; + encryption_method_e cf_file; std::string provided_password; std::string user_password; std::string encryption_key; diff --git a/libqpdf/Pl_AES_PDF.cc b/libqpdf/Pl_AES_PDF.cc index fa8bc3cf..e3294ec5 100644 --- a/libqpdf/Pl_AES_PDF.cc +++ b/libqpdf/Pl_AES_PDF.cc @@ -9,7 +9,7 @@ #include Pl_AES_PDF::Pl_AES_PDF(char const* identifier, Pipeline* next, - bool encrypt, unsigned char key[key_size]) : + bool encrypt, unsigned char const key[key_size]) : Pipeline(identifier, next), encrypt(encrypt), cbc_mode(true), diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index 10777aa4..c7ec873f 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -255,6 +255,9 @@ QPDF::QPDF() : attempt_recovery(true), encryption_V(0), encrypt_metadata(true), + cf_stream(e_none), + cf_string(e_none), + cf_file(e_none), cached_key_objid(0), cached_key_generation(0), first_xref_item_offset(0), diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index a9e2ec51..dd934f3d 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -136,6 +136,11 @@ QPDF_Stream::filterable(std::vector& filters, filterable = false; } } + else if (key == "/Crypt") + { + // XXX untested + // we handle this in decryptStream + } else { filterable = false; @@ -212,7 +217,8 @@ QPDF_Stream::filterable(std::vector& filters, iter != filters.end(); ++iter) { std::string const& filter = *iter; - if (! ((filter == "/FlateDecode") || + if (! ((filter == "/Crypt") || + (filter == "/FlateDecode") || (filter == "/LZWDecode") || (filter == "/ASCII85Decode") || (filter == "/ASCIIHexDecode"))) @@ -266,7 +272,11 @@ QPDF_Stream::pipeStreamData(Pipeline* pipeline, bool filter, iter != filters.rend(); ++iter) { std::string const& filter = *iter; - if (filter == "/FlateDecode") + if (filter == "/Crypt") + { + // Ignore -- handled by pipeStreamData + } + else if (filter == "/FlateDecode") { if (predictor == 12) { diff --git a/libqpdf/QPDF_encryption.cc b/libqpdf/QPDF_encryption.cc index 8e403631..d405759e 100644 --- a/libqpdf/QPDF_encryption.cc +++ b/libqpdf/QPDF_encryption.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -281,6 +282,27 @@ check_owner_password(std::string& user_password, return result; } +QPDF::encryption_method_e +QPDF::interpretCF(QPDFObjectHandle cf) +{ + if (cf.isName()) + { + std::string filter = cf.getName(); + if (this->crypt_filters.count(filter) != 0) + { + return this->crypt_filters[filter]; + } + else + { + return e_unknown; + } + } + else + { + return e_none; + } +} + void QPDF::initializeEncryption() { @@ -322,8 +344,7 @@ QPDF::initializeEncryption() "incorrect length"); } - this->encryption_dictionary = this->trailer.getKey("/Encrypt"); - QPDFObjectHandle& encryption_dict = this->encryption_dictionary; + QPDFObjectHandle encryption_dict = this->trailer.getKey("/Encrypt"); if (! encryption_dict.isDictionary()) { throw QPDFExc(this->file.getName(), this->file.getLastOffset(), @@ -336,6 +357,12 @@ QPDF::initializeEncryption() throw QPDFExc(this->file.getName(), this->file.getLastOffset(), "unsupported encryption filter"); } + if (! encryption_dict.getKey("/SubFilter").isNull()) + { + warn(QPDFExc(this->file.getName(), this->file.getLastOffset(), + "file uses encryption SubFilters," + " which qpdf does not support")); + } if (! (encryption_dict.getKey("/V").isInteger() && encryption_dict.getKey("/R").isInteger() && @@ -388,10 +415,55 @@ QPDF::initializeEncryption() encryption_dict.getKey("/EncryptMetadata").getBoolValue(); } - // XXX warn if /SubFilter is present if (V == 4) { - // XXX get CF + QPDFObjectHandle CF = encryption_dict.getKey("/CF"); + std::set keys = CF.getKeys(); + for (std::set::iterator iter = keys.begin(); + iter != keys.end(); ++iter) + { + std::string const& filter = *iter; + QPDFObjectHandle cdict = CF.getKey(filter); + if (cdict.isDictionary()) + { + encryption_method_e method = e_none; + if (cdict.getKey("/CFM").isName()) + { + std::string method_name = cdict.getKey("/CFM").getName(); + if (method_name == "/V2") + { + // XXX coverage + method = e_rc4; + } + else if (method_name == "/AESV2") + { + // XXX coverage + method = e_aes; + } + else + { + // Don't complain now -- maybe we won't need + // to reference this type. + method = e_unknown; + } + } + this->crypt_filters[filter] = method; + } + } + + QPDFObjectHandle StmF = encryption_dict.getKey("/StmF"); + QPDFObjectHandle StrF = encryption_dict.getKey("/StrF"); + QPDFObjectHandle EFF = encryption_dict.getKey("/EFF"); + this->cf_stream = interpretCF(StmF); + this->cf_string = interpretCF(StrF); + if (EFF.isName()) + { + this->cf_file = interpretCF(EFF); + } + else + { + this->cf_file = this->cf_stream; + } } EncryptionData data(V, R, Length / 8, P, O, U, id1, this->encrypt_metadata); if (check_owner_password( @@ -440,15 +512,50 @@ QPDF::decryptString(std::string& str, int objid, int generation) { return; } - bool use_aes = false; // XXX + bool use_aes = false; + if (this->encryption_V == 4) + { + switch (this->cf_string) + { + case e_none: + return; + + case e_aes: + use_aes = true; + break; + + case e_rc4: + break; + + default: + warn(QPDFExc(this->file.getName(), this->file.getLastOffset(), + "unknown encryption filter for strings" + " (check /StrF in /Encrypt dictionary);" + " strings may be decrypted improperly")); + // To avoid repeated warnings, reset cf_string. Assume + // we'd want to use AES if V == 4. + this->cf_string = e_aes; + break; + } + } + std::string key = getKeyForObject(objid, generation, use_aes); if (use_aes) { - // XXX - throw std::logic_error("XXX"); + // XXX coverage + assert(key.length() == Pl_AES_PDF::key_size); + Pl_Buffer bufpl("decrypted string"); + Pl_AES_PDF pl("aes decrypt string", &bufpl, false, + (unsigned char const*)key.c_str()); + pl.write((unsigned char*)str.c_str(), str.length()); + pl.finish(); + Buffer* buf = bufpl.getBuffer(); + str = std::string((char*)buf->getBuffer(), (size_t)buf->getSize()); + delete buf; } else { + QTC::TC("qpdf", "QPDF_encryption rc4 decode string"); unsigned int vlen = str.length(); char* tmp = QUtil::copy_string(str); RC4 rc4((unsigned char const*)key.c_str(), key.length()); @@ -463,7 +570,6 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, QPDFObjectHandle& stream_dict, std::vector >& heap) { - bool decrypt = true; std::string type; if (stream_dict.getKey("/Type").isName()) { @@ -471,34 +577,77 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, } if (type == "/XRef") { - QTC::TC("qpdf", "QPDF piping xref stream from encrypted file"); - decrypt = false; + QTC::TC("qpdf", "QPDF_encryption xref stream from encrypted file"); + return; } bool use_aes = false; if (this->encryption_V == 4) { - if ((! this->encrypt_metadata) && (type == "/Metadata")) + encryption_method_e method = e_unknown; + std::string method_source = "/StmF from /Encrypt dictionary"; + + if (stream_dict.getKey("/DecodeParms").isDictionary()) { - // XXX no test case for this - decrypt = false; + QPDFObjectHandle decode_parms = stream_dict.getKey("/DecodeParms"); + if (decode_parms.getKey("/Crypt").isDictionary()) + { + // XXX coverage + QPDFObjectHandle crypt = decode_parms.getKey("/Crypt"); + method = interpretCF(crypt.getKey("/Name")); + method_source = "stream's Crypt decode parameters"; + } } - // XXX check crypt filter; if not found, use StmF; see TODO - use_aes = true; // XXX - } - if (! decrypt) - { - return; - } + if (method == e_unknown) + { + if ((! this->encrypt_metadata) && (type == "/Metadata")) + { + // XXX coverage + method = e_none; + } + else + { + method = this->cf_stream; + } + // XXX What about embedded file streams? + } + use_aes = false; + switch (this->cf_stream) + { + case e_none: + return; + break; + + case e_aes: + use_aes = true; + break; + + case e_rc4: + break; + + default: + // filter local to this stream. + warn(QPDFExc(this->file.getName(), this->file.getLastOffset(), + "unknown encryption filter for streams" + " (check " + method_source + ");" + " streams may be decrypted improperly")); + // To avoid repeated warnings, reset cf_stream. Assume + // we'd want to use AES if V == 4. + this->cf_stream = e_aes; + break; + } + } std::string key = getKeyForObject(objid, generation, use_aes); if (use_aes) { + // XXX coverage assert(key.length() == Pl_AES_PDF::key_size); pipeline = new Pl_AES_PDF("AES stream decryption", pipeline, false, (unsigned char*) key.c_str()); } else { + QTC::TC("qpdf", "QPDF_encryption rc4 decode stream"); pipeline = new Pl_RC4("RC4 stream decryption", pipeline, (unsigned char*) key.c_str(), key.length()); } diff --git a/libqpdf/qpdf/Pl_AES_PDF.hh b/libqpdf/qpdf/Pl_AES_PDF.hh index adacc6e5..888ea752 100644 --- a/libqpdf/qpdf/Pl_AES_PDF.hh +++ b/libqpdf/qpdf/Pl_AES_PDF.hh @@ -12,7 +12,7 @@ class DLL_EXPORT Pl_AES_PDF: public Pipeline // key_data should be a pointer to key_size bytes of data static unsigned int const key_size = 16; Pl_AES_PDF(char const* identifier, Pipeline* next, - bool encrypt, unsigned char key[key_size]); + bool encrypt, unsigned char const key[key_size]); virtual ~Pl_AES_PDF(); virtual void write(unsigned char* data, int len); diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index a2905e2a..a5d5a386 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -112,7 +112,7 @@ QPDF found wrong endstream in recovery 0 QPDFObjectHandle indirect to unknown 0 QPDF_Stream pipeStreamData with null pipeline 0 QPDFWriter not recompressing /FlateDecode 0 -QPDF piping xref stream from encrypted file 0 +QPDF_encryption xref stream from encrypted file 0 unable to filter 0 QPDF_String non-trivial UTF-16 0 QPDF xref overwrite object 0 @@ -158,3 +158,5 @@ QPDFWriter using forced PDF version 0 qpdf-c called qpdf_set_minimum_pdf_version 0 qpdf-c called qpdf_force_pdf_version 0 qpdf-c called qpdf_init_write multiple times 0 +QPDF_encryption rc4 decode string 0 +QPDF_encryption rc4 decode stream 0 -- cgit v1.2.3-54-g00ecf