diff --git a/TODO b/TODO index 8a4baaf2..7c5ea6b5 100644 --- a/TODO +++ b/TODO @@ -1,32 +1,6 @@ General ======= - * See if I can support the encryption format used with /R 5 /V 5 - (AESV3), even though a qpdf-announce subscriber with an adobe.com - email address mentioned that this is deprecated. There is also a - new encryption format coming in a future release (PDF 2.0), which - may be better to support. As of the qpdf 3.0 release, the - specification was not publicly available yet. - - AESV3 encryption is supported with PDF 1.7 extension level 3 and is - being deprecated, but there are plenty of files out there. The - encryption format is decribed in adobe_supplement_iso32000.pdf. - Such a file must specify that it uses these extensions in its - document catalog: - - << - /Type /Catalog - /Extensions << - /ADBE << - /BaseVersion /1.7 - /ExtensionLevel 3 - >> - >> - >> - - Possible sha256 implementations: http://sol-biotech.com/code/sha2/, - http://hashlib2plus.sourceforge.net/ - * Improve the random number seed to make it more secure so that we have stronger random numbers, particularly when multiple files are generated in the same second. This code may need to be diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index 7028bf1c..beb5cb7e 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -224,7 +224,7 @@ class QPDF // Encryption support - enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes }; + enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes, e_aesv3 }; class EncryptionData { public: @@ -326,7 +326,7 @@ class QPDF QPDF_DLL static std::string compute_data_key( std::string const& encryption_key, int objid, int generation, - bool use_aes); + bool use_aes, int encryption_V, int encryption_R); QPDF_DLL static std::string compute_encryption_key( std::string const& password, EncryptionData const& data); @@ -337,6 +337,14 @@ class QPDF int V, int R, int key_len, int P, bool encrypt_metadata, std::string const& id1, std::string& O, std::string& U); + QPDF_DLL + static void compute_encryption_parameters_V5( + char const* user_password, char const* owner_password, + int V, int R, int key_len, int P, bool encrypt_metadata, + std::string const& id1, + std::string& encryption_key, + std::string& O, std::string& U, + std::string& OE, std::string& UE, std::string& Perms); // Return the full user password as stored in the PDF file. If // you are attempting to recover the user password in a // user-presentable form, call getTrimmedUserPassword() instead. @@ -345,6 +353,10 @@ class QPDF // Return human-readable form of user password. QPDF_DLL std::string getTrimmedUserPassword() const; + // Return the previously computed or retrieved encryption key for + // this file + QPDF_DLL + std::string getEncryptionKey() const; // Linearization support @@ -628,6 +640,13 @@ class QPDF void initializeEncryption(); std::string getKeyForObject(int objid, int generation, bool use_aes); void decryptString(std::string&, int objid, int generation); + static std::string compute_encryption_key_from_password( + std::string const& password, EncryptionData const& data); + static std::string recover_encryption_key_with_password( + std::string const& password, EncryptionData const& data); + static std::string recover_encryption_key_with_password( + std::string const& password, EncryptionData const& data, + bool& perms_valid); void decryptStream( Pipeline*& pipeline, int objid, int generation, QPDFObjectHandle& stream_dict, @@ -981,6 +1000,7 @@ class QPDF std::ostream* err_stream; bool attempt_recovery; int encryption_V; + int encryption_R; bool encrypt_metadata; std::map crypt_filters; encryption_method_e cf_stream; diff --git a/include/qpdf/QPDFWriter.hh b/include/qpdf/QPDFWriter.hh index 15963890..a8cf5054 100644 --- a/include/qpdf/QPDFWriter.hh +++ b/include/qpdf/QPDFWriter.hh @@ -223,8 +223,9 @@ class QPDFWriter // content normalization. Note that setting R2 encryption // parameters sets the PDF version to at least 1.3, setting R3 // encryption parameters pushes the PDF version number to at least - // 1.4, and setting R4 parameters pushes the version to at least - // 1.5, or if AES is used, 1.6. + // 1.4, setting R4 parameters pushes the version to at least 1.5, + // or if AES is used, 1.6, and setting R5 or R6 parameters pushes + // the version to at least 1.7 with extension level 3. QPDF_DLL void setR2EncryptionParameters( char const* user_password, char const* owner_password, @@ -241,6 +242,21 @@ class QPDFWriter bool allow_accessibility, bool allow_extract, qpdf_r3_print_e print, qpdf_r3_modify_e modify, bool encrypt_metadata, bool use_aes); + // R5 is deprecated. Do not use it for production use. Writing + // R5 is supported by qpdf primarily to generate test files for + // applications that may need to test R5 support. + QPDF_DLL + void setR5EncryptionParameters( + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + qpdf_r3_print_e print, qpdf_r3_modify_e modify, + bool encrypt_metadata); + QPDF_DLL + void setR6EncryptionParameters( + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + qpdf_r3_print_e print, qpdf_r3_modify_e modify, + bool encrypt_metadata_aes); // Create linearized output. Disables qdf mode, content // normalization, and stream prefiltering. @@ -302,7 +318,8 @@ class QPDFWriter int V, int R, int key_len, long P, std::string const& O, std::string const& U, std::string const& OE, std::string const& UE, std::string const& Perms, - std::string const& id1, std::string const& user_password); + std::string const& id1, std::string const& user_password, + std::string const& encryption_key); void setDataKey(int objid); int openObject(int objid = 0); void closeObject(int objid); @@ -378,6 +395,8 @@ class QPDFWriter bool encrypt_metadata; bool encrypt_use_aes; std::map encryption_dictionary; + int encryption_V; + int encryption_R; std::string id1; // for /ID key of std::string id2; // trailer dictionary diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index bee8cde1..46770106 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -97,6 +97,7 @@ QPDF::QPDF() : err_stream(&std::cerr), attempt_recovery(true), encryption_V(0), + encryption_R(0), encrypt_metadata(true), cf_stream(e_none), cf_string(e_none), diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index 8bfb6ff9..a02239c4 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -67,6 +67,8 @@ QPDFWriter::init() min_extension_level = 0; final_extension_level = 0; forced_extension_level = 0; + encryption_V = 0; + encryption_R = 0; encryption_dict_objid = 0; next_objid = 1; cur_stream_length_id = 0; @@ -343,6 +345,38 @@ QPDFWriter::setR4EncryptionParameters( setEncryptionParameters(user_password, owner_password, 4, 4, 16, clear); } +void +QPDFWriter::setR5EncryptionParameters( + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + qpdf_r3_print_e print, qpdf_r3_modify_e modify, + bool encrypt_metadata) +{ + std::set clear; + interpretR3EncryptionParameters( + clear, user_password, owner_password, + allow_accessibility, allow_extract, print, modify); + this->encrypt_use_aes = true; + this->encrypt_metadata = encrypt_metadata; + setEncryptionParameters(user_password, owner_password, 5, 5, 32, clear); +} + +void +QPDFWriter::setR6EncryptionParameters( + char const* user_password, char const* owner_password, + bool allow_accessibility, bool allow_extract, + qpdf_r3_print_e print, qpdf_r3_modify_e modify, + bool encrypt_metadata) +{ + std::set clear; + interpretR3EncryptionParameters( + clear, user_password, owner_password, + allow_accessibility, allow_extract, print, modify); + this->encrypt_use_aes = true; + this->encrypt_metadata = encrypt_metadata; + setEncryptionParameters(user_password, owner_password, 5, 6, 32, clear); +} + void QPDFWriter::interpretR3EncryptionParameters( std::set& clear, @@ -426,6 +460,12 @@ QPDFWriter::setEncryptionParameters( bits_to_clear.insert(1); bits_to_clear.insert(2); + if (R > 3) + { + // Bit 10 is deprecated and should always be set. + bits_to_clear.erase(10); + } + int P = 0; // Create the complement of P, then invert. for (std::set::iterator iter = bits_to_clear.begin(); @@ -438,11 +478,26 @@ QPDFWriter::setEncryptionParameters( generateID(); std::string O; std::string U; - QPDF::compute_encryption_O_U( - user_password, owner_password, V, R, key_len, P, - this->encrypt_metadata, this->id1, O, U); + std::string OE; + std::string UE; + std::string Perms; + std::string encryption_key; + if (V < 5) + { + QPDF::compute_encryption_O_U( + user_password, owner_password, V, R, key_len, P, + this->encrypt_metadata, this->id1, O, U); + } + else + { + QPDF::compute_encryption_parameters_V5( + user_password, owner_password, V, R, key_len, P, + this->encrypt_metadata, this->id1, + encryption_key, O, U, OE, UE, Perms); + } setEncryptionParametersInternal( - V, R, key_len, P, O, U, "", "", "", this->id1, user_password); + V, R, key_len, P, O, U, OE, UE, Perms, + this->id1, user_password, encryption_key); } void @@ -482,6 +537,19 @@ QPDFWriter::copyEncryptionParameters(QPDF& qpdf) this->encrypt_metadata ? 0 : 1); QTC::TC("qpdf", "QPDFWriter copy use_aes", this->encrypt_use_aes ? 0 : 1); + std::string OE; + std::string UE; + std::string Perms; + std::string encryption_key; + if (V >= 5) + { + QTC::TC("qpdf", "QPDFWriter copy V5"); + OE = encrypt.getKey("/OE").getStringValue(); + UE = encrypt.getKey("/UE").getStringValue(); + Perms = encrypt.getKey("/Perms").getStringValue(); + encryption_key = qpdf.getEncryptionKey(); + } + setEncryptionParametersInternal( V, encrypt.getKey("/R").getIntValue(), @@ -489,11 +557,12 @@ QPDFWriter::copyEncryptionParameters(QPDF& qpdf) encrypt.getKey("/P").getIntValue(), encrypt.getKey("/O").getStringValue(), encrypt.getKey("/U").getStringValue(), - "", // XXXX OE - "", // XXXX UE - "", // XXXX Perms + OE, + UE, + Perms, this->id1, // this->id1 == the other file's id1 - qpdf.getPaddedUserPassword()); + qpdf.getPaddedUserPassword(), + encryption_key); } } @@ -605,10 +674,11 @@ QPDFWriter::setEncryptionParametersInternal( int V, int R, int key_len, long P, std::string const& O, std::string const& U, std::string const& OE, std::string const& UE, std::string const& Perms, - std::string const& id1, std::string const& user_password) + std::string const& id1, std::string const& user_password, + std::string const& encryption_key) { - // XXXX OE, UE, Perms, V=5 - + this->encryption_V = V; + this->encryption_R = R; encryption_dictionary["/Filter"] = "/Standard"; encryption_dictionary["/V"] = QUtil::int_to_string(V); encryption_dictionary["/Length"] = QUtil::int_to_string(key_len * 8); @@ -618,9 +688,15 @@ QPDFWriter::setEncryptionParametersInternal( encryption_dictionary["/U"] = QPDF_String(U).unparse(true); if (V >= 5) { - setMinimumPDFVersion("1.4"); + encryption_dictionary["/OE"] = QPDF_String(OE).unparse(true); + encryption_dictionary["/UE"] = QPDF_String(UE).unparse(true); + encryption_dictionary["/Perms"] = QPDF_String(Perms).unparse(true); } - if (R >= 5) + if (R >= 6) + { + setMinimumPDFVersion("1.7", 8); + } + else if (R == 5) { setMinimumPDFVersion("1.7", 3); } @@ -641,14 +717,16 @@ QPDFWriter::setEncryptionParametersInternal( { encryption_dictionary["/EncryptMetadata"] = "false"; } - if (V == 4) + if ((V == 4) || (V == 5)) { // The spec says the value for the crypt filter key can be // anything, and xpdf seems to agree. However, Adobe Reader // won't open our files unless we use /StdCF. encryption_dictionary["/StmF"] = "/StdCF"; encryption_dictionary["/StrF"] = "/StdCF"; - std::string method = (this->encrypt_use_aes ? "/AESV2" : "/V2"); + std::string method = (this->encrypt_use_aes + ? ((V < 5) ? "/AESV2" : "/AESV3") + : "/V2"); encryption_dictionary["/CF"] = "<< /StdCF << /AuthEvent /DocOpen /CFM " + method + " >> >>"; } @@ -656,15 +734,23 @@ QPDFWriter::setEncryptionParametersInternal( this->encrypted = true; QPDF::EncryptionData encryption_data( V, R, key_len, P, O, U, OE, UE, Perms, id1, this->encrypt_metadata); - this->encryption_key = QPDF::compute_encryption_key( - user_password, encryption_data); + if (V < 5) + { + this->encryption_key = QPDF::compute_encryption_key( + user_password, encryption_data); + } + else + { + this->encryption_key = encryption_key; + } } void QPDFWriter::setDataKey(int objid) { this->cur_data_key = QPDF::compute_data_key( - this->encryption_key, objid, 0, this->encrypt_use_aes); + this->encryption_key, objid, 0, + this->encrypt_use_aes, this->encryption_V, this->encryption_R); } int diff --git a/libqpdf/QPDF_encryption.cc b/libqpdf/QPDF_encryption.cc index 568b8771..60d54b77 100644 --- a/libqpdf/QPDF_encryption.cc +++ b/libqpdf/QPDF_encryption.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -23,9 +24,15 @@ static unsigned char const padding_string[] = { 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a }; -static unsigned int const O_key_bytes = sizeof(MD5::Digest); static unsigned int const key_bytes = 32; +// V4 key lengths apply to V <= 4 +static unsigned int const OU_key_bytes_V4 = sizeof(MD5::Digest); + +static unsigned int const OU_key_bytes_V5 = 48; +static unsigned int const OUE_key_bytes_V5 = 32; +static unsigned int const Perms_key_bytes_V5 = 16; + int QPDF::EncryptionData::getV() const { @@ -120,7 +127,7 @@ QPDF::EncryptionData::setV5EncryptionParameters( } static void -pad_or_truncate_password(std::string const& password, char k1[key_bytes]) +pad_or_truncate_password_V4(std::string const& password, char k1[key_bytes]) { int password_bytes = std::min(key_bytes, (unsigned int)password.length()); int pad_bytes = key_bytes - password_bytes; @@ -159,13 +166,19 @@ QPDF::trim_user_password(std::string& user_password) } static std::string -pad_or_truncate_password(std::string const& password) +pad_or_truncate_password_V4(std::string const& password) { char k1[key_bytes]; - pad_or_truncate_password(password, k1); + pad_or_truncate_password_V4(password, k1); return std::string(k1, key_bytes); } +static std::string +truncate_password_V5(std::string const& password) +{ + return password.substr(0, std::min((size_t)127, password.length())); +} + static void iterate_md5_digest(MD5& md5, MD5::Digest& digest, int iterations) { @@ -199,15 +212,146 @@ iterate_rc4(unsigned char* data, int data_len, delete [] key; } +static std::string +process_with_aes(std::string const& key, + bool encrypt, + std::string const& data, + size_t outlength = 0, + unsigned int repetitions = 1, + unsigned char const* iv = 0, + size_t iv_length = 0) +{ + Pl_Buffer buffer("buffer"); + Pl_AES_PDF aes("aes", &buffer, encrypt, + (unsigned char const*)key.c_str(), + (unsigned int)key.length()); + if (iv) + { + aes.setIV(iv, iv_length); + } + else + { + aes.useZeroIV(); + } + aes.disablePadding(); + for (unsigned int i = 0; i < repetitions; ++i) + { + aes.write((unsigned char*)data.c_str(), data.length()); + } + aes.finish(); + PointerHolder bufp = buffer.getBuffer(); + if (outlength == 0) + { + outlength = bufp->getSize(); + } + else + { + outlength = std::min(outlength, bufp->getSize()); + } + return std::string((char const*)bufp->getBuffer(), outlength); +} + +static std::string +hash_V5(std::string const& password, + std::string const& salt, + std::string const& udata, + QPDF::EncryptionData const& data) +{ + Pl_SHA2 hash(256); + hash.write((unsigned char*)password.c_str(), password.length()); + hash.write((unsigned char*)salt.c_str(), salt.length()); + hash.write((unsigned char*)udata.c_str(), udata.length()); + hash.finish(); + std::string K = hash.getRawDigest(); + + std::string result; + if (data.getR() < 6) + { + result = K; + } + else + { + // Algorithm 2.B from ISO 32000-1 chapter 7: Computing a hash + + int round_number = 0; + bool done = false; + while (! done) + { + // The hash algorithm has us setting K initially to the R5 + // value and then repeating a series of steps 64 times + // before starting with the termination case testing. The + // wording of the specification is very unclear as to the + // exact number of times it should be run since the + // wording about whether the initial setup counts as round + // 0 or not is ambiguous. This code counts the initial + // setup (R5) value as round 0, which appears to be + // correct. This was determined to be correct by + // increasing or decreasing the number of rounds by 1 or 2 + // from this value and generating 20 test files. In this + // interpretation, all the test files worked with Adobe + // Reader X. In the other configurations, many of the + // files did not work, and we were accurately able to + // predict which files didn't work by looking at the + // conditions under which we terminated repetition. + + ++round_number; + std::string K1 = password + K + udata; + assert(K.length() >= 32); + std::string E = process_with_aes( + K.substr(0, 16), true, K1, 0, 64, + (unsigned char*)K.substr(16, 16).c_str(), 16); + + // E_mod_3 is supposed to be mod 3 of the first 16 bytes + // of E taken as as a (128-bit) big-endian number. Since + // (xy mod n) is equal to ((x mod n) + (y mod n)) mod n + // and since 256 mod n is 1, we can just take the sums of + // the the mod 3s of each byte to get the same result. + int E_mod_3 = 0; + for (unsigned int i = 0; i < 16; ++i) + { + E_mod_3 += (unsigned char)E[i]; + } + E_mod_3 %= 3; + int next_hash = ((E_mod_3 == 0) ? 256 : + (E_mod_3 == 1) ? 384 : + 512); + Pl_SHA2 hash(next_hash); + hash.write((unsigned char*)E.c_str(), E.length()); + hash.finish(); + K = hash.getRawDigest(); + + if (round_number >= 64) + { + unsigned int ch = (unsigned int)((unsigned char) *(E.rbegin())); + + if (ch <= (unsigned int)(round_number - 32)) + { + done = true; + } + } + } + result = K.substr(0, 32); + } + + return result; +} + std::string QPDF::compute_data_key(std::string const& encryption_key, - int objid, int generation, - bool use_aes) + int objid, int generation, bool use_aes, + int encryption_V, int encryption_R) { // Algorithm 3.1 from the PDF 1.7 Reference Manual std::string result = encryption_key; + if (encryption_V >= 5) + { + // Algorithm 3.1a (PDF 1.7 extension level 3): just use + // encryption key straight. + return result; + } + // Append low three bytes of object ID and low two bytes of generation result += (char) (objid & 0xff); result += (char) ((objid >> 8) & 0xff); @@ -230,12 +374,38 @@ QPDF::compute_data_key(std::string const& encryption_key, std::string QPDF::compute_encryption_key( std::string const& password, EncryptionData const& data) +{ + if (data.getV() >= 5) + { + // For V >= 5, the encryption key is generated and stored in + // the file, encrypted separately with both user and owner + // passwords. + return recover_encryption_key_with_password(password, data); + } + else + { + // For V < 5, the encryption key is derived from the user + // password. + return compute_encryption_key_from_password(password, data); + } +} + +std::string +QPDF::compute_encryption_key_from_password( + std::string const& password, EncryptionData const& data) { // Algorithm 3.2 from the PDF 1.7 Reference Manual + // This code does not properly handle Unicode passwords. + // Passwords are supposed to be converted from OS codepage + // characters to PDFDocEncoding. Unicode passwords are supposed + // to be converted to OS codepage before converting to + // PDFDocEncoding. We instead require the password to be + // presented in its final form. + MD5 md5; md5.encodeDataIncrementally( - pad_or_truncate_password(password).c_str(), key_bytes); + pad_or_truncate_password_V4(password).c_str(), key_bytes); md5.encodeDataIncrementally(data.getO().c_str(), key_bytes); char pbytes[4]; int P = data.getP(); @@ -261,8 +431,13 @@ static void compute_O_rc4_key(std::string const& user_password, std::string const& owner_password, QPDF::EncryptionData const& data, - unsigned char key[O_key_bytes]) + unsigned char key[OU_key_bytes_V4]) { + if (data.getV() >= 5) + { + throw std::logic_error( + "compute_O_rc4_key called for file with V >= 5"); + } std::string password = owner_password; if (password.empty()) { @@ -270,10 +445,10 @@ compute_O_rc4_key(std::string const& user_password, } MD5 md5; md5.encodeDataIncrementally( - pad_or_truncate_password(password).c_str(), key_bytes); + pad_or_truncate_password_V4(password).c_str(), key_bytes); MD5::Digest digest; iterate_md5_digest(md5, digest, ((data.getR() >= 3) ? 50 : 0)); - memcpy(key, digest, O_key_bytes); + memcpy(key, digest, OU_key_bytes_V4); } static std::string @@ -283,11 +458,11 @@ compute_O_value(std::string const& user_password, { // Algorithm 3.3 from the PDF 1.7 Reference Manual - unsigned char O_key[O_key_bytes]; + unsigned char O_key[OU_key_bytes_V4]; compute_O_rc4_key(user_password, owner_password, data, O_key); char upass[key_bytes]; - pad_or_truncate_password(user_password, upass); + pad_or_truncate_password_V4(user_password, upass); iterate_rc4((unsigned char*) upass, key_bytes, O_key, data.getLengthBytes(), (data.getR() >= 3) ? 20 : 1, false); @@ -303,7 +478,7 @@ compute_U_value_R2(std::string const& user_password, std::string k1 = QPDF::compute_encryption_key(user_password, data); char udata[key_bytes]; - pad_or_truncate_password("", udata); + pad_or_truncate_password_V4("", udata); iterate_rc4((unsigned char*) udata, key_bytes, (unsigned char*)k1.c_str(), data.getLengthBytes(), 1, false); return std::string(udata, key_bytes); @@ -319,7 +494,7 @@ compute_U_value_R3(std::string const& user_password, std::string k1 = QPDF::compute_encryption_key(user_password, data); MD5 md5; md5.encodeDataIncrementally( - pad_or_truncate_password("").c_str(), key_bytes); + pad_or_truncate_password_V4("").c_str(), key_bytes); md5.encodeDataIncrementally(data.getId1().c_str(), (int)data.getId1().length()); MD5::Digest digest; @@ -350,38 +525,212 @@ compute_U_value(std::string const& user_password, } static bool -check_user_password(std::string const& user_password, - QPDF::EncryptionData const& data) +check_user_password_V4(std::string const& user_password, + QPDF::EncryptionData const& data) { // Algorithm 3.6 from the PDF 1.7 Reference Manual std::string u_value = compute_U_value(user_password, data); - int to_compare = ((data.getR() >= 3) ? sizeof(MD5::Digest) : key_bytes); + int to_compare = ((data.getR() >= 3) ? sizeof(MD5::Digest) + : key_bytes); return (memcmp(data.getU().c_str(), u_value.c_str(), to_compare) == 0); } +static bool +check_user_password_V5(std::string const& user_password, + QPDF::EncryptionData const& data) +{ + // Algorithm 3.11 from the PDF 1.7 extension level 3 + + std::string user_data = data.getU().substr(0, 32); + std::string validation_salt = data.getU().substr(32, 8); + std::string password = truncate_password_V5(user_password); + return (hash_V5(password, validation_salt, "", data) == user_data); +} + +static bool +check_user_password(std::string const& user_password, + QPDF::EncryptionData const& data) +{ + if (data.getV() < 5) + { + return check_user_password_V4(user_password, data); + } + else + { + return check_user_password_V5(user_password, data); + } +} + +static bool +check_owner_password_V4(std::string& user_password, + std::string const& owner_password, + QPDF::EncryptionData const& data) +{ + // Algorithm 3.7 from the PDF 1.7 Reference Manual + + unsigned char key[OU_key_bytes_V4]; + compute_O_rc4_key(user_password, owner_password, data, key); + unsigned char O_data[key_bytes]; + memcpy(O_data, (unsigned char*) data.getO().c_str(), key_bytes); + iterate_rc4(O_data, key_bytes, key, data.getLengthBytes(), + (data.getR() >= 3) ? 20 : 1, true); + std::string new_user_password = + std::string((char*)O_data, key_bytes); + bool result = false; + if (check_user_password(new_user_password, data)) + { + result = true; + user_password = new_user_password; + } + return result; +} + +static bool +check_owner_password_V5(std::string const& owner_password, + QPDF::EncryptionData const& data) +{ + // Algorithm 3.12 from the PDF 1.7 extension level 3 + + std::string user_data = data.getU().substr(0, 48); + std::string owner_data = data.getO().substr(0, 32); + std::string validation_salt = data.getO().substr(32, 8); + std::string password = truncate_password_V5(owner_password); + return (hash_V5(password, validation_salt, user_data, + data) == owner_data); +} + static bool check_owner_password(std::string& user_password, std::string const& owner_password, QPDF::EncryptionData const& data) { - // Algorithm 3.7 from the PDF 1.7 Reference Manual - - unsigned char key[O_key_bytes]; - compute_O_rc4_key(user_password, owner_password, data, key); - unsigned char O_data[key_bytes]; - memcpy(O_data, (unsigned char*) data.getO().c_str(), key_bytes); - iterate_rc4(O_data, key_bytes, key, data.getLengthBytes(), - (data.getR() >= 3) ? 20 : 1, true); - std::string new_user_password = - std::string((char*)O_data, key_bytes); - bool result = false; - if (check_user_password(new_user_password, data)) + if (data.getV() < 5) { - result = true; - user_password = new_user_password; + return check_owner_password_V4(user_password, owner_password, data); } - return result; + else + { + return check_owner_password_V5(owner_password, data); + } +} + +std::string +QPDF::recover_encryption_key_with_password( + std::string const& password, EncryptionData const& data) +{ + // Disregard whether Perms is valid. + bool disregard; + return recover_encryption_key_with_password(password, data, disregard); +} + +static void +compute_U_UE_value_V5(std::string const& user_password, + std::string const& encryption_key, + QPDF::EncryptionData const& data, + std::string& U, std::string& UE) +{ + // Algorithm 3.8 from the PDF 1.7 extension level 3 + char k[16]; + QUtil::initializeWithRandomBytes((unsigned char*) k, sizeof(k)); + std::string validation_salt(k, 8); + std::string key_salt(k + 8, 8); + U = hash_V5(user_password, validation_salt, "", data) + + validation_salt + key_salt; + std::string intermediate_key = hash_V5(user_password, key_salt, "", data); + UE = process_with_aes(intermediate_key, true, encryption_key); +} + +static void +compute_O_OE_value_V5(std::string const& owner_password, + std::string const& encryption_key, + QPDF::EncryptionData const& data, + std::string const& U, + std::string& O, std::string& OE) +{ + // Algorithm 3.9 from the PDF 1.7 extension level 3 + char k[16]; + QUtil::initializeWithRandomBytes((unsigned char*) k, sizeof(k)); + std::string validation_salt(k, 8); + std::string key_salt(k + 8, 8); + O = hash_V5(owner_password, validation_salt, U, data) + + validation_salt + key_salt; + std::string intermediate_key = hash_V5(owner_password, key_salt, U, data); + OE = process_with_aes(intermediate_key, true, encryption_key); +} + +void +compute_Perms_value_V5_clear(std::string const& encryption_key, + QPDF::EncryptionData const& data, + unsigned char k[16]) +{ + // From algorithm 3.10 from the PDF 1.7 extension level 3 + unsigned long long extended_perms = 0xffffffff00000000LL | data.getP(); + for (int i = 0; i < 8; ++i) + { + k[i] = (unsigned char) (extended_perms & 0xff); + extended_perms >>= 8; + } + k[8] = data.getEncryptMetadata() ? 'T' : 'F'; + k[9] = 'a'; + k[10] = 'd'; + k[11] = 'b'; + QUtil::initializeWithRandomBytes(k + 12, 4); +} + +static std::string +compute_Perms_value_V5(std::string const& encryption_key, + QPDF::EncryptionData const& data) +{ + // Algorithm 3.10 from the PDF 1.7 extension level 3 + unsigned char k[16]; + compute_Perms_value_V5_clear(encryption_key, data, k); + return process_with_aes(encryption_key, true, + std::string((char const*) k, sizeof(k))); +} + +std::string +QPDF::recover_encryption_key_with_password( + std::string const& password, EncryptionData const& data, + bool& perms_valid) +{ + // Algorithm 3.2a from the PDF 1.7 extension level 3 + + // This code does not handle Unicode passwords correctly. + // Empirical evidence suggests that most viewers don't. We are + // supposed to process the input string with the SASLprep (RFC + // 4013) profile of stringprep (RFC 3454) and then convert the + // result to UTF-8. + + perms_valid = false; + std::string key_password = truncate_password_V5(password); + std::string key_salt; + std::string user_data; + std::string encrypted_file_key; + if (check_owner_password_V5(key_password, data)) + { + key_salt = data.getO().substr(40, 8); + user_data = data.getU().substr(0, 48); + encrypted_file_key = data.getOE().substr(0, 32); + } + else if (check_user_password_V5(key_password, data)) + { + key_salt = data.getU().substr(40, 8); + encrypted_file_key = data.getUE().substr(0, 32); + } + std::string intermediate_key = + hash_V5(key_password, key_salt, user_data, data); + std::string file_key = + process_with_aes(intermediate_key, false, encrypted_file_key); + + // Decrypt Perms and check against expected value + std::string perms_check = + process_with_aes(file_key, false, data.getPerms(), 12); + unsigned char k[16]; + compute_Perms_value_V5_clear(file_key, data, k); + perms_valid = (memcmp(perms_check.c_str(), k, 12) == 0); + + return file_key; } QPDF::encryption_method_e @@ -487,29 +836,70 @@ QPDF::initializeEncryption() std::string U = encryption_dict.getKey("/U").getStringValue(); unsigned int P = (unsigned int) encryption_dict.getKey("/P").getIntValue(); - if (! (((R == 2) || (R == 3) || (R == 4)) && - ((V == 1) || (V == 2) || (V == 4)))) + // If supporting new encryption R/V values, remember to update + // error message inside this if statement. + if (! (((R >= 2) && (R <= 6)) && + ((V == 1) || (V == 2) || (V == 4) || (V == 5)))) { throw QPDFExc(qpdf_e_unsupported, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), - "Unsupported /R or /V in encryption dictionary"); + "Unsupported /R or /V in encryption dictionary; R = " + + QUtil::int_to_string(R) + " (max 6), V = " + + QUtil::int_to_string(V) + " (max 5)"); } this->encryption_V = V; + this->encryption_R = R; - if (! ((O.length() == key_bytes) && (U.length() == key_bytes))) + // OE, UE, and Perms are only present if V >= 5. + std::string OE; + std::string UE; + std::string Perms; + + if (V < 5) { - throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), - "encryption dictionary", this->file->getLastOffset(), - "incorrect length for /O and/or /P in " - "encryption dictionary"); + if (! ((O.length() == key_bytes) && (U.length() == key_bytes))) + { + throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), + "encryption dictionary", this->file->getLastOffset(), + "incorrect length for /O and/or /U in " + "encryption dictionary"); + } + } + else + { + if (! (encryption_dict.getKey("/OE").isString() && + encryption_dict.getKey("/UE").isString() && + encryption_dict.getKey("/Perms").isString())) + { + throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), + "encryption dictionary", this->file->getLastOffset(), + "some V=5 encryption dictionary parameters are " + "missing or the wrong type"); + } + OE = encryption_dict.getKey("/OE").getStringValue(); + UE = encryption_dict.getKey("/UE").getStringValue(); + Perms = encryption_dict.getKey("/Perms").getStringValue(); + + if ((O.length() < OU_key_bytes_V5) || + (U.length() < OU_key_bytes_V5) || + (OE.length() < OUE_key_bytes_V5) || + (UE.length() < OUE_key_bytes_V5) || + (Perms.length() < Perms_key_bytes_V5)) + { + throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), + "encryption dictionary", this->file->getLastOffset(), + "incorrect length for some of" + " /O, /U, /OE, /UE, or /Perms in" + " encryption dictionary"); + } } int Length = 40; if (encryption_dict.getKey("/Length").isInteger()) { Length = encryption_dict.getKey("/Length").getIntValue(); - if ((Length % 8) || (Length < 40) || (Length > 128)) + if ((Length % 8) || (Length < 40) || (Length > 256)) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), @@ -524,7 +914,7 @@ QPDF::initializeEncryption() encryption_dict.getKey("/EncryptMetadata").getBoolValue(); } - if (V == 4) + if ((V == 4) || (V == 5)) { QPDFObjectHandle CF = encryption_dict.getKey("/CF"); std::set keys = CF.getKeys(); @@ -549,6 +939,11 @@ QPDF::initializeEncryption() QTC::TC("qpdf", "QPDF_encryption CFM AESV2"); method = e_aes; } + else if (method_name == "/AESV3") + { + QTC::TC("qpdf", "QPDF_encryption CFM AESV3"); + method = e_aesv3; + } else { // Don't complain now -- maybe we won't need @@ -574,13 +969,14 @@ QPDF::initializeEncryption() this->cf_file = this->cf_stream; } } - EncryptionData data(V, R, Length / 8, P, O, U, "", "", "", + + EncryptionData data(V, R, Length / 8, P, O, U, OE, UE, Perms, id1, this->encrypt_metadata); if (check_owner_password( this->user_password, this->provided_password, data)) { // password supplied was owner password; user_password has - // been initialized + // been initialized for V < 5 } else if (check_user_password(this->provided_password, data)) { @@ -592,7 +988,30 @@ QPDF::initializeEncryption() "", 0, "invalid password"); } - this->encryption_key = compute_encryption_key(this->user_password, data); + if (V < 5) + { + // For V < 5, the user password is encrypted with the owner + // password, and the user password is always used for + // computing the encryption key. + this->encryption_key = compute_encryption_key( + this->user_password, data); + } + else + { + // For V >= 5, either password can be used independently to + // compute the encryption key, and neither password can be + // used to recover the other. + bool perms_valid; + this->encryption_key = recover_encryption_key_with_password( + this->provided_password, data, perms_valid); + if (! perms_valid) + { + warn(QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), + "encryption dictionary", this->file->getLastOffset(), + "/Perms field in encryption dictionary" + " doesn't match expected value")); + } + } } std::string @@ -608,7 +1027,8 @@ QPDF::getKeyForObject(int objid, int generation, bool use_aes) (generation == this->cached_key_generation))) { this->cached_object_encryption_key = - compute_data_key(this->encryption_key, objid, generation, use_aes); + compute_data_key(this->encryption_key, objid, generation, + use_aes, this->encryption_V, this->encryption_R); this->cached_key_objid = objid; this->cached_key_generation = generation; } @@ -624,7 +1044,7 @@ QPDF::decryptString(std::string& str, int objid, int generation) return; } bool use_aes = false; - if (this->encryption_V == 4) + if (this->encryption_V >= 4) { switch (this->cf_string) { @@ -635,6 +1055,10 @@ QPDF::decryptString(std::string& str, int objid, int generation) use_aes = true; break; + case e_aesv3: + use_aes = true; + break; + case e_rc4: break; @@ -710,7 +1134,7 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, return; } bool use_aes = false; - if (this->encryption_V == 4) + if (this->encryption_V >= 4) { encryption_method_e method = e_unknown; std::string method_source = "/StmF from /Encrypt dictionary"; @@ -747,7 +1171,7 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, if (crypt_params.isDictionary() && crypt_params.getKey("/Name").isName()) { -// XXX QTC::TC("qpdf", "QPDF_encrypt crypt array"); + QTC::TC("qpdf", "QPDF_encrypt crypt array"); method = interpretCF( crypt_params.getKey("/Name")); method_source = "stream's Crypt " @@ -790,6 +1214,10 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, use_aes = true; break; + case e_aesv3: + use_aes = true; + break; + case e_rc4: break; @@ -831,6 +1259,11 @@ QPDF::compute_encryption_O_U( int V, int R, int key_len, int P, bool encrypt_metadata, std::string const& id1, std::string& O, std::string& U) { + if (V >= 5) + { + throw std::logic_error( + "compute_encryption_O_U called for file with V >= 5"); + } EncryptionData data(V, R, key_len, P, "", "", "", "", "", id1, encrypt_metadata); data.setO(compute_O_value(user_password, owner_password, data)); @@ -839,6 +1272,26 @@ QPDF::compute_encryption_O_U( U = data.getU(); } +void +QPDF::compute_encryption_parameters_V5( + char const* user_password, char const* owner_password, + int V, int R, int key_len, int P, bool encrypt_metadata, + std::string const& id1, + std::string& encryption_key, + std::string& O, std::string& U, + std::string& OE, std::string& UE, std::string& Perms) +{ + EncryptionData data(V, R, key_len, P, "", "", "", "", "", + id1, encrypt_metadata); + unsigned char k[key_bytes]; + QUtil::initializeWithRandomBytes(k, key_bytes); + encryption_key = std::string((char const*)k, key_bytes); + compute_U_UE_value_V5(user_password, encryption_key, data, U, UE); + compute_O_OE_value_V5(owner_password, encryption_key, data, U, O, OE); + Perms = compute_Perms_value_V5(encryption_key, data); + data.setV5EncryptionParameters(O, OE, U, UE, Perms); +} + std::string const& QPDF::getPaddedUserPassword() const { @@ -853,6 +1306,12 @@ QPDF::getTrimmedUserPassword() const return result; } +std::string +QPDF::getEncryptionKey() const +{ + return this->encryption_key; +} + bool QPDF::isEncrypted() const { diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index e5b47494..bd64618d 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -97,7 +97,7 @@ Note that -- terminates parsing of encryption flags.\n\ Either or both of the user password and the owner password may be\n\ empty strings.\n\ \n\ -key-length may be 40 or 128\n\ +key-length may be 40, 128, or 256\n\ \n\ Additional flags are dependent upon key length.\n\ \n\ @@ -117,6 +117,11 @@ Additional flags are dependent upon key length.\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\ + If 256, options are the same as 128 with these exceptions:\n\ + --force-V4 this option is not available with 256-bit keys\n\ + --use-aes this option is always on with 256-bit keys\n\ + --force-R5 forces use of deprecated R=5 encryption\n\ \n\ print-opt may be:\n\ \n\ @@ -283,6 +288,9 @@ static std::string show_encryption_method(QPDF::encryption_method_e method) case QPDF::e_aes: result = "AESv2"; break; + case QPDF::e_aesv3: + result = "AESv3"; + break; // no default so gcc will warn for missing case } return result; @@ -485,7 +493,8 @@ parse_encrypt_options( bool& r2_print, bool& r2_modify, bool& r2_extract, bool& r2_annotate, bool& r3_accessibility, bool& r3_extract, qpdf_r3_print_e& r3_print, qpdf_r3_modify_e& r3_modify, - bool& force_V4, bool& cleartext_metadata, bool& use_aes) + bool& force_V4, bool& cleartext_metadata, bool& use_aes, + bool& force_R5) { if (cur_arg + 3 >= argc) { @@ -502,9 +511,14 @@ parse_encrypt_options( { keylen = 128; } + else if (len_str == "256") + { + keylen = 256; + use_aes = true; + } else { - usage("encryption key length must be 40 or 128"); + usage("encryption key length must be 40, 128, or 256"); } while (1) { @@ -736,15 +750,30 @@ parse_encrypt_options( { usage("--force-V4 does not take a parameter"); } - if (keylen == 40) + if (keylen != 128) { - usage("--force-V4 is invalid for 40-bit keys"); + usage("--force-V4 is invalid only for 128-bit keys"); } else { force_V4 = true; } } + else if (strcmp(arg, "force-R5") == 0) + { + if (parameter) + { + usage("--force-R5 does not take a parameter"); + } + if (keylen != 256) + { + usage("--force-R5 is invalid only for 256-bit keys"); + } + else + { + force_R5 = true; + } + } else if (strcmp(arg, "use-aes") == 0) { if (parameter == 0) @@ -765,10 +794,16 @@ parse_encrypt_options( { usage("invalid -use-aes parameter"); } - if (keylen == 40) + if ((keylen == 40) && result) { usage("use-aes is invalid for 40-bit keys"); } + else if ((keylen == 256) && (! result)) + { + // qpdf would happily create files encrypted with RC4 + // using /V=5, but Adobe reader can't read them. + usage("use-aes can't be disabled with 256-bit keys"); + } else { use_aes = result; @@ -921,6 +956,7 @@ int main(int argc, char* argv[]) qpdf_r3_print_e r3_print = qpdf_r3p_full; qpdf_r3_modify_e r3_modify = qpdf_r3m_all; bool force_V4 = false; + bool force_R5 = false; bool cleartext_metadata = false; bool use_aes = false; @@ -1004,7 +1040,7 @@ int main(int argc, char* argv[]) user_password, owner_password, keylen, r2_print, r2_modify, r2_extract, r2_annotate, r3_accessibility, r3_extract, r3_print, r3_modify, - force_V4, cleartext_metadata, use_aes); + force_V4, cleartext_metadata, use_aes, force_R5); encrypt = true; decrypt = false; copy_encryption = false; @@ -1612,6 +1648,23 @@ int main(int argc, char* argv[]) r3_accessibility, r3_extract, r3_print, r3_modify); } } + else if (keylen == 256) + { + if (force_R5) + { + w.setR5EncryptionParameters( + user_password.c_str(), owner_password.c_str(), + r3_accessibility, r3_extract, r3_print, r3_modify, + !cleartext_metadata); + } + else + { + w.setR6EncryptionParameters( + user_password.c_str(), owner_password.c_str(), + r3_accessibility, r3_extract, r3_print, r3_modify, + !cleartext_metadata); + } + } else { throw std::logic_error("bad encryption keylen"); diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index 4b748bc8..fd80bfb2 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -243,6 +243,7 @@ QPDFWriter extra header text add newline 0 QPDF bogus 0 offset 0 QPDF global offset 0 QPDFWriter make stream key direct 0 +QPDFWriter copy V5 0 QPDFWriter increasing extension level 0 QPDFWriter make Extensions direct 0 QPDFWriter make ADBE direct 1 @@ -253,3 +254,5 @@ QPDFWriter remove existing Extensions 0 QPDFWriter skip Extensions 0 QPDFWriter preserve ADBE 0 QPDF_encryption skip 0x28 0 +QPDF_encrypt crypt array 0 +QPDF_encryption CFM AESV3 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index db1c6033..58bc764f 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -1250,6 +1250,10 @@ $td->notify("--- Encryption Tests ---"); # resulting files were saved and manually checked with Acrobat 5.0 to # ensure that the security settings were as intended. +# The enc-XI-file.pdf files were treated the same way but with Acrobat +# XI instead of Acrobat 5.0. They were used to create test files with +# newer encryption formats. + # Values: basename, password, encryption flags, /P Encrypt key, # extract-for-accessibility, extract-for-any-purpose, # print-low-res, print-high-res, modify-assembly, modify-forms, @@ -1293,9 +1297,25 @@ my @encrypted_files = '', -4, 1, 1, 1, 1, 1, 1, 1, 1, 1], ['long-password', 'asdf asdf asdf asdf asdf asdf qwer'], - ['long-password', 'asdf asdf asdf asdf asdf asdf qw']); + ['long-password', 'asdf asdf asdf asdf asdf asdf qw'], + ['XI-base', ''], + ['XI-R6,V5,O=master', '', + '-extract=n -print=none -modify=assembly', -2368, + 1, 0, 0, 0, 1, 0, 0, 0, 0], + ['XI-R6,V5,O=master', 'master', + '-extract=n -print=none -modify=assembly', -2368, + 1, 0, 0, 0, 1, 0, 0, 0, 0], + ['XI-R6,V5,U=view,O=master', 'view', + '-print=low', -2052, + 1, 1, 1, 0, 1, 1, 1, 1, 1], + ['XI-R6,V5,U=view,O=master', 'master', + '-print=low', -2052, + 1, 1, 1, 0, 1, 1, 1, 1, 1], + ['XI-long-password', 'qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'], + ['XI-long-password', 'qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcv'], + ); -$n_tests += 3 + (2 * (@encrypted_files)) + (6 * (@encrypted_files - 3)) + 9; +$n_tests += 5 + (2 * (@encrypted_files)) + (6 * (@encrypted_files - 6)) + 9; $td->runtest("encrypted file", {$td->COMMAND => "test_driver 2 U25A0.pdf"}, @@ -1312,6 +1332,19 @@ $td->runtest("recheck encrypted file", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Test that long passwords that are one character too short fail. We +# test the truncation cases in the loop below by using passwords +# longer than the supported length. +$td->runtest("significant password characters (V < 5)", + {$td->COMMAND => "qpdf --check enc-long-password.pdf" . + " --password='asdf asdf asdf asdf asdf asdf q'"}, + {$td->REGEXP => ".*invalid password.*", $td->EXIT_STATUS => 2}); +$td->runtest("significant password characters (V = 5)", + {$td->COMMAND => "qpdf --check enc-XI-long-password.pdf" . + " --password=qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxc"}, + {$td->REGEXP => ".*invalid password.*", $td->EXIT_STATUS => 2}); + +my $enc_base = undef; foreach my $d (@encrypted_files) { my ($file, $pass, $xeflags, $P, @@ -1330,17 +1363,26 @@ foreach my $d (@encrypted_files) "modify annotations: " . &$f($modifyannot) . "\n" . "modify other: " . &$f($modifyother) . "\n" . "modify anything: " . &$f($modifyall) . "\n"; + if ($file =~ m/XI-/) + { + $enc_details .= + "stream encryption method: AESv3\n" . + "string encryption method: AESv3\n" . + "file encryption method: AESv3\n"; + } # Test writing to stdout $td->runtest("decrypt $file", {$td->COMMAND => - "qpdf --static-id -qdf --no-original-object-ids" . + "qpdf --static-id -qdf --object-streams=disable" . + " --no-original-object-ids" . " --password=\"$pass\" enc-$file.pdf -" . " > $file.enc"}, {$td->STRING => "", $td->EXIT_STATUS => 0}); - if ($file eq 'base') + if ($file =~ m/base$/) { + $enc_base = $file; $td->runtest("check ID", {$td->COMMAND => "perl check-ID.pl $file.enc"}, {$td->STRING => "ID okay\n", @@ -1350,20 +1392,27 @@ foreach my $d (@encrypted_files) else { $td->runtest("check against base", - {$td->COMMAND => "./diff-encrypted base.enc $file.enc"}, + {$td->COMMAND => + "./diff-encrypted $enc_base.enc $file.enc"}, {$td->STRING => "okay\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); } - if ($file =~ m/^R(\d),V(\d)(?:,U=(\w+))?(?:,O=(\w+))?$/) + if ($file =~ m/^(?:XI-)?R(\d),V(\d)(?:,U=(\w+))?(?:,O=(\w+))?$/) { my $R = $1; my $V = $2; my $upass = $3 || ""; my $opass = $4 || ""; - my $bits = (($V == 2) ? 128 : 40); + my $bits = (($V == 5) ? 256 : ($V == 2) ? 128 : 40); my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; + if (($pass ne $upass) && ($V >= 5)) + { + # V >= 5 can no longer recover user password with owner + # password. + $upass = ""; + } $td->runtest("encrypt $file", {$td->COMMAND => "qpdf --static-id --no-original-object-ids -qdf" . @@ -1488,7 +1537,7 @@ $td->runtest("check linearization", $td->NORMALIZE_NEWLINES); # Test AES encryption in various ways. -$n_tests += 14; +$n_tests += 18; $td->runtest("encrypt with AES", {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . " enc-base.pdf a.pdf"}, @@ -1548,6 +1597,24 @@ $td->runtest("make sure there is no xref stream", {$td->COMMAND => "grep /ObjStm b.pdf | wc -l"}, {$td->REGEXP => "\\s*0\\s*", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("encrypt with V=5,R=5", + {$td->COMMAND => + "qpdf --encrypt user owner 256 --force-R5 -- " . + "minimal.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check encryption", + {$td->COMMAND => "qpdf --check a.pdf --password=owner"}, + {$td->FILE => "V5R5.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("encrypt with V=5,R=6", + {$td->COMMAND => + "qpdf --encrypt user owner 256 -- " . + "minimal.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check encryption", + {$td->COMMAND => "qpdf --check a.pdf --password=user"}, + {$td->FILE => "V5R6.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); # Look at some actual V4 files $n_tests += 14; @@ -1629,6 +1696,36 @@ $td->runtest("compare qdf", {$td->STRING => "okay\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Files with attachments +my @attachments = ( + 'enc-XI-attachments-base.pdf', + 'enc-XI-R6,V5,U=attachment,encrypted-attachments.pdf', + 'enc-XI-R6,V5,U=view,attachments,cleartext-metadata.pdf'); +$n_tests += 4 * @attachments; +foreach my $f (@attachments) +{ + my $pass = ''; + my $tpass = ''; + if ($f =~ m/U=([^,]+)/) + { + $pass = "--password=$1"; + $tpass = $1; + } + $td->runtest("decrypt $f", + {$td->COMMAND => "qpdf --decrypt $pass $f a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); + $td->runtest("extract attachments", + {$td->COMMAND => "test_driver 35 a.pdf"}, + {$td->FILE => "attachments.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("copy $f", + {$td->COMMAND => "qpdf $pass $f a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); + $td->runtest("extract attachments", + {$td->COMMAND => "test_driver 35 a.pdf $tpass"}, + {$td->FILE => "attachments.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +} show_ntests(); # ---------- diff --git a/qpdf/qtest/qpdf/V5R5.out b/qpdf/qtest/qpdf/V5R5.out new file mode 100644 index 00000000..91fcd99a --- /dev/null +++ b/qpdf/qtest/qpdf/V5R5.out @@ -0,0 +1,20 @@ +checking a.pdf +PDF Version: 1.7 extension level 3 +R = 5 +P = -4 +User password = +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv3 +string encryption method: AESv3 +file encryption method: AESv3 +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect diff --git a/qpdf/qtest/qpdf/V5R6.out b/qpdf/qtest/qpdf/V5R6.out new file mode 100644 index 00000000..fa5569a3 --- /dev/null +++ b/qpdf/qtest/qpdf/V5R6.out @@ -0,0 +1,20 @@ +checking a.pdf +PDF Version: 1.7 extension level 8 +R = 6 +P = -4 +User password = user +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv3 +string encryption method: AESv3 +file encryption method: AESv3 +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect diff --git a/qpdf/qtest/qpdf/attachments.out b/qpdf/qtest/qpdf/attachments.out new file mode 100644 index 00000000..b4caa33b --- /dev/null +++ b/qpdf/qtest/qpdf/attachments.out @@ -0,0 +1,6 @@ +attachment1.txt: +This is the first attachment. +--END-- +attachment2.png: +.PNG........IHDR...1 (2620 bytes)--END-- +test 35 done diff --git a/qpdf/qtest/qpdf/diff-encrypted b/qpdf/qtest/qpdf/diff-encrypted index a68822ec..d730536c 100755 --- a/qpdf/qtest/qpdf/diff-encrypted +++ b/qpdf/qtest/qpdf/diff-encrypted @@ -1,5 +1,5 @@ #!/bin/sh -lines=$(expr + $(diff $1 $2 | egrep '^[<>]' | egrep -v 'Date' | wc -l)) +lines=$(expr + $(diff $1 $2 | egrep '^[<>]' | egrep -v '(Date|InstanceID)' | wc -l)) if [ "$lines" = "0" ]; then echo okay else diff --git a/qpdf/qtest/qpdf/enc-XI-R6,V5,O=master.pdf b/qpdf/qtest/qpdf/enc-XI-R6,V5,O=master.pdf new file mode 100644 index 00000000..a3774a43 Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-R6,V5,O=master.pdf differ diff --git a/qpdf/qtest/qpdf/enc-XI-R6,V5,U=attachment,encrypted-attachments.pdf b/qpdf/qtest/qpdf/enc-XI-R6,V5,U=attachment,encrypted-attachments.pdf new file mode 100644 index 00000000..7c1a02fa Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-R6,V5,U=attachment,encrypted-attachments.pdf differ diff --git a/qpdf/qtest/qpdf/enc-XI-R6,V5,U=view,O=master.pdf b/qpdf/qtest/qpdf/enc-XI-R6,V5,U=view,O=master.pdf new file mode 100644 index 00000000..2bb72655 Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-R6,V5,U=view,O=master.pdf differ diff --git a/qpdf/qtest/qpdf/enc-XI-R6,V5,U=view,attachments,cleartext-metadata.pdf b/qpdf/qtest/qpdf/enc-XI-R6,V5,U=view,attachments,cleartext-metadata.pdf new file mode 100644 index 00000000..5adff443 Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-R6,V5,U=view,attachments,cleartext-metadata.pdf differ diff --git a/qpdf/qtest/qpdf/enc-XI-attachments-base.pdf b/qpdf/qtest/qpdf/enc-XI-attachments-base.pdf new file mode 100644 index 00000000..4356b7e7 Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-attachments-base.pdf differ diff --git a/qpdf/qtest/qpdf/enc-XI-base.pdf b/qpdf/qtest/qpdf/enc-XI-base.pdf new file mode 100644 index 00000000..695c463a Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-base.pdf differ diff --git a/qpdf/qtest/qpdf/enc-XI-long-password.pdf b/qpdf/qtest/qpdf/enc-XI-long-password.pdf new file mode 100644 index 00000000..f4f206ed Binary files /dev/null and b/qpdf/qtest/qpdf/enc-XI-long-password.pdf differ diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index 2ad7655d..45190042 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -112,7 +112,12 @@ void runtest(int n, char const* filename1, char const* arg2) { pdf.setAttemptRecovery(false); } - if (n % 2 == 0) + if ((n == 35) && (arg2 != 0)) + { + // arg2 is password + pdf.processFile(filename1, arg2); + } + else if (n % 2 == 0) { if (n % 4 == 0) { @@ -1150,6 +1155,65 @@ void runtest(int n, char const* filename1, char const* arg2) << "extension level: " << pdf.getExtensionLevel() << std::endl << pdf.getRoot().getKey("/Extensions").unparse() << std::endl; } + else if (n == 35) + { + // Extract attachments + + std::map > attachments; + QPDFObjectHandle root = pdf.getRoot(); + QPDFObjectHandle names = root.getKey("/Names"); + QPDFObjectHandle embeddedFiles = names.getKey("/EmbeddedFiles"); + names = embeddedFiles.getKey("/Names"); + for (int i = 0; i < names.getArrayNItems(); ++i) + { + QPDFObjectHandle item = names.getArrayItem(i); + if (item.isDictionary() && + item.getKey("/Type").isName() && + (item.getKey("/Type").getName() == "/Filespec") && + item.getKey("/EF").isDictionary() && + item.getKey("/EF").getKey("/F").isStream()) + { + std::string filename = item.getKey("/F").getStringValue(); + QPDFObjectHandle stream = item.getKey("/EF").getKey("/F"); + attachments[filename] = stream.getStreamData(); + } + } + for (std::map >::iterator iter = + attachments.begin(); iter != attachments.end(); ++iter) + { + std::string const& filename = (*iter).first; + std::string data = std::string( + (char const*)(*iter).second->getBuffer(), + (*iter).second->getSize()); + bool is_binary = false; + for (size_t i = 0; i < data.size(); ++i) + { + if (data[i] < 0) + { + is_binary = true; + break; + } + } + if (is_binary) + { + std::string t; + for (size_t i = 0; i < std::min(data.size(), (size_t)20); ++i) + { + if ((data[i] >= 32) && (data[i] <= 126)) + { + t += data[i]; + } + else + { + t += "."; + } + } + t += " (" + QUtil::int_to_string(data.size()) + " bytes)"; + data = t; + } + std::cout << filename << ":\n" << data << "--END--\n"; + } + } else { throw std::runtime_error(std::string("invalid test ") +