2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-05-31 17:30:54 +00:00

Add command line option to copy encryption from other file

Add --copy-encryption and --encryption-file-password options to qpdf.
Also strengthen test suite for copying encryption.  The strengthened
test suite would have caught the failure to preserve AES and the
failure to update the file version, which was invalidating the
encrypted data.
This commit is contained in:
Jay Berkenbilt 2012-07-15 21:15:24 -04:00
parent b26ce88ea1
commit a101533e0a
13 changed files with 282 additions and 37 deletions

View File

@ -1,8 +1,18 @@
2012-07-15 Jay Berkenbilt <ejb@ql.org> 2012-07-15 Jay Berkenbilt <ejb@ql.org>
* add new QPDF::isEncrypted method that returns some additional
information beyond other versions.
* libqpdf/QPDFWriter.cc: fix copyEncryptionParameters to fix the
minimum PDF version based on other file's encryption needs. This
is a fix to code added on 2012-07-14 and did not impact previously
released code.
* libqpdf/QPDFWriter.cc (copyEncryptionParameters): Bug fix: qpdf * libqpdf/QPDFWriter.cc (copyEncryptionParameters): Bug fix: qpdf
was not preserving whether or not AES encryption was being used was not preserving whether or not AES encryption was being used
when copying encryption parameters. when copying encryption parameters. The file would still have
been properly encrypted, but a file that started off encrypted
with AES could have become encrypted with RC4.
2012-07-14 Jay Berkenbilt <ejb@ql.org> 2012-07-14 Jay Berkenbilt <ejb@ql.org>

8
TODO
View File

@ -64,9 +64,11 @@ Next
- Tests through qpdf command line: copy pages from multiple PDFs - Tests through qpdf command line: copy pages from multiple PDFs
starting with one PDF and also starting with empty. starting with one PDF and also starting with empty.
* qpdf commandline: provide an option to copy encryption parameters * Document --copy-encryption and --encryption-file-password in
from another file, specifying file and password. Search for "Copy manual. Mention that the first half of /ID as well as all the
encryption parameters" in qpdf.test. encryption parameters are copied. Maybe mention about StrF and
StrM with respect to AES here and also with encryption
preservation.
Soon Soon

View File

@ -248,6 +248,12 @@ class QPDF
QPDF_DLL QPDF_DLL
bool isEncrypted(int& R, int& P); bool isEncrypted(int& R, int& P);
QPDF_DLL
bool isEncrypted(int& R, int& P, int& V,
encryption_method_e& stream_method,
encryption_method_e& string_method,
encryption_method_e& file_method);
// Encryption permissions -- not enforced by QPDF // Encryption permissions -- not enforced by QPDF
QPDF_DLL QPDF_DLL
bool allowAccessibility(); bool allowAccessibility();

View File

