Support for encryption with /V=5 and /R=5 and /R=6

Read and write support is implemented for /V=5 with /R=5 as well as
/R=6.  /R=5 is the deprecated encryption method used by Acrobat IX.
/R=6 is the encryption method used by PDF 2.0 from ISO 32000-2.
This commit is contained in:
Jay Berkenbilt 2012-12-29 21:31:21 -05:00
parent 93ac1695a4
commit e57c25814e
21 changed files with 937 additions and 115 deletions

26
TODO
View File

@ -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

View File

@ -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<std::string, encryption_method_e> crypt_filters;
encryption_method_e cf_stream;

View File

@ -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<std::string, std::string> encryption_dictionary;
int encryption_V;
int encryption_R;
std::string id1; // for /ID key of
std::string id2; // trailer dictionary

View File

@ -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),

View File

@ -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<int> 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<int> 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<int>& 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<int>::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

View File

@ -10,6 +10,7 @@
#include <qpdf/Pl_RC4.hh>
#include <qpdf/Pl_AES_PDF.hh>
#include <qpdf/Pl_Buffer.hh>
#include <qpdf/Pl_SHA2.hh>
#include <qpdf/RC4.hh>
#include <qpdf/MD5.hh>
@ -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<Buffer> 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<std::string> 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
{

View File

@ -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");

View File

@ -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

View File

@ -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();
# ----------

20
qpdf/qtest/qpdf/V5R5.out Normal file
View File

@ -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

20
qpdf/qtest/qpdf/V5R6.out Normal file
View File

@ -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

View File

@ -0,0 +1,6 @@
attachment1.txt:
This is the first attachment.
--END--
attachment2.png:
.PNG........IHDR...1 (2620 bytes)--END--
test 35 done

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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<std::string, PointerHolder<Buffer> > 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<std::string, PointerHolder<Buffer> >::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 ") +