Commit 569d74d36ba287b6951687ee1bdea45ae19091f8
1 parent
3e306ae6
Allow raw encryption key to be specified
Add options to enable the raw encryption key to be directly shown or specified. Thanks to Didier Stevens <didier.stevens@gmail.com> for the idea and contribution of one implementation of this idea.
Showing
12 changed files
with
164 additions
and
14 deletions
ChangeLog
| 1 | +2018-01-14 Jay Berkenbilt <ejb@ql.org> | |
| 2 | + | |
| 3 | + * Allow raw encryption key to be specified in libary and command | |
| 4 | + line with the QPDF::setPasswordIsHexKey method and | |
| 5 | + --password-is-hex-key option. Allow encryption key to be displayed | |
| 6 | + with --show-encryption-key option. Thanks to Didier Stevens | |
| 7 | + <didier.stevens@gmail.com> for the idea and contribution of one | |
| 8 | + implementation of this idea. See his blog post at | |
| 9 | + https://blog.didierstevens.com/2017/12/28/cracking-encrypted-pdfs-part-3/ | |
| 10 | + for a discussion of using this for cracking encrypted PDFs. I hope | |
| 11 | + that a future release of qpdf will include some additional | |
| 12 | + recovery options that may also make use of this capability. | |
| 13 | + | |
| 1 | 14 | 2018-01-13 Jay Berkenbilt <ejb@ql.org> |
| 2 | 15 | |
| 3 | 16 | * Fix lexical error: the PDF specification allows floating point | ... | ... |
TODO
| 1 | 1 | Soon |
| 2 | 2 | ==== |
| 3 | 3 | |
| 4 | - * Take changes on encryption-keys branch and make them usable. | |
| 5 | - Replace the hex encoding and decoding piece, and come up with a | |
| 6 | - more robust way of specifying the key. | |
| 7 | - | |
| 8 | 4 | * Consider whether there should be a mode in which QPDFObjectHandle |
| 9 | 5 | returns nulls for operations on the wrong type instead of asserting |
| 10 | 6 | the type. The way things are wired up now, this would have to be a | ... | ... |
include/qpdf/QPDF.hh
| ... | ... | @@ -64,7 +64,11 @@ class QPDF |
| 64 | 64 | // those that set parameters. If the input file is not |
| 65 | 65 | // encrypted,either a null password or an empty password can be |
| 66 | 66 | // used. If the file is encrypted, either the user password or |
| 67 | - // the owner password may be supplied. | |
| 67 | + // the owner password may be supplied. The method | |
| 68 | + // setPasswordIsHexKey may be called prior to calling this method | |
| 69 | + // or any of the other process methods to force the password to be | |
| 70 | + // interpreted as a raw encryption key. See comments on | |
| 71 | + // setPasswordIsHexKey for more information. | |
| 68 | 72 | QPDF_DLL |
| 69 | 73 | void processFile(char const* filename, char const* password = 0); |
| 70 | 74 | |
| ... | ... | @@ -94,6 +98,18 @@ class QPDF |
| 94 | 98 | void processInputSource(PointerHolder<InputSource>, |
| 95 | 99 | char const* password = 0); |
| 96 | 100 | |
| 101 | + // For certain forensic or investigatory purposes, it may | |
| 102 | + // sometimes be useful to specify the encryption key directly, | |
| 103 | + // even though regular PDF applications do not provide a way to do | |
| 104 | + // this. calling setPasswordIsHexKey(true) before calling any of | |
| 105 | + // the process methods will bypass the normal encryption key | |
| 106 | + // computation or recovery mechanisms and interpret the bytes in | |
| 107 | + // the password as a hex-encoded encryption key. Note that we | |
| 108 | + // hex-encode the key because it may contain null bytes and | |
| 109 | + // therefore can't be represented in a char const*. | |
| 110 | + QPDF_DLL | |
| 111 | + void setPasswordIsHexKey(bool); | |
| 112 | + | |
| 97 | 113 | // Create a QPDF object for an empty PDF. This PDF has no pages |
| 98 | 114 | // or objects other than a minimal trailer, a document catalog, |
| 99 | 115 | // and a /Pages tree containing zero pages. Pages and other |
| ... | ... | @@ -1145,6 +1161,7 @@ class QPDF |
| 1145 | 1161 | QPDFTokenizer tokenizer; |
| 1146 | 1162 | PointerHolder<InputSource> file; |
| 1147 | 1163 | std::string last_object_description; |
| 1164 | + bool provided_password_is_hex_key; | |
| 1148 | 1165 | bool encrypted; |
| 1149 | 1166 | bool encryption_initialized; |
| 1150 | 1167 | bool ignore_xref_streams; | ... | ... |
libqpdf/QPDF.cc
| ... | ... | @@ -75,6 +75,7 @@ QPDF::QPDFVersion() |
| 75 | 75 | } |
| 76 | 76 | |
| 77 | 77 | QPDF::Members::Members() : |
| 78 | + provided_password_is_hex_key(false), | |
| 78 | 79 | encrypted(false), |
| 79 | 80 | encryption_initialized(false), |
| 80 | 81 | ignore_xref_streams(false), |
| ... | ... | @@ -172,6 +173,12 @@ QPDF::processInputSource(PointerHolder<InputSource> source, |
| 172 | 173 | } |
| 173 | 174 | |
| 174 | 175 | void |
| 176 | +QPDF::setPasswordIsHexKey(bool val) | |
| 177 | +{ | |
| 178 | + this->m->provided_password_is_hex_key = val; | |
| 179 | +} | |
| 180 | + | |
| 181 | +void | |
| 175 | 182 | QPDF::emptyPDF() |
| 176 | 183 | { |
| 177 | 184 | processMemoryFile("empty PDF", EMPTY_PDF, strlen(EMPTY_PDF)); | ... | ... |
libqpdf/QPDF_encryption.cc
| ... | ... | @@ -1007,8 +1007,12 @@ QPDF::initializeEncryption() |
| 1007 | 1007 | |
| 1008 | 1008 | EncryptionData data(V, R, Length / 8, P, O, U, OE, UE, Perms, |
| 1009 | 1009 | id1, this->m->encrypt_metadata); |
| 1010 | - if (check_owner_password( | |
| 1011 | - this->m->user_password, this->m->provided_password, data)) | |
| 1010 | + if (this->m->provided_password_is_hex_key) | |
| 1011 | + { | |
| 1012 | + // ignore passwords in file | |
| 1013 | + } | |
| 1014 | + else if (check_owner_password( | |
| 1015 | + this->m->user_password, this->m->provided_password, data)) | |
| 1012 | 1016 | { |
| 1013 | 1017 | // password supplied was owner password; user_password has |
| 1014 | 1018 | // been initialized for V < 5 |
| ... | ... | @@ -1023,7 +1027,11 @@ QPDF::initializeEncryption() |
| 1023 | 1027 | "", 0, "invalid password"); |
| 1024 | 1028 | } |
| 1025 | 1029 | |
| 1026 | - if (V < 5) | |
| 1030 | + if (this->m->provided_password_is_hex_key) | |
| 1031 | + { | |
| 1032 | + this->m->encryption_key = QUtil::hex_decode(this->m->provided_password); | |
| 1033 | + } | |
| 1034 | + else if (V < 5) | |
| 1027 | 1035 | { |
| 1028 | 1036 | // For V < 5, the user password is encrypted with the owner |
| 1029 | 1037 | // password, and the user password is always used for | ... | ... |
qpdf/qpdf.cc
| ... | ... | @@ -61,6 +61,7 @@ struct Options |
| 61 | 61 | encryption_file(0), |
| 62 | 62 | encryption_file_password(0), |
| 63 | 63 | encrypt(false), |
| 64 | + password_is_hex_key(false), | |
| 64 | 65 | keylen(0), |
| 65 | 66 | r2_print(true), |
| 66 | 67 | r2_modify(true), |
| ... | ... | @@ -95,6 +96,7 @@ struct Options |
| 95 | 96 | static_aes_iv(false), |
| 96 | 97 | suppress_original_object_id(false), |
| 97 | 98 | show_encryption(false), |
| 99 | + show_encryption_key(false), | |
| 98 | 100 | check_linearization(false), |
| 99 | 101 | show_linearization(false), |
| 100 | 102 | show_xref(false), |
| ... | ... | @@ -120,6 +122,7 @@ struct Options |
| 120 | 122 | char const* encryption_file; |
| 121 | 123 | char const* encryption_file_password; |
| 122 | 124 | bool encrypt; |
| 125 | + bool password_is_hex_key; | |
| 123 | 126 | std::string user_password; |
| 124 | 127 | std::string owner_password; |
| 125 | 128 | int keylen; |
| ... | ... | @@ -158,6 +161,7 @@ struct Options |
| 158 | 161 | bool static_aes_iv; |
| 159 | 162 | bool suppress_original_object_id; |
| 160 | 163 | bool show_encryption; |
| 164 | + bool show_encryption_key; | |
| 161 | 165 | bool check_linearization; |
| 162 | 166 | bool show_linearization; |
| 163 | 167 | bool show_xref; |
| ... | ... | @@ -227,6 +231,7 @@ Basic Options\n\ |
| 227 | 231 | parameters are being copied\n\ |
| 228 | 232 | --encrypt options -- generate an encrypted file\n\ |
| 229 | 233 | --decrypt remove any encryption on the file\n\ |
| 234 | +--password-is-hex-key treat primary password option as a hex-encoded key\n\ | |
| 230 | 235 | --pages options -- select specific pages from one or more files\n\ |
| 231 | 236 | --rotate=[+|-]angle:page-range\n\ |
| 232 | 237 | rotate each specified page 90, 180, or 270 degrees\n\ |
| ... | ... | @@ -240,6 +245,11 @@ parameters will be copied, including both user and owner passwords, even\n\ |
| 240 | 245 | if the user password is used to open the other file. This works even if\n\ |
| 241 | 246 | the owner password is not known.\n\ |
| 242 | 247 | \n\ |
| 248 | +The --password-is-hex-key option overrides the normal computation of\n\ | |
| 249 | +encryption keys. It only applies to the password used to open the main\n\ | |
| 250 | +file. This option is not ordinarily useful but can be helpful for forensic\n\ | |
| 251 | +or investigatory purposes. See manual for further discussion.\n\ | |
| 252 | +\n\ | |
| 243 | 253 | The --rotate flag can be used to specify pages to rotate pages either\n\ |
| 244 | 254 | 90, 180, or 270 degrees. The page range is specified in the same\n\ |
| 245 | 255 | format as with the --pages option, described below. Repeat the option\n\ |
| ... | ... | @@ -434,6 +444,7 @@ automated test suites for software that uses the qpdf library.\n\ |
| 434 | 444 | This is option is not secure! FOR TESTING ONLY!\n\ |
| 435 | 445 | --no-original-object-ids suppress original object ID comments in qdf mode\n\ |
| 436 | 446 | --show-encryption quickly show encryption parameters\n\ |
| 447 | +--show-encryption-key when showing encryption, reveal the actual key\n\ | |
| 437 | 448 | --check-linearization check file integrity and linearization status\n\ |
| 438 | 449 | --show-linearization check and show all linearization data\n\ |
| 439 | 450 | --show-xref show the contents of the cross-reference table\n\ |
| ... | ... | @@ -501,7 +512,7 @@ static std::string show_encryption_method(QPDF::encryption_method_e method) |
| 501 | 512 | return result; |
| 502 | 513 | } |
| 503 | 514 | |
| 504 | -static void show_encryption(QPDF& pdf) | |
| 515 | +static void show_encryption(QPDF& pdf, Options& o) | |
| 505 | 516 | { |
| 506 | 517 | // Extract /P from /Encrypt |
| 507 | 518 | int R = 0; |
| ... | ... | @@ -520,8 +531,14 @@ static void show_encryption(QPDF& pdf) |
| 520 | 531 | std::cout << "R = " << R << std::endl; |
| 521 | 532 | std::cout << "P = " << P << std::endl; |
| 522 | 533 | std::string user_password = pdf.getTrimmedUserPassword(); |
| 523 | - std::cout << "User password = " << user_password << std::endl | |
| 524 | - << "extract for accessibility: " | |
| 534 | + std::string encryption_key = pdf.getEncryptionKey(); | |
| 535 | + std::cout << "User password = " << user_password << std::endl; | |
| 536 | + if (o.show_encryption_key) | |
| 537 | + { | |
| 538 | + std::cout << "Encryption key = " | |
| 539 | + << QUtil::hex_encode(encryption_key) << std::endl; | |
| 540 | + } | |
| 541 | + std::cout << "extract for accessibility: " | |
| 525 | 542 | << show_bool(pdf.allowAccessibility()) << std::endl |
| 526 | 543 | << "extract for any purpose: " |
| 527 | 544 | << show_bool(pdf.allowExtractAll()) << std::endl |
| ... | ... | @@ -1339,6 +1356,10 @@ static void parse_options(int argc, char* argv[], Options& o) |
| 1339 | 1356 | o.encrypt = false; |
| 1340 | 1357 | o.copy_encryption = false; |
| 1341 | 1358 | } |
| 1359 | + else if (strcmp(arg, "password-is-hex-key") == 0) | |
| 1360 | + { | |
| 1361 | + o.password_is_hex_key = true; | |
| 1362 | + } | |
| 1342 | 1363 | else if (strcmp(arg, "copy-encryption") == 0) |
| 1343 | 1364 | { |
| 1344 | 1365 | if (parameter == 0) |
| ... | ... | @@ -1559,6 +1580,10 @@ static void parse_options(int argc, char* argv[], Options& o) |
| 1559 | 1580 | o.show_encryption = true; |
| 1560 | 1581 | o.require_outfile = false; |
| 1561 | 1582 | } |
| 1583 | + else if (strcmp(arg, "show-encryption-key") == 0) | |
| 1584 | + { | |
| 1585 | + o.show_encryption_key = true; | |
| 1586 | + } | |
| 1562 | 1587 | else if (strcmp(arg, "check-linearization") == 0) |
| 1563 | 1588 | { |
| 1564 | 1589 | o.check_linearization = true; |
| ... | ... | @@ -1673,6 +1698,10 @@ static void set_qpdf_options(QPDF& pdf, Options& o) |
| 1673 | 1698 | { |
| 1674 | 1699 | pdf.setAttemptRecovery(false); |
| 1675 | 1700 | } |
| 1701 | + if (o.password_is_hex_key) | |
| 1702 | + { | |
| 1703 | + pdf.setPasswordIsHexKey(true); | |
| 1704 | + } | |
| 1676 | 1705 | } |
| 1677 | 1706 | |
| 1678 | 1707 | static void do_check(QPDF& pdf, Options& o, int& exit_code) |
| ... | ... | @@ -1693,7 +1722,7 @@ static void do_check(QPDF& pdf, Options& o, int& exit_code) |
| 1693 | 1722 | << pdf.getExtensionLevel(); |
| 1694 | 1723 | } |
| 1695 | 1724 | std::cout << std::endl; |
| 1696 | - show_encryption(pdf); | |
| 1725 | + show_encryption(pdf, o); | |
| 1697 | 1726 | if (pdf.isLinearized()) |
| 1698 | 1727 | { |
| 1699 | 1728 | std::cout << "File is linearized\n"; |
| ... | ... | @@ -1877,7 +1906,7 @@ static void do_inspection(QPDF& pdf, Options& o) |
| 1877 | 1906 | } |
| 1878 | 1907 | if (o.show_encryption) |
| 1879 | 1908 | { |
| 1880 | - show_encryption(pdf); | |
| 1909 | + show_encryption(pdf, o); | |
| 1881 | 1910 | } |
| 1882 | 1911 | if (o.check_linearization) |
| 1883 | 1912 | { | ... | ... |
qpdf/qtest/qpdf.test
| ... | ... | @@ -314,7 +314,7 @@ foreach my $file (qw(short-id long-id)) |
| 314 | 314 | $td->NORMALIZE_NEWLINES); |
| 315 | 315 | |
| 316 | 316 | $td->runtest("check $file.pdf", |
| 317 | - {$td->COMMAND => "qpdf --check a.pdf"}, | |
| 317 | + {$td->COMMAND => "qpdf --check --show-encryption-key a.pdf"}, | |
| 318 | 318 | {$td->FILE => "$file-check.out", |
| 319 | 319 | $td->EXIT_STATUS => 0}, |
| 320 | 320 | $td->NORMALIZE_NEWLINES); |
| ... | ... | @@ -2244,6 +2244,21 @@ $td->runtest("copy of unfilterable with crypt", |
| 2244 | 2244 | $td->EXIT_STATUS => 0}, |
| 2245 | 2245 | $td->NORMALIZE_NEWLINES); |
| 2246 | 2246 | |
| 2247 | +# Raw encryption key | |
| 2248 | +my @enc_key = (['user', '--password=user3'], | |
| 2249 | + ['owner', '--password=owner3'], | |
| 2250 | + ['hex', '--password-is-hex-key --password=35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020']); | |
| 2251 | +$n_tests += scalar(@enc_key); | |
| 2252 | +foreach my $d (@enc_key) | |
| 2253 | +{ | |
| 2254 | + my ($description, $pass) = @$d; | |
| 2255 | + $td->runtest("use/show encryption key ($description)", | |
| 2256 | + {$td->COMMAND => | |
| 2257 | + "qpdf --check --show-encryption-key c-r5-in.pdf $pass"}, | |
| 2258 | + {$td->FILE => "c-r5-key-$description.out", $td->EXIT_STATUS => 0}, | |
| 2259 | + $td->NORMALIZE_NEWLINES); | |
| 2260 | +} | |
| 2261 | + | |
| 2247 | 2262 | show_ntests(); |
| 2248 | 2263 | # ---------- |
| 2249 | 2264 | $td->notify("--- Content Preservation Tests ---"); | ... | ... |
qpdf/qtest/qpdf/c-r5-key-hex.out
0 โ 100644
| 1 | +checking c-r5-in.pdf | |
| 2 | +PDF Version: 1.7 extension level 3 | |
| 3 | +R = 5 | |
| 4 | +P = -2052 | |
| 5 | +User password = | |
| 6 | +Encryption key = 35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020 | |
| 7 | +extract for accessibility: allowed | |
| 8 | +extract for any purpose: allowed | |
| 9 | +print low resolution: allowed | |
| 10 | +print high resolution: not allowed | |
| 11 | +modify document assembly: allowed | |
| 12 | +modify forms: allowed | |
| 13 | +modify annotations: allowed | |
| 14 | +modify other: allowed | |
| 15 | +modify anything: allowed | |
| 16 | +stream encryption method: AESv3 | |
| 17 | +string encryption method: AESv3 | |
| 18 | +file encryption method: AESv3 | |
| 19 | +File is not linearized | |
| 20 | +No syntax or stream encoding errors found; the file may still contain | |
| 21 | +errors that qpdf cannot detect | ... | ... |
qpdf/qtest/qpdf/c-r5-key-owner.out
0 โ 100644
| 1 | +checking c-r5-in.pdf | |
| 2 | +PDF Version: 1.7 extension level 3 | |
| 3 | +R = 5 | |
| 4 | +P = -2052 | |
| 5 | +User password = | |
| 6 | +Encryption key = 35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020 | |
| 7 | +extract for accessibility: allowed | |
| 8 | +extract for any purpose: allowed | |
| 9 | +print low resolution: allowed | |
| 10 | +print high resolution: not allowed | |
| 11 | +modify document assembly: allowed | |
| 12 | +modify forms: allowed | |
| 13 | +modify annotations: allowed | |
| 14 | +modify other: allowed | |
| 15 | +modify anything: allowed | |
| 16 | +stream encryption method: AESv3 | |
| 17 | +string encryption method: AESv3 | |
| 18 | +file encryption method: AESv3 | |
| 19 | +File is not linearized | |
| 20 | +No syntax or stream encoding errors found; the file may still contain | |
| 21 | +errors that qpdf cannot detect | ... | ... |
qpdf/qtest/qpdf/c-r5-key-user.out
0 โ 100644
| 1 | +checking c-r5-in.pdf | |
| 2 | +PDF Version: 1.7 extension level 3 | |
| 3 | +R = 5 | |
| 4 | +P = -2052 | |
| 5 | +User password = user3 | |
| 6 | +Encryption key = 35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020 | |
| 7 | +extract for accessibility: allowed | |
| 8 | +extract for any purpose: allowed | |
| 9 | +print low resolution: allowed | |
| 10 | +print high resolution: not allowed | |
| 11 | +modify document assembly: allowed | |
| 12 | +modify forms: allowed | |
| 13 | +modify annotations: allowed | |
| 14 | +modify other: allowed | |
| 15 | +modify anything: allowed | |
| 16 | +stream encryption method: AESv3 | |
| 17 | +string encryption method: AESv3 | |
| 18 | +file encryption method: AESv3 | |
| 19 | +File is not linearized | |
| 20 | +No syntax or stream encoding errors found; the file may still contain | |
| 21 | +errors that qpdf cannot detect | ... | ... |
qpdf/qtest/qpdf/long-id-check.out