@ -743,6 +743,17 @@ QPDF::isEncrypted() const
bool bool
QPDF::isEncrypted(int& R, int& P) QPDF::isEncrypted(int& R, int& P)
{
int V;
encryption_method_e stream, string, file;
return isEncrypted(R, P, V, stream, string, file);
}
bool
QPDF::isEncrypted(int& R, int& P, int& V,
encryption_method_e& stream_method,
encryption_method_e& string_method,
encryption_method_e& file_method)
{ {
if (this->encrypted) if (this->encrypted)
{ {
@ -750,8 +761,13 @@ QPDF::isEncrypted(int& R, int& P)
QPDFObjectHandle encrypt = trailer.getKey("/Encrypt"); QPDFObjectHandle encrypt = trailer.getKey("/Encrypt");
QPDFObjectHandle Pkey = encrypt.getKey("/P"); QPDFObjectHandle Pkey = encrypt.getKey("/P");
QPDFObjectHandle Rkey = encrypt.getKey("/R"); QPDFObjectHandle Rkey = encrypt.getKey("/R");
QPDFObjectHandle Vkey = encrypt.getKey("/V");
P = Pkey.getIntValue(); P = Pkey.getIntValue();
R = Rkey.getIntValue(); R = Rkey.getIntValue();
V = Vkey.getIntValue();
stream_method = this->cf_stream;
string_method = this->cf_stream;
file_method = this->cf_file;
return true; return true;
} }
else else

View File

@ -33,18 +33,29 @@ Usage: qpdf [ options ] infilename [ outfilename ]\n\
\n\ \n\
An option summary appears below. Please see the documentation for details.\n\ An option summary appears below. Please see the documentation for details.\n\
\n\ \n\
Note that when contradictory options are provided, whichever options are\n\
provided last take precedence.\n\
\n\
\n\ \n\
Basic Options\n\ Basic Options\n\
-------------\n\ -------------\n\
\n\ \n\
--password=password specify a password for accessing encrypted files\n\ --password=password specify a password for accessing encrypted files\n\
--linearize generated a linearized (web optimized) file\n\ --linearize generated a linearized (web optimized) file\n\
--copy-encryption=file copy encryption parameters from specified file\n\
--encryption-file-password=password\n\
password used to open the file from which encryption\n\
parameters are being copied\n\
--encrypt options -- generate an encrypted file\n\ --encrypt options -- generate an encrypted file\n\
--decrypt remove any encryption on the file\n\ --decrypt remove any encryption on the file\n\
\n\ \n\
If neither --encrypt or --decrypt are given, qpdf will preserve any\n\ If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n\
encryption data associated with a file.\n\ preserve any encryption data associated with a file.\n\
\n\ \n\
Note that when copying encryption parameters from another file, all\n\
parameters will be copied, including both user and owner passwords, even\n\
if the user password is used to open the other file. This works even if\n\
the owner password is not known.\n\
\n\ \n\
Encryption Options\n\ Encryption Options\n\
------------------\n\ ------------------\n\
@ -192,12 +203,39 @@ static std::string show_bool(bool v)
return v ? "allowed" : "not allowed"; return v ? "allowed" : "not allowed";
} }
static std::string show_encryption_method(QPDF::encryption_method_e method)
{
std::string result = "unknown";
switch (method)
{
case QPDF::e_none:
result = "none";
break;
case QPDF::e_unknown:
result = "unknown";
break;
case QPDF::e_rc4:
result = "RC4";
break;
case QPDF::e_aes:
result = "AESv2";
break;
// no default so gcc will warn for missing case
}
return result;
}
static void show_encryption(QPDF& pdf) static void show_encryption(QPDF& pdf)
{ {
// Extract /P from /Encrypt // Extract /P from /Encrypt
int R = 0; int R = 0;
int P = 0; int P = 0;
if (! pdf.isEncrypted(R, P)) int V = 0;
QPDF::encryption_method_e stream_method = QPDF::e_unknown;
QPDF::encryption_method_e string_method = QPDF::e_unknown;
QPDF::encryption_method_e file_method = QPDF::e_unknown;
if (! pdf.isEncrypted(R, P, V,
stream_method, string_method, file_method))
{ {
std::cout << "File is not encrypted" << std::endl; std::cout << "File is not encrypted" << std::endl;
} }
@ -206,25 +244,34 @@ static void show_encryption(QPDF& pdf)
std::cout << "R = " << R << std::endl; std::cout << "R = " << R << std::endl;
std::cout << "P = " << P << std::endl; std::cout << "P = " << P << std::endl;
std::string user_password = pdf.getTrimmedUserPassword(); std::string user_password = pdf.getTrimmedUserPassword();
std::cout << "User password = " << user_password << std::endl; std::cout << "User password = " << user_password << std::endl
std::cout << "extract for accessibility: " << "extract for accessibility: "
<< show_bool(pdf.allowAccessibility()) << std::endl; << show_bool(pdf.allowAccessibility()) << std::endl
std::cout << "extract for any purpose: " << "extract for any purpose: "
<< show_bool(pdf.allowExtractAll()) << std::endl; << show_bool(pdf.allowExtractAll()) << std::endl
std::cout << "print low resolution: " << "print low resolution: "
<< show_bool(pdf.allowPrintLowRes()) << std::endl; << show_bool(pdf.allowPrintLowRes()) << std::endl
std::cout << "print high resolution: " << "print high resolution: "
<< show_bool(pdf.allowPrintHighRes()) << std::endl; << show_bool(pdf.allowPrintHighRes()) << std::endl
std::cout << "modify document assembly: " << "modify document assembly: "
<< show_bool(pdf.allowModifyAssembly()) << std::endl; << show_bool(pdf.allowModifyAssembly()) << std::endl
std::cout << "modify forms: " << "modify forms: "
<< show_bool(pdf.allowModifyForm()) << std::endl; << show_bool(pdf.allowModifyForm()) << std::endl
std::cout << "modify annotations: " << "modify annotations: "
<< show_bool(pdf.allowModifyAnnotation()) << std::endl; << show_bool(pdf.allowModifyAnnotation()) << std::endl
std::cout << "modify other: " << "modify other: "
<< show_bool(pdf.allowModifyOther()) << std::endl; << show_bool(pdf.allowModifyOther()) << std::endl
std::cout << "modify anything: " << "modify anything: "
<< show_bool(pdf.allowModifyAll()) << std::endl; << show_bool(pdf.allowModifyAll()) << std::endl;
if (V >= 4)
{
std::cout << "stream encryption method: "
<< show_encryption_method(stream_method) << std::endl
<< "string encryption method: "
<< show_encryption_method(string_method) << std::endl
<< "file encryption method: "
<< show_encryption_method(file_method) << std::endl;
}
} }
} }
@ -579,6 +626,10 @@ int main(int argc, char* argv[])
bool linearize = false; bool linearize = false;
bool decrypt = false; bool decrypt = false;
bool copy_encryption = false;
char const* encryption_file = 0;
char const* encryption_file_password = "";
bool encrypt = false; bool encrypt = false;
std::string user_password; std::string user_password;
std::string owner_password; std::string owner_password;
@ -664,11 +715,36 @@ int main(int argc, char* argv[])
r3_accessibility, r3_extract, r3_print, r3_modify, r3_accessibility, r3_extract, r3_print, r3_modify,
force_V4, cleartext_metadata, use_aes); force_V4, cleartext_metadata, use_aes);
encrypt = true; encrypt = true;
decrypt = false;
copy_encryption = false;
} }
else if (strcmp(arg, "decrypt") == 0) else if (strcmp(arg, "decrypt") == 0)
{ {
decrypt = true; decrypt = true;
encrypt = false;
copy_encryption = false;
} }
else if (strcmp(arg, "copy-encryption") == 0)
{
if (parameter == 0)
{
usage("--copy-encryption must be given as"
"--copy_encryption=file");
}
encryption_file = parameter;
copy_encryption = true;
encrypt = false;
decrypt = false;
}
else if (strcmp(arg, "encryption-file-password") == 0)
{
if (parameter == 0)
{
usage("--encryption-file-password must be given as"
"--encryption-file-password=password");
}
encryption_file_password = parameter;
}
else if (strcmp(arg, "stream-data") == 0) else if (strcmp(arg, "stream-data") == 0)
{ {
if (parameter == 0) if (parameter == 0)
@ -865,6 +941,7 @@ int main(int argc, char* argv[])
try try
{ {
QPDF pdf; QPDF pdf;
QPDF encryption_pdf;
if (ignore_xref_streams) if (ignore_xref_streams)
{ {
pdf.setIgnoreXRefStreams(true); pdf.setIgnoreXRefStreams(true);
@ -1082,6 +1159,12 @@ int main(int argc, char* argv[])
{ {
w.setSuppressOriginalObjectIDs(true); w.setSuppressOriginalObjectIDs(true);
} }
if (copy_encryption)
{
encryption_pdf.processFile(
encryption_file, encryption_file_password);
w.copyEncryptionParameters(encryption_pdf);
}
if (encrypt) if (encrypt)
{ {
if (keylen == 40) if (keylen == 40)

View File

@ -1271,7 +1271,7 @@ $td->runtest("linearize and encrypt file",
$td->EXIT_STATUS => 0}); $td->EXIT_STATUS => 0});
$td->runtest("check encryption", $td->runtest("check encryption",
{$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf", {$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf",
$td->FILTER => "grep -v allowed"}, $td->FILTER => "grep -v allowed | grep -v method"},
{$td->STRING => "R = 4\nP = -4\nUser password = user\n", {$td->STRING => "R = 4\nP = -4\nUser password = user\n",
$td->EXIT_STATUS => 0}, $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
@ -1290,7 +1290,7 @@ $td->runtest("encrypt with AES",
{$td->STRING => "", $td->EXIT_STATUS => 0}); {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption", $td->runtest("check encryption",
{$td->COMMAND => "qpdf --show-encryption a.pdf", {$td->COMMAND => "qpdf --show-encryption a.pdf",
$td->FILTER => "grep -v allowed"}, $td->FILTER => "grep -v allowed | grep -v method"},
{$td->STRING => "R = 4\nP = -4\nUser password = \n", {$td->STRING => "R = 4\nP = -4\nUser password = \n",
$td->EXIT_STATUS => 0}, $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
@ -1311,7 +1311,7 @@ $td->runtest("linearize with AES and object streams",
{$td->STRING => "", $td->EXIT_STATUS => 0}); {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption", $td->runtest("check encryption",
{$td->COMMAND => "qpdf --show-encryption a.pdf", {$td->COMMAND => "qpdf --show-encryption a.pdf",
$td->FILTER => "grep -v allowed"}, $td->FILTER => "grep -v allowed | grep -v method"},
{$td->STRING => "R = 4\nP = -4\nUser password = \n", {$td->STRING => "R = 4\nP = -4\nUser password = \n",
$td->EXIT_STATUS => 0}, $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
@ -1345,7 +1345,7 @@ $td->runtest("make sure there is no xref stream",
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
# Look at some actual V4 files # Look at some actual V4 files
$n_tests += 10; $n_tests += 14;
foreach my $d (['--force-V4', 'V4'], foreach my $d (['--force-V4', 'V4'],
['--cleartext-metadata', 'V4-clearmeta'], ['--cleartext-metadata', 'V4-clearmeta'],
['--use-aes=y', 'V4-aes'], ['--use-aes=y', 'V4-aes'],
@ -1359,6 +1359,10 @@ foreach my $d (['--force-V4', 'V4'],
$td->runtest("check output", $td->runtest("check output",
{$td->FILE => "a.pdf"}, {$td->FILE => "a.pdf"},
{$td->FILE => "$out.pdf"}); {$td->FILE => "$out.pdf"});
$td->runtest("show encryption",
{$td->COMMAND => "qpdf --show-encryption a.pdf"},
{$td->FILE => "$out-encryption.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
} }
# Crypt Filter # Crypt Filter
$td->runtest("decrypt with crypt filter", $td->runtest("decrypt with crypt filter",
@ -1370,7 +1374,11 @@ $td->runtest("check output",
{$td->FILE => 'decrypted-crypt-filter.pdf'}); {$td->FILE => 'decrypted-crypt-filter.pdf'});
# Copy encryption parameters # Copy encryption parameters
$n_tests += 3; $n_tests += 10;
$td->runtest("create reference qdf",
{$td->COMMAND =>
"qpdf --qdf --no-original-object-ids minimal.pdf a.qdf"},
{$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("create encrypted file", $td->runtest("create encrypted file",
{$td->COMMAND => {$td->COMMAND =>
"qpdf --encrypt user owner 128 --use-aes=y --extract=n --" . "qpdf --encrypt user owner 128 --use-aes=y --extract=n --" .
@ -1380,11 +1388,42 @@ $td->runtest("copy encryption parameters",
{$td->COMMAND => "test_driver 30 minimal.pdf a.pdf"}, {$td->COMMAND => "test_driver 30 minimal.pdf a.pdf"},
{$td->STRING => "test 30 done\n", $td->EXIT_STATUS => 0}, {$td->STRING => "test 30 done\n", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
$td->runtest("checkout encryption", $td->runtest("check output encryption",
{$td->COMMAND => "qpdf --show-encryption b.pdf --password=owner"}, {$td->COMMAND => "qpdf --show-encryption b.pdf --password=owner"},
{$td->FILE => "copied-encryption.out", {$td->FILE => "copied-encryption.out",
$td->EXIT_STATUS => 0}, $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
$td->runtest("convert to qdf",
{$td->COMMAND =>
"qpdf --qdf b.pdf b.qdf" .
" --password=owner --no-original-object-ids"},
{$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare qdf",
{$td->COMMAND => "./diff-ignore-ID-version a.qdf b.qdf"},
{$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("copy encryption with qpdf",
{$td->COMMAND =>
"qpdf --copy-encryption=a.pdf".
" --encryption-file-password=user" .
" minimal.pdf c.pdf"},
{$td->STRING => "", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("check output encryption",
{$td->COMMAND => "qpdf --show-encryption c.pdf --password=owner"},
{$td->FILE => "copied-encryption.out",
$td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("convert to qdf",
{$td->COMMAND =>
"qpdf --qdf c.pdf c.qdf" .
" --password=owner --no-original-object-ids"},
{$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare qdf",
{$td->COMMAND => "./diff-ignore-ID-version a.qdf c.qdf"},
{$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
show_ntests(); show_ntests();
# ---------- # ----------
@ -1753,6 +1792,5 @@ sub get_md5_checksum
sub cleanup sub cleanup
{ {
system("rm -rf *.ps *.pnm a.pdf a.qdf b.pdf b.qdf c.pdf" . system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache");
" *.enc* tif1 tif2 tiff-cache");
} }

View File

@ -0,0 +1,15 @@
R = 4
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: AESv2
string encryption method: AESv2
file encryption method: AESv2

View File

@ -0,0 +1,15 @@
R = 4
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: AESv2
string encryption method: AESv2
file encryption method: AESv2

View File

@ -0,0 +1,15 @@
R = 4
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: RC4
string encryption method: RC4
file encryption method: RC4

View File

@ -0,0 +1,15 @@
R = 4
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: RC4
string encryption method: RC4
file encryption method: RC4

View File

@ -10,3 +10,6 @@ modify forms: allowed
modify annotations: allowed modify annotations: allowed
modify other: allowed modify other: allowed
modify anything: allowed modify anything: allowed
stream encryption method: AESv2
string encryption method: AESv2
file encryption method: AESv2

View File

@ -0,0 +1,8 @@
#!/bin/sh
lines=$(expr + $(diff $1 $2 | egrep '^[<>]' | \
egrep -v '/ID' | egrep -v '%PDF-' | wc -l))
if [ "$lines" = "0" ]; then
echo okay
else
diff -a -U 0 $1 $2
fi

View File

@ -58,12 +58,17 @@ class Provider: public QPDFObjectHandle::StreamDataProvider
bool bad_length; bool bad_length;
}; };
static void checkPageContents(QPDFObjectHandle page, static std::string getPageContents(QPDFObjectHandle page)
std::string const& wanted_string)
{ {
PointerHolder<Buffer> b1 = PointerHolder<Buffer> b1 =
page.getKey("/Contents").getStreamData(); page.getKey("/Contents").getStreamData();
std::string contents = std::string((char *)(b1->getBuffer())); return std::string((char *)(b1->getBuffer()), b1->getSize()) + "\0";
}
static void checkPageContents(QPDFObjectHandle page,
std::string const& wanted_string)
{
std::string contents = getPageContents(page);
if (contents.find(wanted_string) == std::string::npos) if (contents.find(wanted_string) == std::string::npos)
{ {
std::cout << "didn't find " << wanted_string << " in " std::cout << "didn't find " << wanted_string << " in "
@ -1030,10 +1035,24 @@ void runtest(int n, char const* filename1, char const* filename2)
QPDF encrypted; QPDF encrypted;
encrypted.processFile(filename2, "user"); encrypted.processFile(filename2, "user");
QPDFWriter w(pdf, "b.pdf"); QPDFWriter w(pdf, "b.pdf");
w.setStaticID(true);
w.setStreamDataMode(qpdf_s_preserve); w.setStreamDataMode(qpdf_s_preserve);
w.copyEncryptionParameters(encrypted); w.copyEncryptionParameters(encrypted);
w.write(); w.write();
// Make sure the contents are actually the same
QPDF final;
final.processFile("b.pdf", "user");
std::vector<QPDFObjectHandle> pages = pdf.getAllPages();
std::string orig_contents = getPageContents(pages[0]);
pages = final.getAllPages();
std::string new_contents = getPageContents(pages[0]);
if (orig_contents != new_contents)
{
std::cout << "oops -- page contents don't match" << std::endl
<< "original:\n" << orig_contents
<< "new:\n" << new_contents
<< std::endl;
}
} }
else else
{ {