Commit 21b0f4acfc0d6827f3d2d9a85873b7b649dc96f0
1 parent
faa2e3dd
Require --allow-insecure to create certain encrypted files (fixes #501)
For now, --allow-insecure allows creation of files with the owner passwords empty or matching the user password.
Showing
5 changed files
with
137 additions
and
11 deletions
ChangeLog
| 1 | +2021-02-04 Jay Berkenbilt <ejb@ql.org> | ||
| 2 | + | ||
| 3 | + * By default, give an error if a user attempts to encrypt a file | ||
| 4 | + with an empty owner password or an owner password that is the same | ||
| 5 | + as the user password. Such files are insecure. Most viewers either | ||
| 6 | + won't open such files or will not enforce security settings. To | ||
| 7 | + allow explicit creation of files like this, pass the new | ||
| 8 | + --allow-insecure option. Fixes #501. | ||
| 9 | + | ||
| 1 | 2021-02-02 Jay Berkenbilt <ejb@ql.org> | 10 | 2021-02-02 Jay Berkenbilt <ejb@ql.org> |
| 2 | 11 | ||
| 3 | * Bug fix: if a form XObject lacks a resources dictionary, | 12 | * Bug fix: if a form XObject lacks a resources dictionary, |
manual/qpdf-manual.xml
| @@ -1214,7 +1214,11 @@ make | @@ -1214,7 +1214,11 @@ make | ||
| 1214 | </para> | 1214 | </para> |
| 1215 | <para> | 1215 | <para> |
| 1216 | Either or both of the user password and the owner password may be | 1216 | Either or both of the user password and the owner password may be |
| 1217 | - empty strings. | 1217 | + empty strings. Starting in qpdf 10.2, qpdf defaults to not |
| 1218 | + allowing creation of PDF files with an empty owner password or an | ||
| 1219 | + owner password that matches the user password. If you want to | ||
| 1220 | + create such files, specify the encryption option | ||
| 1221 | + <option>--allow-insecure</option>, as described below. | ||
| 1218 | </para> | 1222 | </para> |
| 1219 | <para> | 1223 | <para> |
| 1220 | The value for | 1224 | The value for |
| @@ -1224,6 +1228,25 @@ make | @@ -1224,6 +1228,25 @@ make | ||
| 1224 | fully permissive. | 1228 | fully permissive. |
| 1225 | </para> | 1229 | </para> |
| 1226 | <para> | 1230 | <para> |
| 1231 | + For all key lengths, the following options are available: | ||
| 1232 | + <variablelist> | ||
| 1233 | + <varlistentry> | ||
| 1234 | + <term><option>--allow-insecure</option></term> | ||
| 1235 | + <listitem> | ||
| 1236 | + <para> | ||
| 1237 | + From qpdf 10.2, qpdf defaults to not allowing creation of PDF | ||
| 1238 | + files where the owner password is blank or matches the user | ||
| 1239 | + password. Files created in this way are insecure and can't be | ||
| 1240 | + opened by some viewers. Users would ordinarily never want to | ||
| 1241 | + create such files. If you are using qpdf to intentionally | ||
| 1242 | + created strange files for testing (a definite valid use of | ||
| 1243 | + qpdf!), this option allows you to create such insecure files. | ||
| 1244 | + </para> | ||
| 1245 | + </listitem> | ||
| 1246 | + </varlistentry> | ||
| 1247 | + </variablelist> | ||
| 1248 | + </para> | ||
| 1249 | + <para> | ||
| 1227 | If <option><replaceable>key-length</replaceable></option> is 40, | 1250 | If <option><replaceable>key-length</replaceable></option> is 40, |
| 1228 | the following restriction options are available: | 1251 | the following restriction options are available: |
| 1229 | <variablelist> | 1252 | <variablelist> |
| @@ -4824,7 +4847,28 @@ print "\n"; | @@ -4824,7 +4847,28 @@ print "\n"; | ||
| 4824 | <itemizedlist> | 4847 | <itemizedlist> |
| 4825 | <listitem> | 4848 | <listitem> |
| 4826 | <para> | 4849 | <para> |
| 4827 | - Behavior Changes | 4850 | + CLI Behavior Changes |
| 4851 | + </para> | ||
| 4852 | + <itemizedlist> | ||
| 4853 | + <listitem> | ||
| 4854 | + <para> | ||
| 4855 | + By default, <command>qpdf</command> no longer allows | ||
| 4856 | + creation of encrypted PDF files whose owner password is | ||
| 4857 | + empty or matches the user password. The | ||
| 4858 | + <option>--allow-insecure</option>, specified inside the | ||
| 4859 | + <option>--encrypt</option> options, allows creation of such | ||
| 4860 | + files. Behavior changes in the CLI are avoided when | ||
| 4861 | + possible, but an exception was made here because this is | ||
| 4862 | + security-related. qpdf must always allow creation of weird | ||
| 4863 | + files for testing purposes, but it should not default to | ||
| 4864 | + letting users unknowingly create insecure files. | ||
| 4865 | + </para> | ||
| 4866 | + </listitem> | ||
| 4867 | + </itemizedlist> | ||
| 4868 | + </listitem> | ||
| 4869 | + <listitem> | ||
| 4870 | + <para> | ||
| 4871 | + Library Behavior Changes | ||
| 4828 | </para> | 4872 | </para> |
| 4829 | <itemizedlist> | 4873 | <itemizedlist> |
| 4830 | <listitem> | 4874 | <listitem> |
qpdf/qpdf.cc
| @@ -113,6 +113,7 @@ struct Options | @@ -113,6 +113,7 @@ struct Options | ||
| 113 | password_is_hex_key(false), | 113 | password_is_hex_key(false), |
| 114 | suppress_password_recovery(false), | 114 | suppress_password_recovery(false), |
| 115 | password_mode(pm_auto), | 115 | password_mode(pm_auto), |
| 116 | + allow_insecure(false), | ||
| 116 | keylen(0), | 117 | keylen(0), |
| 117 | r2_print(true), | 118 | r2_print(true), |
| 118 | r2_modify(true), | 119 | r2_modify(true), |
| @@ -211,6 +212,7 @@ struct Options | @@ -211,6 +212,7 @@ struct Options | ||
| 211 | bool password_is_hex_key; | 212 | bool password_is_hex_key; |
| 212 | bool suppress_password_recovery; | 213 | bool suppress_password_recovery; |
| 213 | password_mode_e password_mode; | 214 | password_mode_e password_mode; |
| 215 | + bool allow_insecure; | ||
| 214 | std::string user_password; | 216 | std::string user_password; |
| 215 | std::string owner_password; | 217 | std::string owner_password; |
| 216 | int keylen; | 218 | int keylen; |
| @@ -742,6 +744,7 @@ class ArgParser | @@ -742,6 +744,7 @@ class ArgParser | ||
| 742 | void argEncrypt(); | 744 | void argEncrypt(); |
| 743 | void argDecrypt(); | 745 | void argDecrypt(); |
| 744 | void argPasswordIsHexKey(); | 746 | void argPasswordIsHexKey(); |
| 747 | + void argAllowInsecure(); | ||
| 745 | void argPasswordMode(char* parameter); | 748 | void argPasswordMode(char* parameter); |
| 746 | void argSuppressPasswordRecovery(); | 749 | void argSuppressPasswordRecovery(); |
| 747 | void argCopyEncryption(char* parameter); | 750 | void argCopyEncryption(char* parameter); |
| @@ -1074,13 +1077,17 @@ ArgParser::initOptionTable() | @@ -1074,13 +1077,17 @@ ArgParser::initOptionTable() | ||
| 1074 | 1077 | ||
| 1075 | t = &this->encrypt40_option_table; | 1078 | t = &this->encrypt40_option_table; |
| 1076 | (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); | 1079 | (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); |
| 1080 | + (*t)["allow-insecure"] = oe_bare(&ArgParser::argAllowInsecure); | ||
| 1081 | + // The above 40-bit options are also 128-bit and 256-bit options, | ||
| 1082 | + // so copy what we have so far to 128. Then continue separately | ||
| 1083 | + // with 128. We later copy 128 to 256. | ||
| 1084 | + this->encrypt128_option_table = this->encrypt40_option_table; | ||
| 1077 | (*t)["print"] = oe_requiredChoices(&ArgParser::arg40Print, yn); | 1085 | (*t)["print"] = oe_requiredChoices(&ArgParser::arg40Print, yn); |
| 1078 | (*t)["modify"] = oe_requiredChoices(&ArgParser::arg40Modify, yn); | 1086 | (*t)["modify"] = oe_requiredChoices(&ArgParser::arg40Modify, yn); |
| 1079 | (*t)["extract"] = oe_requiredChoices(&ArgParser::arg40Extract, yn); | 1087 | (*t)["extract"] = oe_requiredChoices(&ArgParser::arg40Extract, yn); |
| 1080 | (*t)["annotate"] = oe_requiredChoices(&ArgParser::arg40Annotate, yn); | 1088 | (*t)["annotate"] = oe_requiredChoices(&ArgParser::arg40Annotate, yn); |
| 1081 | 1089 | ||
| 1082 | t = &this->encrypt128_option_table; | 1090 | t = &this->encrypt128_option_table; |
| 1083 | - (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); | ||
| 1084 | (*t)["accessibility"] = oe_requiredChoices( | 1091 | (*t)["accessibility"] = oe_requiredChoices( |
| 1085 | &ArgParser::arg128Accessibility, yn); | 1092 | &ArgParser::arg128Accessibility, yn); |
| 1086 | (*t)["extract"] = oe_requiredChoices(&ArgParser::arg128Extract, yn); | 1093 | (*t)["extract"] = oe_requiredChoices(&ArgParser::arg128Extract, yn); |
| @@ -1317,6 +1324,10 @@ ArgParser::argHelp() | @@ -1317,6 +1324,10 @@ ArgParser::argHelp() | ||
| 1317 | << "\n" | 1324 | << "\n" |
| 1318 | << "Additional flags are dependent upon key length.\n" | 1325 | << "Additional flags are dependent upon key length.\n" |
| 1319 | << "\n" | 1326 | << "\n" |
| 1327 | + << " For all key lengths:\n" | ||
| 1328 | + << " --allow-insecure allow the owner password to be empty or the\n" | ||
| 1329 | + << " same as the user password\n" | ||
| 1330 | + << "\n" | ||
| 1320 | << " If 40:\n" | 1331 | << " If 40:\n" |
| 1321 | << "\n" | 1332 | << "\n" |
| 1322 | << " --print=[yn] allow printing\n" | 1333 | << " --print=[yn] allow printing\n" |
| @@ -1850,6 +1861,12 @@ ArgParser::argPasswordMode(char* parameter) | @@ -1850,6 +1861,12 @@ ArgParser::argPasswordMode(char* parameter) | ||
| 1850 | } | 1861 | } |
| 1851 | 1862 | ||
| 1852 | void | 1863 | void |
| 1864 | +ArgParser::argAllowInsecure() | ||
| 1865 | +{ | ||
| 1866 | + o.allow_insecure = true; | ||
| 1867 | +} | ||
| 1868 | + | ||
| 1869 | +void | ||
| 1853 | ArgParser::argCopyEncryption(char* parameter) | 1870 | ArgParser::argCopyEncryption(char* parameter) |
| 1854 | { | 1871 | { |
| 1855 | o.encryption_file = parameter; | 1872 | o.encryption_file = parameter; |
| @@ -3337,6 +3354,18 @@ ArgParser::doFinalChecks() | @@ -3337,6 +3354,18 @@ ArgParser::doFinalChecks() | ||
| 3337 | " together"); | 3354 | " together"); |
| 3338 | } | 3355 | } |
| 3339 | 3356 | ||
| 3357 | + if (o.encrypt && (! o.allow_insecure) && | ||
| 3358 | + (o.owner_password.empty() || | ||
| 3359 | + (o.owner_password == o.user_password))) | ||
| 3360 | + { | ||
| 3361 | + usage("An encrypted PDF with an empty owner password or an" | ||
| 3362 | + " owner password that is the same as a user password" | ||
| 3363 | + " is insecure and can't be opened by some viewers. If you" | ||
| 3364 | + " really want to do this, you must also give the" | ||
| 3365 | + " --allow-insecure option before the -- that follows" | ||
| 3366 | + " --encrypt."); | ||
| 3367 | + } | ||
| 3368 | + | ||
| 3340 | if (o.require_outfile && o.outfilename && | 3369 | if (o.require_outfile && o.outfilename && |
| 3341 | (strcmp(o.outfilename, "-") == 0)) | 3370 | (strcmp(o.outfilename, "-") == 0)) |
| 3342 | { | 3371 | { |
qpdf/qtest/qpdf.test
| @@ -3189,19 +3189,19 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) | @@ -3189,19 +3189,19 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) | ||
| 3189 | check_metadata("a.pdf", 0, 1); | 3189 | check_metadata("a.pdf", 0, 1); |
| 3190 | $td->runtest("encrypt normally", | 3190 | $td->runtest("encrypt normally", |
| 3191 | {$td->COMMAND => | 3191 | {$td->COMMAND => |
| 3192 | - "qpdf --encrypt '' '' 128 -- a.pdf b.pdf"}, | 3192 | + "qpdf --encrypt '' o 128 -- a.pdf b.pdf"}, |
| 3193 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3193 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3194 | check_metadata("b.pdf", 1, 0); | 3194 | check_metadata("b.pdf", 1, 0); |
| 3195 | unlink "b.pdf"; | 3195 | unlink "b.pdf"; |
| 3196 | $td->runtest("encrypt V4", | 3196 | $td->runtest("encrypt V4", |
| 3197 | {$td->COMMAND => | 3197 | {$td->COMMAND => |
| 3198 | - "qpdf --encrypt '' '' 128 --force-V4 -- a.pdf b.pdf"}, | 3198 | + "qpdf --encrypt '' o 128 --force-V4 -- a.pdf b.pdf"}, |
| 3199 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3199 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3200 | check_metadata("b.pdf", 1, 0); | 3200 | check_metadata("b.pdf", 1, 0); |
| 3201 | unlink "b.pdf"; | 3201 | unlink "b.pdf"; |
| 3202 | $td->runtest("encrypt with cleartext metadata", | 3202 | $td->runtest("encrypt with cleartext metadata", |
| 3203 | {$td->COMMAND => | 3203 | {$td->COMMAND => |
| 3204 | - "qpdf --encrypt '' '' 128 --cleartext-metadata --" . | 3204 | + "qpdf --encrypt '' o 128 --cleartext-metadata --" . |
| 3205 | " a.pdf b.pdf"}, | 3205 | " a.pdf b.pdf"}, |
| 3206 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3206 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3207 | check_metadata("b.pdf", 1, 1); | 3207 | check_metadata("b.pdf", 1, 1); |
| @@ -3212,7 +3212,7 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) | @@ -3212,7 +3212,7 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) | ||
| 3212 | unlink "b.pdf", "c.pdf"; | 3212 | unlink "b.pdf", "c.pdf"; |
| 3213 | $td->runtest("encrypt with aes and cleartext metadata", | 3213 | $td->runtest("encrypt with aes and cleartext metadata", |
| 3214 | {$td->COMMAND => | 3214 | {$td->COMMAND => |
| 3215 | - "qpdf --encrypt '' '' 128" . | 3215 | + "qpdf --encrypt '' o 128" . |
| 3216 | " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"}, | 3216 | " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"}, |
| 3217 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3217 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3218 | check_metadata("b.pdf", 1, 1); | 3218 | check_metadata("b.pdf", 1, 1); |
| @@ -3441,7 +3441,7 @@ my @encrypted_files = | @@ -3441,7 +3441,7 @@ my @encrypted_files = | ||
| 3441 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | 3441 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| 3442 | ); | 3442 | ); |
| 3443 | 3443 | ||
| 3444 | -$n_tests += 5 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9; | 3444 | +$n_tests += 9 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9; |
| 3445 | 3445 | ||
| 3446 | $td->runtest("encrypted file", | 3446 | $td->runtest("encrypted file", |
| 3447 | {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"}, | 3447 | {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"}, |
| @@ -3458,6 +3458,26 @@ $td->runtest("recheck encrypted file", | @@ -3458,6 +3458,26 @@ $td->runtest("recheck encrypted file", | ||
| 3458 | $td->EXIT_STATUS => 0}, | 3458 | $td->EXIT_STATUS => 0}, |
| 3459 | $td->NORMALIZE_NEWLINES); | 3459 | $td->NORMALIZE_NEWLINES); |
| 3460 | 3460 | ||
| 3461 | +$td->runtest("empty owner password", | ||
| 3462 | + {$td->COMMAND => "qpdf --encrypt '' '' 128 -- minimal.pdf a.pdf"}, | ||
| 3463 | + {$td->REGEXP => ".*is insecure.*--allow-insecure.*", | ||
| 3464 | + $td->EXIT_STATUS => 2}, | ||
| 3465 | + $td->NORMALIZE_NEWLINES); | ||
| 3466 | +$td->runtest("matching user/owner password", | ||
| 3467 | + {$td->COMMAND => "qpdf --encrypt q q 128 -- minimal.pdf a.pdf"}, | ||
| 3468 | + {$td->REGEXP => ".*is insecure.*--allow-insecure.*", | ||
| 3469 | + $td->EXIT_STATUS => 2}, | ||
| 3470 | + $td->NORMALIZE_NEWLINES); | ||
| 3471 | +$td->runtest("allow insecure", | ||
| 3472 | + {$td->COMMAND => "qpdf --encrypt '' '' 128 --allow-insecure --" . | ||
| 3473 | + " minimal.pdf a.pdf"}, | ||
| 3474 | + {$td->STRING => "", $td->EXIT_STATUS => 0}, | ||
| 3475 | + $td->NORMALIZE_NEWLINES); | ||
| 3476 | +$td->runtest("check insecure", | ||
| 3477 | + {$td->COMMAND => "qpdf --check a.pdf"}, | ||
| 3478 | + {$td->FILE => "insecure-passwords.out", $td->EXIT_STATUS => 0}, | ||
| 3479 | + $td->NORMALIZE_NEWLINES); | ||
| 3480 | + | ||
| 3461 | # Test that long passwords that are one character too short fail. We | 3481 | # Test that long passwords that are one character too short fail. We |
| 3462 | # test the truncation cases in the loop below by using passwords | 3482 | # test the truncation cases in the loop below by using passwords |
| 3463 | # longer than the supported length. | 3483 | # longer than the supported length. |
| @@ -3587,6 +3607,10 @@ foreach my $d (@encrypted_files) | @@ -3587,6 +3607,10 @@ foreach my $d (@encrypted_files) | ||
| 3587 | $enc_json =~ s/---upm---/$upm/; | 3607 | $enc_json =~ s/---upm---/$upm/; |
| 3588 | 3608 | ||
| 3589 | my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; | 3609 | my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; |
| 3610 | + if (($opass eq "") || ($opass eq $upass)) | ||
| 3611 | + { | ||
| 3612 | + $eflags =~ s/--$/--allow-insecure --/; | ||
| 3613 | + } | ||
| 3590 | if (($pass ne $upass) && ($V >= 5)) | 3614 | if (($pass ne $upass) && ($V >= 5)) |
| 3591 | { | 3615 | { |
| 3592 | # V >= 5 can no longer recover user password with owner | 3616 | # V >= 5 can no longer recover user password with owner |
| @@ -3758,7 +3782,7 @@ $td->runtest("check linearization", | @@ -3758,7 +3782,7 @@ $td->runtest("check linearization", | ||
| 3758 | # Test AES encryption in various ways. | 3782 | # Test AES encryption in various ways. |
| 3759 | $n_tests += 18; | 3783 | $n_tests += 18; |
| 3760 | $td->runtest("encrypt with AES", | 3784 | $td->runtest("encrypt with AES", |
| 3761 | - {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . | 3785 | + {$td->COMMAND => "qpdf --encrypt '' o 128 --use-aes=y --" . |
| 3762 | " enc-base.pdf a.pdf"}, | 3786 | " enc-base.pdf a.pdf"}, |
| 3763 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3787 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3764 | $td->runtest("check encryption", | 3788 | $td->runtest("check encryption", |
| @@ -3779,7 +3803,7 @@ $td->runtest("compare files", | @@ -3779,7 +3803,7 @@ $td->runtest("compare files", | ||
| 3779 | {$td->FILE => 'a.qdf'}, | 3803 | {$td->FILE => 'a.qdf'}, |
| 3780 | {$td->FILE => 'b.qdf'}); | 3804 | {$td->FILE => 'b.qdf'}); |
| 3781 | $td->runtest("linearize with AES and object streams", | 3805 | $td->runtest("linearize with AES and object streams", |
| 3782 | - {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . | 3806 | + {$td->COMMAND => "qpdf --encrypt '' o 128 --use-aes=y --" . |
| 3783 | " --linearize --object-streams=generate enc-base.pdf a.pdf"}, | 3807 | " --linearize --object-streams=generate enc-base.pdf a.pdf"}, |
| 3784 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3808 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3785 | $td->runtest("check encryption", | 3809 | $td->runtest("check encryption", |
| @@ -3845,7 +3869,8 @@ foreach my $d (['--force-V4', 'V4'], | @@ -3845,7 +3869,8 @@ foreach my $d (['--force-V4', 'V4'], | ||
| 3845 | my ($args, $out) = @$d; | 3869 | my ($args, $out) = @$d; |
| 3846 | $td->runtest("encrypt $args", | 3870 | $td->runtest("encrypt $args", |
| 3847 | {$td->COMMAND => "qpdf --static-aes-iv --static-id" . | 3871 | {$td->COMMAND => "qpdf --static-aes-iv --static-id" . |
| 3848 | - " --encrypt '' '' 128 $args -- enc-base.pdf a.pdf"}, | 3872 | + " --encrypt '' '' 128 $args --allow-insecure --" . |
| 3873 | + " enc-base.pdf a.pdf"}, | ||
| 3849 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3874 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3850 | $td->runtest("check output", | 3875 | $td->runtest("check output", |
| 3851 | {$td->FILE => "a.pdf"}, | 3876 | {$td->FILE => "a.pdf"}, |
qpdf/qtest/qpdf/insecure-passwords.out
0 → 100644
| 1 | +checking a.pdf | ||
| 2 | +PDF Version: 1.4 | ||
| 3 | +R = 3 | ||
| 4 | +P = -4 | ||
| 5 | +User password = | ||
| 6 | +Supplied password is owner password | ||
| 7 | +Supplied password is user password | ||
| 8 | +extract for accessibility: allowed | ||
| 9 | +extract for any purpose: allowed | ||
| 10 | +print low resolution: allowed | ||
| 11 | +print high resolution: allowed | ||
| 12 | +modify document assembly: allowed | ||
| 13 | +modify forms: allowed | ||
| 14 | +modify annotations: allowed | ||
| 15 | +modify other: allowed | ||
| 16 | +modify anything: allowed | ||
| 17 | +File is not linearized | ||
| 18 | +No syntax or stream encoding errors found; the file may still contain | ||
| 19 | +errors that qpdf cannot detect |