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 | 10 | 2021-02-02 Jay Berkenbilt <ejb@ql.org> |
| 2 | 11 | |
| 3 | 12 | * Bug fix: if a form XObject lacks a resources dictionary, | ... | ... |
manual/qpdf-manual.xml
| ... | ... | @@ -1214,7 +1214,11 @@ make |
| 1214 | 1214 | </para> |
| 1215 | 1215 | <para> |
| 1216 | 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 | 1222 | </para> |
| 1219 | 1223 | <para> |
| 1220 | 1224 | The value for |
| ... | ... | @@ -1224,6 +1228,25 @@ make |
| 1224 | 1228 | fully permissive. |
| 1225 | 1229 | </para> |
| 1226 | 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 | 1250 | If <option><replaceable>key-length</replaceable></option> is 40, |
| 1228 | 1251 | the following restriction options are available: |
| 1229 | 1252 | <variablelist> |
| ... | ... | @@ -4824,7 +4847,28 @@ print "\n"; |
| 4824 | 4847 | <itemizedlist> |
| 4825 | 4848 | <listitem> |
| 4826 | 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 | 4872 | </para> |
| 4829 | 4873 | <itemizedlist> |
| 4830 | 4874 | <listitem> | ... | ... |
qpdf/qpdf.cc
| ... | ... | @@ -113,6 +113,7 @@ struct Options |
| 113 | 113 | password_is_hex_key(false), |
| 114 | 114 | suppress_password_recovery(false), |
| 115 | 115 | password_mode(pm_auto), |
| 116 | + allow_insecure(false), | |
| 116 | 117 | keylen(0), |
| 117 | 118 | r2_print(true), |
| 118 | 119 | r2_modify(true), |
| ... | ... | @@ -211,6 +212,7 @@ struct Options |
| 211 | 212 | bool password_is_hex_key; |
| 212 | 213 | bool suppress_password_recovery; |
| 213 | 214 | password_mode_e password_mode; |
| 215 | + bool allow_insecure; | |
| 214 | 216 | std::string user_password; |
| 215 | 217 | std::string owner_password; |
| 216 | 218 | int keylen; |
| ... | ... | @@ -742,6 +744,7 @@ class ArgParser |
| 742 | 744 | void argEncrypt(); |
| 743 | 745 | void argDecrypt(); |
| 744 | 746 | void argPasswordIsHexKey(); |
| 747 | + void argAllowInsecure(); | |
| 745 | 748 | void argPasswordMode(char* parameter); |
| 746 | 749 | void argSuppressPasswordRecovery(); |
| 747 | 750 | void argCopyEncryption(char* parameter); |
| ... | ... | @@ -1074,13 +1077,17 @@ ArgParser::initOptionTable() |
| 1074 | 1077 | |
| 1075 | 1078 | t = &this->encrypt40_option_table; |
| 1076 | 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 | 1085 | (*t)["print"] = oe_requiredChoices(&ArgParser::arg40Print, yn); |
| 1078 | 1086 | (*t)["modify"] = oe_requiredChoices(&ArgParser::arg40Modify, yn); |
| 1079 | 1087 | (*t)["extract"] = oe_requiredChoices(&ArgParser::arg40Extract, yn); |
| 1080 | 1088 | (*t)["annotate"] = oe_requiredChoices(&ArgParser::arg40Annotate, yn); |
| 1081 | 1089 | |
| 1082 | 1090 | t = &this->encrypt128_option_table; |
| 1083 | - (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); | |
| 1084 | 1091 | (*t)["accessibility"] = oe_requiredChoices( |
| 1085 | 1092 | &ArgParser::arg128Accessibility, yn); |
| 1086 | 1093 | (*t)["extract"] = oe_requiredChoices(&ArgParser::arg128Extract, yn); |
| ... | ... | @@ -1317,6 +1324,10 @@ ArgParser::argHelp() |
| 1317 | 1324 | << "\n" |
| 1318 | 1325 | << "Additional flags are dependent upon key length.\n" |
| 1319 | 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 | 1331 | << " If 40:\n" |
| 1321 | 1332 | << "\n" |
| 1322 | 1333 | << " --print=[yn] allow printing\n" |
| ... | ... | @@ -1850,6 +1861,12 @@ ArgParser::argPasswordMode(char* parameter) |
| 1850 | 1861 | } |
| 1851 | 1862 | |
| 1852 | 1863 | void |
| 1864 | +ArgParser::argAllowInsecure() | |
| 1865 | +{ | |
| 1866 | + o.allow_insecure = true; | |
| 1867 | +} | |
| 1868 | + | |
| 1869 | +void | |
| 1853 | 1870 | ArgParser::argCopyEncryption(char* parameter) |
| 1854 | 1871 | { |
| 1855 | 1872 | o.encryption_file = parameter; |
| ... | ... | @@ -3337,6 +3354,18 @@ ArgParser::doFinalChecks() |
| 3337 | 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 | 3369 | if (o.require_outfile && o.outfilename && |
| 3341 | 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 | 3189 | check_metadata("a.pdf", 0, 1); |
| 3190 | 3190 | $td->runtest("encrypt normally", |
| 3191 | 3191 | {$td->COMMAND => |
| 3192 | - "qpdf --encrypt '' '' 128 -- a.pdf b.pdf"}, | |
| 3192 | + "qpdf --encrypt '' o 128 -- a.pdf b.pdf"}, | |
| 3193 | 3193 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3194 | 3194 | check_metadata("b.pdf", 1, 0); |
| 3195 | 3195 | unlink "b.pdf"; |
| 3196 | 3196 | $td->runtest("encrypt V4", |
| 3197 | 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 | 3199 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3200 | 3200 | check_metadata("b.pdf", 1, 0); |
| 3201 | 3201 | unlink "b.pdf"; |
| 3202 | 3202 | $td->runtest("encrypt with cleartext metadata", |
| 3203 | 3203 | {$td->COMMAND => |
| 3204 | - "qpdf --encrypt '' '' 128 --cleartext-metadata --" . | |
| 3204 | + "qpdf --encrypt '' o 128 --cleartext-metadata --" . | |
| 3205 | 3205 | " a.pdf b.pdf"}, |
| 3206 | 3206 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3207 | 3207 | check_metadata("b.pdf", 1, 1); |
| ... | ... | @@ -3212,7 +3212,7 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) |
| 3212 | 3212 | unlink "b.pdf", "c.pdf"; |
| 3213 | 3213 | $td->runtest("encrypt with aes and cleartext metadata", |
| 3214 | 3214 | {$td->COMMAND => |
| 3215 | - "qpdf --encrypt '' '' 128" . | |
| 3215 | + "qpdf --encrypt '' o 128" . | |
| 3216 | 3216 | " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"}, |
| 3217 | 3217 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3218 | 3218 | check_metadata("b.pdf", 1, 1); |
| ... | ... | @@ -3441,7 +3441,7 @@ my @encrypted_files = |
| 3441 | 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 | 3446 | $td->runtest("encrypted file", |
| 3447 | 3447 | {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"}, |
| ... | ... | @@ -3458,6 +3458,26 @@ $td->runtest("recheck encrypted file", |
| 3458 | 3458 | $td->EXIT_STATUS => 0}, |
| 3459 | 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 | 3481 | # Test that long passwords that are one character too short fail. We |
| 3462 | 3482 | # test the truncation cases in the loop below by using passwords |
| 3463 | 3483 | # longer than the supported length. |
| ... | ... | @@ -3587,6 +3607,10 @@ foreach my $d (@encrypted_files) |
| 3587 | 3607 | $enc_json =~ s/---upm---/$upm/; |
| 3588 | 3608 | |
| 3589 | 3609 | my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; |
| 3610 | + if (($opass eq "") || ($opass eq $upass)) | |
| 3611 | + { | |
| 3612 | + $eflags =~ s/--$/--allow-insecure --/; | |
| 3613 | + } | |
| 3590 | 3614 | if (($pass ne $upass) && ($V >= 5)) |
| 3591 | 3615 | { |
| 3592 | 3616 | # V >= 5 can no longer recover user password with owner |
| ... | ... | @@ -3758,7 +3782,7 @@ $td->runtest("check linearization", |
| 3758 | 3782 | # Test AES encryption in various ways. |
| 3759 | 3783 | $n_tests += 18; |
| 3760 | 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 | 3786 | " enc-base.pdf a.pdf"}, |
| 3763 | 3787 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3764 | 3788 | $td->runtest("check encryption", |
| ... | ... | @@ -3779,7 +3803,7 @@ $td->runtest("compare files", |
| 3779 | 3803 | {$td->FILE => 'a.qdf'}, |
| 3780 | 3804 | {$td->FILE => 'b.qdf'}); |
| 3781 | 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 | 3807 | " --linearize --object-streams=generate enc-base.pdf a.pdf"}, |
| 3784 | 3808 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3785 | 3809 | $td->runtest("check encryption", |
| ... | ... | @@ -3845,7 +3869,8 @@ foreach my $d (['--force-V4', 'V4'], |
| 3845 | 3869 | my ($args, $out) = @$d; |
| 3846 | 3870 | $td->runtest("encrypt $args", |
| 3847 | 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 | 3874 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3850 | 3875 | $td->runtest("check output", |
| 3851 | 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 | ... | ... |