diff --git a/TODO b/TODO index 488f588c..b2da8d35 100644 --- a/TODO +++ b/TODO @@ -43,48 +43,53 @@ (http://delphi.about.com). .. use at your own risk and for whatever the purpose you want .. no support provided. Sample code provided." - * Implement as much of R = 4 encryption as possible. Already able to - decode AES-128-CBC and check passwords. + * R = 4, V = 4 encryption. - aes test suite: use fips-197 test vector with cbc disabled; encrypt - and decrypt some other files including multiples of 16 and not to - test cbc mode. + - Update C API for R4 encryption - /Encrypt keys (if V == 4) + - When we write encrypted files, we must remember to omit any + encryption filter settings from original streams. - /StmF - name of crypt filter for streams; default /Identity - /StrF - name of crypt filter for strings; default /Identity - /EFF - crypt filter for embedded files without their own crypt - filters; default is to use /StmF + - test various combinations with and without cleartext-metadata + and aes in compression tests - /CF - keys are crypt filter names, values are are crypt - dictionaries + - figure out a way to test crypt filters defined on a stream - Individual streams may also have crypt filters. Filter type - /Crypt; /DecodeParms must contain a Crypt filter decode - parameters dictionary whose /Name entry specifies the particular - filter to be used. If /Name is missing, use /Identity. - /DecodeParms << /Crypt << /Name /XYZ >> >> where /XYZ is - /Identity or a key in /CF. + - would be nice to test strings and streams with different + encryption types, but without sample data, we'd have to write + them ourselves which is not that useful - /Identity means not to encrypt. + - figure out why xpdf can open my files but not acroread - Crypt Dictionaries + - figure out how to look at the metadata so I can tell whether + /EncryptMetadata is working the way it's supposed to - /Type (optional) /CryptFilter - /CFM: - /V2 - use rc4 - /AESV2 - use aes - /Length - supposed to be key length, but the one file I have - has a bogus value for it, so I'm ignoring it. + - Do something with embedded files, but what and how? - We will ignore remaining fields and values. + - General notes: - Remember to honor /EncryptMetadata; applies to streams of /Type - /Metadata + /CF - keys are crypt filter names, values are are crypt + dictionaries - When we write encrypted files, we must remember to omit any - encryption filter settings from original streams. + Individual streams may also have crypt filters. Filter type + /Crypt; /DecodeParms must contain a Crypt filter decode + parameters dictionary whose /Name entry specifies the particular + filter to be used. If /Name is missing, use /Identity. + /DecodeParms << /Crypt << /Name /XYZ >> >> where /XYZ is + /Identity or a key in /CF. + + /Identity means not to encrypt. + + Crypt Dictionaries + + /Type (optional) /CryptFilter + /CFM: + /V2 - use rc4 + /AESV2 - use aes + /Length - supposed to be key length, but the one file I have + has a bogus value for it, so I'm ignoring it. + + We will ignore remaining fields and values. 2.2 === diff --git a/include/qpdf/QPDFWriter.hh b/include/qpdf/QPDFWriter.hh index a9392839..8a4e9ebe 100644 --- a/include/qpdf/QPDFWriter.hh +++ b/include/qpdf/QPDFWriter.hh @@ -118,9 +118,10 @@ class DLL_EXPORT QPDFWriter // Set up for encrypted output. Disables stream prefiltering and // content normalization. Note that setting R2 encryption - // parameters sets the PDF version to at least 1.3, and setting R3 + // parameters sets the PDF version to at least 1.3, setting R3 // encryption parameters pushes the PDF version number to at least - // 1.4. + // 1.4, and setting R4 parameters pushes the version to at least + // 1.5, or if AES is used, 1.6. void setR2EncryptionParameters( char const* user_password, char const* owner_password, bool allow_print, bool allow_modify, @@ -143,6 +144,11 @@ class DLL_EXPORT QPDFWriter char const* user_password, char const* owner_password, bool allow_accessibility, bool allow_extract, r3_print_e print, r3_modify_e modify); + void setR4EncryptionParameters( + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + r3_print_e print, r3_modify_e modify, + bool encrypt_metadata, bool use_aes); // Create linearized output. Disables qdf mode, content // normalization, and stream prefiltering. @@ -182,6 +188,11 @@ class DLL_EXPORT QPDFWriter void preserveObjectStreams(); void generateObjectStreams(); void generateID(); + void interpretR3EncryptionParameters( + std::set& bits_to_clear, + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + r3_print_e print, r3_modify_e modify); void setEncryptionParameters( char const* user_password, char const* owner_password, int V, int R, int key_len, std::set& bits_to_clear); @@ -231,6 +242,7 @@ class DLL_EXPORT QPDFWriter // stack items are of type Pl_Buffer, the buffer is retrieved. void popPipelineStack(PointerHolder* bp = 0); + void adjustAESStreamLength(unsigned long& length); void pushEncryptionFilter(); void pushDiscardFilter(); @@ -251,6 +263,8 @@ class DLL_EXPORT QPDFWriter bool linearized; object_stream_e object_stream_mode; std::string encryption_key; + bool encrypt_metadata; + bool encrypt_use_aes; std::map encryption_dictionary; std::string id1; // for /ID key of @@ -267,7 +281,7 @@ class DLL_EXPORT QPDFWriter std::map lengths; int next_objid; int cur_stream_length_id; - int cur_stream_length; + unsigned long cur_stream_length; bool added_newline; int max_ostream_index; std::set normalized_streams; diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index 71d1fa5c..8cdd2759 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ QPDFWriter::QPDFWriter(QPDF& pdf, char const* filename) : preserve_encryption(true), linearized(false), object_stream_mode(o_preserve), + encrypt_metadata(true), + encrypt_use_aes(false), encryption_dict_objid(0), next_objid(1), cur_stream_length_id(0), @@ -187,6 +190,38 @@ QPDFWriter::setR3EncryptionParameters( char const* user_password, char const* owner_password, bool allow_accessibility, bool allow_extract, r3_print_e print, r3_modify_e modify) +{ + std::set clear; + interpretR3EncryptionParameters( + clear, user_password, owner_password, + allow_accessibility, allow_extract, print, modify); + setMinimumPDFVersion("1.4"); + setEncryptionParameters(user_password, owner_password, 2, 3, 16, clear); +} + +void +QPDFWriter::setR4EncryptionParameters( + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + r3_print_e print, r3_modify_e modify, + bool encrypt_metadata, bool use_aes) +{ + std::set clear; + interpretR3EncryptionParameters( + clear, user_password, owner_password, + allow_accessibility, allow_extract, print, modify); + this->encrypt_use_aes = use_aes; + this->encrypt_metadata = encrypt_metadata; + setMinimumPDFVersion(use_aes ? "1.6" : "1.5"); + setEncryptionParameters(user_password, owner_password, 4, 4, 16, clear); +} + +void +QPDFWriter::interpretR3EncryptionParameters( + std::set& clear, + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + r3_print_e print, r3_modify_e modify) { // Acrobat 5 security options: @@ -206,7 +241,6 @@ QPDFWriter::setR3EncryptionParameters( // Low Resolution // Full printing - std::set clear; if (! allow_accessibility) { clear.insert(10); @@ -251,9 +285,6 @@ QPDFWriter::setR3EncryptionParameters( // no default so gcc warns for missing cases } - - setMinimumPDFVersion("1.4"); - setEncryptionParameters(user_password, owner_password, 2, 3, 16, clear); } void @@ -282,7 +313,7 @@ QPDFWriter::setEncryptionParameters( std::string U; QPDF::compute_encryption_O_U( user_password, owner_password, V, R, key_len, P, - /*XXX encrypt_metadata*/true, this->id1, O, U); + this->encrypt_metadata, this->id1, O, U); setEncryptionParametersInternal( V, R, key_len, P, O, U, this->id1, user_password); } @@ -326,9 +357,22 @@ QPDFWriter::setEncryptionParametersInternal( encryption_dictionary["/P"] = QUtil::int_to_string(P); encryption_dictionary["/O"] = QPDF_String(O).unparse(true); encryption_dictionary["/U"] = QPDF_String(U).unparse(true); + if ((R >= 4) && (! encrypt_metadata)) + { + encryption_dictionary["/EncryptMetadata"] = "false"; + } + if (V == 4) + { + encryption_dictionary["/StmF"] = "/CF1"; + encryption_dictionary["/StrF"] = "/CF1"; + std::string method = (this->encrypt_use_aes ? "/AESV2" : "/V2"); + encryption_dictionary["/CF"] = + "<< /CF1 << /AuthEvent /DocOpen /CFM " + method + " >> >>"; + } + this->encrypted = true; - QPDF::EncryptionData encryption_data(V, R, key_len, P, O, U, this->id1, - /*XXX encrypt_metadata*/true); + QPDF::EncryptionData encryption_data( + V, R, key_len, P, O, U, this->id1, this->encrypt_metadata); this->encryption_key = QPDF::compute_encryption_key( user_password, encryption_data); } @@ -337,7 +381,7 @@ void QPDFWriter::setDataKey(int objid) { this->cur_data_key = QPDF::compute_data_key( - this->encryption_key, objid, 0, /*XXX use_aes */false); + this->encryption_key, objid, 0, this->encrypt_use_aes); } int @@ -435,15 +479,37 @@ QPDFWriter::popPipelineStack(PointerHolder* bp) this->pipeline = dynamic_cast(this->pipeline_stack.back()); } +void +QPDFWriter::adjustAESStreamLength(unsigned long& length) +{ + if (this->encrypted && (! this->cur_data_key.empty()) && + this->encrypt_use_aes) + { + // Stream length will be padded with 1 to 16 bytes to end up + // as a multiple of 16. It will also be prepended by 16 bits + // of random data. + length += 32 - (length & 0xf); + } +} + void QPDFWriter::pushEncryptionFilter() { if (this->encrypted && (! this->cur_data_key.empty())) { - Pipeline* p = - new Pl_RC4("stream encryption", this->pipeline, - (unsigned char*) this->cur_data_key.c_str(), - this->cur_data_key.length()); + Pipeline* p = 0; + if (this->encrypt_use_aes) + { + p = new Pl_AES_PDF( + "aes stream encryption", this->pipeline, true, + (unsigned char*) this->cur_data_key.c_str()); + } + else + { + p = new Pl_RC4("rc4 stream encryption", this->pipeline, + (unsigned char*) this->cur_data_key.c_str(), + this->cur_data_key.length()); + } pushPipeline(p); } // Must call this unconditionally so we can call popPipelineStack @@ -722,6 +788,8 @@ QPDFWriter::unparseObject(QPDFObjectHandle object, int level, } else if (object.isDictionary()) { + // XXX Must not preserve Crypt filters from original stream + // dictionary writeString("<<"); writeStringQDF("\n"); std::set keys = object.getKeys(); @@ -836,6 +904,15 @@ QPDFWriter::unparseObject(QPDFObjectHandle object, int level, } this->cur_stream_length = stream_data.getPointer()->getSize(); + if (this->encrypted && + stream_dict.getKey("/Type").isName() && + (stream_dict.getKey("/Type").getName() == "/Metadata") && + (! this->encrypt_metadata)) + { + // Don't encrypt stream data for the metadata stream + this->cur_data_key.clear(); + } + adjustAESStreamLength(this->cur_stream_length); unparseObject(stream_dict, 0, flags, this->cur_stream_length, compress); writeString("\nstream\n"); pushEncryptionFilter(); @@ -864,13 +941,29 @@ QPDFWriter::unparseObject(QPDFObjectHandle object, int level, (! this->cur_data_key.empty())) { val = object.getStringValue(); - char* tmp = QUtil::copy_string(val); - unsigned int vlen = val.length(); - RC4 rc4((unsigned char const*)this->cur_data_key.c_str(), - this->cur_data_key.length()); - rc4.process((unsigned char*)tmp, vlen); - val = QPDF_String(std::string(tmp, vlen)).unparse(); - delete [] tmp; + if (this->encrypt_use_aes) + { + Pl_Buffer bufpl("encrypted string"); + Pl_AES_PDF pl("aes encrypt string", &bufpl, true, + (unsigned char const*)this->cur_data_key.c_str()); + pl.write((unsigned char*) val.c_str(), val.length()); + pl.finish(); + Buffer* buf = bufpl.getBuffer(); + val = QPDF_String( + std::string((char*)buf->getBuffer(), + (size_t)buf->getSize())).unparse(); + delete buf; + } + else + { + char* tmp = QUtil::copy_string(val); + unsigned int vlen = val.length(); + RC4 rc4((unsigned char const*)this->cur_data_key.c_str(), + this->cur_data_key.length()); + rc4.process((unsigned char*)tmp, vlen); + val = QPDF_String(std::string(tmp, vlen)).unparse(); + delete [] tmp; + } } else { @@ -1000,8 +1093,9 @@ QPDFWriter::writeObjectStream(QPDFObjectHandle object) writeStringQDF("\n "); writeString(" /Type /ObjStm"); writeStringQDF("\n "); - writeString(" /Length " + - QUtil::int_to_string(stream_buffer.getPointer()->getSize())); + unsigned long length = stream_buffer.getPointer()->getSize(); + adjustAESStreamLength(length); + writeString(" /Length " + QUtil::int_to_string(length)); writeStringQDF("\n "); if (compressed) { @@ -1489,6 +1583,7 @@ QPDFWriter::writeHintStream(int hint_id) writeString(QUtil::int_to_string(O)); } writeString(" /Length "); + adjustAESStreamLength(hlen); writeString(QUtil::int_to_string(hlen)); writeString(" >>\nstream\n"); diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 85bbea1c..c54504b8 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -72,6 +72,9 @@ Additional flags are dependent upon key length.\n\ --extract=[yn] allow other text/graphic extraction\n\ --print=print-opt control printing access\n\ --modify=modify-opt control modify access\n\ + --cleartext-metadata prevents encryption of metadata\n\ + --use-aes=[yn] indicates whether to use AES encryption\n\ + --force-V4 forces use of V=4 encryption handler\n\ \n\ print-opt may be:\n\ \n\ @@ -89,6 +92,14 @@ Additional flags are dependent upon key length.\n\ \n\ The default for each permission option is to be fully permissive.\n\ \n\ +Specifying cleartext-metadata forces the PDF version to at least 1.5.\n\ +Specifying use of AES forces the PDF version to at least 1.6. These\n\ +options are both off by default.\n\ +\n\ +The --force-V4 flag forces the V=4 encryption handler introduced in PDF 1.5\n\ +to be used even if not otherwise needed. This option is primarily useful\n\ +for testing qpdf and has no other practical use.\n\ +\n\ \n\ Advanced Transformation Options\n\ -------------------------------\n\ @@ -220,7 +231,8 @@ parse_encrypt_options( std::string& user_password, std::string& owner_password, int& keylen, bool& r2_print, bool& r2_modify, bool& r2_extract, bool& r2_annotate, bool& r3_accessibility, bool& r3_extract, - QPDFWriter::r3_print_e& r3_print, QPDFWriter::r3_modify_e& r3_modify) + QPDFWriter::r3_print_e& r3_print, QPDFWriter::r3_modify_e& r3_modify, + bool& force_V4, bool& cleartext_metadata, bool& use_aes) { if (cur_arg + 3 >= argc) { @@ -450,6 +462,65 @@ parse_encrypt_options( usage("-accessibility invalid for 40-bit keys"); } } + else if (strcmp(arg, "cleartext-metadata") == 0) + { + if (parameter) + { + usage("--cleartext-metadata does not take a parameter"); + } + if (keylen == 40) + { + usage("--cleartext-metadata is invalid for 40-bit keys"); + } + else + { + cleartext_metadata = true; + } + } + else if (strcmp(arg, "force-V4") == 0) + { + if (parameter) + { + usage("--force-V4 does not take a parameter"); + } + if (keylen == 40) + { + usage("--force-V4 is invalid for 40-bit keys"); + } + else + { + force_V4 = true; + } + } + else if (strcmp(arg, "use-aes") == 0) + { + if (parameter == 0) + { + usage("--use-aes must be given as --extract=option"); + } + std::string val = parameter; + bool result = false; + if (val == "y") + { + result = true; + } + else if (val == "n") + { + result = false; + } + else + { + usage("invalid -use-aes parameter"); + } + if (keylen == 40) + { + usage("use-aes is invalid for 40-bit keys"); + } + else + { + use_aes = result; + } + } else { usage(std::string("invalid encryption parameter --") + arg); @@ -516,6 +587,9 @@ int main(int argc, char* argv[]) bool r3_extract = true; QPDFWriter::r3_print_e r3_print = QPDFWriter::r3p_full; QPDFWriter::r3_modify_e r3_modify = QPDFWriter::r3m_all; + bool force_V4 = false; + bool cleartext_metadata = false; + bool use_aes = false; bool stream_data_set = false; QPDFWriter::stream_data_e stream_data_mode = QPDFWriter::s_compress; @@ -582,7 +656,8 @@ int main(int argc, char* argv[]) argc, argv, ++i, user_password, owner_password, keylen, r2_print, r2_modify, r2_extract, r2_annotate, - r3_accessibility, r3_extract, r3_print, r3_modify); + r3_accessibility, r3_extract, r3_print, r3_modify, + force_V4, cleartext_metadata, use_aes); encrypt = true; } else if (strcmp(arg, "decrypt") == 0) @@ -988,9 +1063,19 @@ int main(int argc, char* argv[]) } else if (keylen == 128) { - w.setR3EncryptionParameters( - user_password.c_str(), owner_password.c_str(), - r3_accessibility, r3_extract, r3_print, r3_modify); + if (force_V4 || cleartext_metadata || use_aes) + { + w.setR4EncryptionParameters( + user_password.c_str(), owner_password.c_str(), + r3_accessibility, r3_extract, r3_print, r3_modify, + !cleartext_metadata, use_aes); + } + else + { + w.setR3EncryptionParameters( + user_password.c_str(), owner_password.c_str(), + r3_accessibility, r3_extract, r3_print, r3_modify); + } } else {