// This file implements methods from the QPDF class that involve // encryption. #include #include #include #include #include #include #include #include #include #include #include static unsigned char const padding_string[] = { 0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08, 0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 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; void pad_or_truncate_password(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; memcpy(k1, password.c_str(), password_bytes); memcpy(k1 + password_bytes, padding_string, pad_bytes); } void QPDF::trim_user_password(std::string& user_password) { // Although unnecessary, this routine trims the padding string // from the end of a user password. Its only purpose is for // recovery of user passwords which is done in the test suite. char const* cstr = user_password.c_str(); size_t len = user_password.length(); if (len < key_bytes) { return; } char const* p = 0; while ((p = strchr(cstr, '\x28')) != 0) { if (memcmp(p, padding_string, len - (p - cstr)) == 0) { user_password = user_password.substr(0, p - cstr); return; } } } static std::string pad_or_truncate_password(std::string const& password) { char k1[key_bytes]; pad_or_truncate_password(password, k1); return std::string(k1, key_bytes); } static void iterate_md5_digest(MD5& md5, MD5::Digest& digest, int iterations) { md5.digest(digest); for (int i = 0; i < iterations; ++i) { MD5 m; m.encodeDataIncrementally((char*)digest, sizeof(digest)); m.digest(digest); } } static void iterate_rc4(unsigned char* data, int data_len, unsigned char* okey, int key_len, int iterations, bool reverse) { unsigned char* key = new unsigned char[key_len]; for (int i = 0; i < iterations; ++i) { int const xor_value = (reverse ? iterations - 1 - i : i); for (int j = 0; j < key_len; ++j) { key[j] = okey[j] ^ xor_value; } RC4 rc4(key, key_len); rc4.process(data, data_len); } delete [] key; } std::string QPDF::compute_data_key(std::string const& encryption_key, int objid, int generation, bool use_aes) { // Algorithm 3.1 from the PDF 1.7 Reference Manual std::string result = encryption_key; // Append low three bytes of object ID and low two bytes of generation result += (char) (objid & 0xff); result += (char) ((objid >> 8) & 0xff); result += (char) ((objid >> 16) & 0xff); result += (char) (generation & 0xff); result += (char) ((generation >> 8) & 0xff); if (use_aes) { result += "sAlT"; } MD5 md5; md5.encodeDataIncrementally(result.c_str(), (int)result.length()); MD5::Digest digest; md5.digest(digest); return std::string((char*) digest, std::min(result.length(), (size_t) 16)); } std::string QPDF::compute_encryption_key( std::string const& password, EncryptionData const& data) { // Algorithm 3.2 from the PDF 1.7 Reference Manual MD5 md5; md5.encodeDataIncrementally( pad_or_truncate_password(password).c_str(), key_bytes); md5.encodeDataIncrementally(data.O.c_str(), key_bytes); char pbytes[4]; pbytes[0] = (char) (data.P & 0xff); pbytes[1] = (char) ((data.P >> 8) & 0xff); pbytes[2] = (char) ((data.P >> 16) & 0xff); pbytes[3] = (char) ((data.P >> 24) & 0xff); md5.encodeDataIncrementally(pbytes, 4); md5.encodeDataIncrementally(data.id1.c_str(), (int)data.id1.length()); if ((data.R >= 4) && (! data.encrypt_metadata)) { char bytes[4]; memset(bytes, 0xff, 4); md5.encodeDataIncrementally(bytes, 4); } MD5::Digest digest; iterate_md5_digest(md5, digest, ((data.R >= 3) ? 50 : 0)); return std::string((char*)digest, data.Length_bytes); } 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]) { std::string password = owner_password; if (password.empty()) { password = user_password; } MD5 md5; md5.encodeDataIncrementally( pad_or_truncate_password(password).c_str(), key_bytes); MD5::Digest digest; iterate_md5_digest(md5, digest, ((data.R >= 3) ? 50 : 0)); memcpy(key, digest, O_key_bytes); } static std::string compute_O_value(std::string const& user_password, std::string const& owner_password, QPDF::EncryptionData const& data) { // Algorithm 3.3 from the PDF 1.7 Reference Manual unsigned char O_key[O_key_bytes]; compute_O_rc4_key(user_password, owner_password, data, O_key); char upass[key_bytes]; pad_or_truncate_password(user_password, upass); iterate_rc4((unsigned char*) upass, key_bytes, O_key, data.Length_bytes, (data.R >= 3) ? 20 : 1, false); return std::string(upass, key_bytes); } static std::string compute_U_value_R2(std::string const& user_password, QPDF::EncryptionData const& data) { // Algorithm 3.4 from the PDF 1.7 Reference Manual std::string k1 = QPDF::compute_encryption_key(user_password, data); char udata[key_bytes]; pad_or_truncate_password("", udata); iterate_rc4((unsigned char*) udata, key_bytes, (unsigned char*)k1.c_str(), data.Length_bytes, 1, false); return std::string(udata, key_bytes); } static std::string compute_U_value_R3(std::string const& user_password, QPDF::EncryptionData const& data) { // Algorithm 3.5 from the PDF 1.7 Reference Manual std::string k1 = QPDF::compute_encryption_key(user_password, data); MD5 md5; md5.encodeDataIncrementally( pad_or_truncate_password("").c_str(), key_bytes); md5.encodeDataIncrementally(data.id1.c_str(), (int)data.id1.length()); MD5::Digest digest; md5.digest(digest); iterate_rc4(digest, sizeof(MD5::Digest), (unsigned char*) k1.c_str(), data.Length_bytes, 20, false); char result[key_bytes]; memcpy(result, digest, sizeof(MD5::Digest)); // pad with arbitrary data -- make it consistent for the sake of // testing for (unsigned int i = sizeof(MD5::Digest); i < key_bytes; ++i) { result[i] = (char)((i * i) % 0xff); } return std::string(result, key_bytes); } static std::string compute_U_value(std::string const& user_password, QPDF::EncryptionData const& data) { if (data.R >= 3) { return compute_U_value_R3(user_password, data); } return compute_U_value_R2(user_password, data); } static bool check_user_password(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.R >= 3) ? sizeof(MD5::Digest) : key_bytes); return (memcmp(data.U.c_str(), u_value.c_str(), to_compare) == 0); } 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.O.c_str(), key_bytes); iterate_rc4(O_data, key_bytes, key, data.Length_bytes, (data.R >= 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; } 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 if (filter == "/Identity") { return e_none; } else { return e_unknown; } } else { // Default: /Identity return e_none; } } void QPDF::initializeEncryption() { if (this->encryption_initialized) { return; } this->encryption_initialized = true; // After we initialize encryption parameters, we must used stored // key information and never look at /Encrypt again. Otherwise, // things could go wrong if someone mutates the encryption // dictionary. if (! this->trailer.hasKey("/Encrypt")) { return; } // Go ahead and set this->encryption here. That way, isEncrypted // will return true even if there were errors reading the // encryption dictionary. this->encrypted = true; QPDFObjectHandle id_obj = this->trailer.getKey("/ID"); if (! (id_obj.isArray() && (id_obj.getArrayNItems() == 2) && id_obj.getArrayItem(0).isString())) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), "trailer", this->file->getLastOffset(), "invalid /ID in trailer dictionary"); } std::string id1 = id_obj.getArrayItem(0).getStringValue(); QPDFObjectHandle encryption_dict = this->trailer.getKey("/Encrypt"); if (! encryption_dict.isDictionary()) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), this->last_object_description, this->file->getLastOffset(), "/Encrypt in trailer dictionary is not a dictionary"); } if (! (encryption_dict.getKey("/Filter").isName() && (encryption_dict.getKey("/Filter").getName() == "/Standard"))) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), "unsupported encryption filter"); } if (! encryption_dict.getKey("/SubFilter").isNull()) { warn(QPDFExc(qpdf_e_unsupported, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), "file uses encryption SubFilters," " which qpdf does not support")); } if (! (encryption_dict.getKey("/V").isInteger() && encryption_dict.getKey("/R").isInteger() && encryption_dict.getKey("/O").isString() && encryption_dict.getKey("/U").isString() && encryption_dict.getKey("/P").isInteger())) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), "some encryption dictionary parameters are missing " "or the wrong type"); } int V = encryption_dict.getKey("/V").getIntValue(); int R = encryption_dict.getKey("/R").getIntValue(); std::string O = encryption_dict.getKey("/O").getStringValue(); 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)))) { throw QPDFExc(qpdf_e_unsupported, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), "Unsupported /R or /V in encryption dictionary"); } this->encryption_V = V; 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 /P 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)) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), "invalid /Length value in encryption dictionary"); } } this->encrypt_metadata = true; if ((V >= 4) && (encryption_dict.getKey("/EncryptMetadata").isBool())) { this->encrypt_metadata = encryption_dict.getKey("/EncryptMetadata").getBoolValue(); } if (V == 4) { 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") { QTC::TC("qpdf", "QPDF_encryption CFM V2"); method = e_rc4; } else if (method_name == "/AESV2") { QTC::TC("qpdf", "QPDF_encryption CFM AESV2"); 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; } if (this->cf_file != this->cf_stream) { // The issue for qpdf is that it can't tell the difference // between an embedded file stream and a regular stream. // Search for a comment containing cf_file. To fix this, // we need files with encrypted embedded files and // non-encrypted native streams and vice versa. Also if // it is possible for them to be encrypted in different // ways, we should have some of those too. In cases where // we can detect whether a stream is encrypted or not, we // might want to try to detecet that automatically in // defense of possible logic errors surrounding detection // of embedded file streams, unless that's really clear // from the specification. throw QPDFExc(qpdf_e_unsupported, this->file->getName(), "encryption dictionary", this->file->getLastOffset(), "This document has embedded files that are" " encrypted differently from the rest of the file." " qpdf does not presently support this due to" " lack of test data; if possible, please submit" " a bug report that includes this file."); } } EncryptionData data(V, R, Length / 8, P, O, U, 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 } else if (check_user_password(this->provided_password, data)) { this->user_password = this->provided_password; } else { throw QPDFExc(qpdf_e_password, this->file->getName(), "", 0, "invalid password"); } this->encryption_key = compute_encryption_key(this->user_password, data); } std::string QPDF::getKeyForObject(int objid, int generation, bool use_aes) { if (! this->encrypted) { throw std::logic_error( "request for encryption key in non-encrypted PDF"); } if (! ((objid == this->cached_key_objid) && (generation == this->cached_key_generation))) { this->cached_object_encryption_key = compute_data_key(this->encryption_key, objid, generation, use_aes); this->cached_key_objid = objid; this->cached_key_generation = generation; } return this->cached_object_encryption_key; } void QPDF::decryptString(std::string& str, int objid, int generation) { if (objid == 0) { return; } 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(qpdf_e_damaged_pdf, this->file->getName(), this->last_object_description, 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); try { if (use_aes) { QTC::TC("qpdf", "QPDF_encryption aes decode string"); 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(); PointerHolder buf = bufpl.getBuffer(); str = std::string((char*)buf->getBuffer(), buf->getSize()); } else { QTC::TC("qpdf", "QPDF_encryption rc4 decode string"); unsigned int vlen = (int)str.length(); // Using PointerHolder guarantees that tmp will // be freed even if rc4.process throws an exception. PointerHolder tmp(true, QUtil::copy_string(str)); RC4 rc4((unsigned char const*)key.c_str(), (int)key.length()); rc4.process((unsigned char*)tmp.getPointer(), vlen); str = std::string(tmp.getPointer(), vlen); } } catch (QPDFExc& e) { throw; } catch (std::runtime_error& e) { throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), this->last_object_description, this->file->getLastOffset(), "error decrypting string for object " + QUtil::int_to_string(objid) + " " + QUtil::int_to_string(generation) + ": " + e.what()); } } void QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, QPDFObjectHandle& stream_dict, std::vector >& heap) { std::string type; if (stream_dict.getKey("/Type").isName()) { type = stream_dict.getKey("/Type").getName(); } if (type == "/XRef") { QTC::TC("qpdf", "QPDF_encryption xref stream from encrypted file"); return; } bool use_aes = false; if (this->encryption_V == 4) { encryption_method_e method = e_unknown; std::string method_source = "/StmF from /Encrypt dictionary"; if (stream_dict.getKey("/Filter").isOrHasName("/Crypt") && stream_dict.getKey("/DecodeParms").isDictionary()) { QPDFObjectHandle decode_parms = stream_dict.getKey("/DecodeParms"); if (decode_parms.getKey("/Type").isName() && (decode_parms.getKey("/Type").getName() == "/CryptFilterDecodeParms")) { QTC::TC("qpdf", "QPDF_encryption stream crypt filter"); method = interpretCF(decode_parms.getKey("/Name")); method_source = "stream's Crypt decode parameters"; } } if (method == e_unknown) { if ((! this->encrypt_metadata) && (type == "/Metadata")) { QTC::TC("qpdf", "QPDF_encryption cleartext metadata"); method = e_none; } else { // NOTE: We should should use cf_file if this is an // embedded file, but we can't yet detect embedded // file streams as such. When fixing, search for all // occurrences of cf_file to find a reference to this // comment. method = this->cf_stream; } } use_aes = false; switch (method) { case e_none: return; break; case e_aes: use_aes = true; break; case e_rc4: break; default: // filter local to this stream. warn(QPDFExc(qpdf_e_damaged_pdf, this->file->getName(), this->last_object_description, 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) { QTC::TC("qpdf", "QPDF_encryption aes decode stream"); 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(), (int)key.length()); } heap.push_back(pipeline); } void QPDF::compute_encryption_O_U( 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& O, std::string& U) { EncryptionData data(V, R, key_len, P, "", "", id1, encrypt_metadata); data.O = compute_O_value(user_password, owner_password, data); O = data.O; U = compute_U_value(user_password, data); } std::string const& QPDF::getPaddedUserPassword() const { return this->user_password; } std::string QPDF::getTrimmedUserPassword() const { std::string result = this->user_password; trim_user_password(result); return result; } bool QPDF::isEncrypted() const { return this->encrypted; } bool QPDF::isEncrypted(int& R, int& P) { if (this->encrypted) { QPDFObjectHandle trailer = getTrailer(); QPDFObjectHandle encrypt = trailer.getKey("/Encrypt"); QPDFObjectHandle Pkey = encrypt.getKey("/P"); QPDFObjectHandle Rkey = encrypt.getKey("/R"); P = Pkey.getIntValue(); R = Rkey.getIntValue(); return true; } else { return false; } } static bool is_bit_set(int P, int bit) { // Bits in P are numbered from 1 in the spec return (P & (1 << (bit - 1))); } bool QPDF::allowAccessibility() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { if (R < 3) { status = is_bit_set(P, 5); } else { status = is_bit_set(P, 10); } } return status; } bool QPDF::allowExtractAll() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { status = is_bit_set(P, 5); } return status; } bool QPDF::allowPrintLowRes() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { status = is_bit_set(P, 3); } return status; } bool QPDF::allowPrintHighRes() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { status = is_bit_set(P, 3); if ((R >= 3) && (! is_bit_set(P, 12))) { status = false; } } return status; } bool QPDF::allowModifyAssembly() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { if (R < 3) { status = is_bit_set(P, 4); } else { status = is_bit_set(P, 11); } } return status; } bool QPDF::allowModifyForm() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { if (R < 3) { status = is_bit_set(P, 6); } else { status = is_bit_set(P, 9); } } return status; } bool QPDF::allowModifyAnnotation() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { status = is_bit_set(P, 6); } return status; } bool QPDF::allowModifyOther() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { status = is_bit_set(P, 4); } return status; } bool QPDF::allowModifyAll() { int R = 0; int P = 0; bool status = true; if (isEncrypted(R, P)) { status = (is_bit_set(P, 4) && is_bit_set(P, 6)); if (R >= 3) { status = status && (is_bit_set(P, 9) && is_bit_set(P, 11)); } } return status; }