Commit 569d74d36ba287b6951687ee1bdea45ae19091f8

Authored by Jay Berkenbilt
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.
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
... ...
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&lt;InputSource&gt; 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&amp; 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&amp; 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&amp; 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&amp; pdf, Options&amp; 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&amp; pdf, Options&amp; o, int&amp; 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&amp; pdf, Options&amp; 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-&gt;runtest(&quot;copy of unfilterable with crypt&quot;,
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
... ... @@ -3,6 +3,7 @@ PDF Version: 1.3
3 3 R = 2
4 4 P = -4
5 5 User password =
  6 +Encryption key = 2f382cf6e1
6 7 extract for accessibility: allowed
7 8 extract for any purpose: allowed
8 9 print low resolution: allowed
... ...
qpdf/qtest/qpdf/short-id-check.out
... ... @@ -3,6 +3,7 @@ PDF Version: 1.3
3 3 R = 2
4 4 P = -4
5 5 User password =
  6 +Encryption key = 897d768fbd
6 7 extract for accessibility: allowed
7 8 extract for any purpose: allowed
8 9 print low resolution: allowed
... ...