From 569d74d36ba287b6951687ee1bdea45ae19091f8 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sun, 14 Jan 2018 10:17:17 -0500 Subject: [PATCH] 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 for the idea and contribution of one implementation of this idea. --- ChangeLog | 13 ++++++++++ TODO | 4 --- include/qpdf/QPDF.hh | 19 ++++++++++++++- libqpdf/QPDF.cc | 7 ++++++ libqpdf/QPDF_encryption.cc | 14 ++++++++--- qpdf/qpdf.cc | 39 ++++++++++++++++++++++++++---- qpdf/qtest/qpdf.test | 17 ++++++++++++- qpdf/qtest/qpdf/c-r5-key-hex.out | 21 ++++++++++++++++ qpdf/qtest/qpdf/c-r5-key-owner.out | 21 ++++++++++++++++ qpdf/qtest/qpdf/c-r5-key-user.out | 21 ++++++++++++++++ qpdf/qtest/qpdf/long-id-check.out | 1 + qpdf/qtest/qpdf/short-id-check.out | 1 + 12 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 qpdf/qtest/qpdf/c-r5-key-hex.out create mode 100644 qpdf/qtest/qpdf/c-r5-key-owner.out create mode 100644 qpdf/qtest/qpdf/c-r5-key-user.out diff --git a/ChangeLog b/ChangeLog index 76cbef33..eb7470d9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2018-01-14 Jay Berkenbilt + + * Allow raw encryption key to be specified in libary and command + line with the QPDF::setPasswordIsHexKey method and + --password-is-hex-key option. Allow encryption key to be displayed + with --show-encryption-key option. Thanks to Didier Stevens + for the idea and contribution of one + implementation of this idea. See his blog post at + https://blog.didierstevens.com/2017/12/28/cracking-encrypted-pdfs-part-3/ + for a discussion of using this for cracking encrypted PDFs. I hope + that a future release of qpdf will include some additional + recovery options that may also make use of this capability. + 2018-01-13 Jay Berkenbilt * Fix lexical error: the PDF specification allows floating point diff --git a/TODO b/TODO index 33d9fa01..3399a0d2 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,6 @@ Soon ==== - * Take changes on encryption-keys branch and make them usable. - Replace the hex encoding and decoding piece, and come up with a - more robust way of specifying the key. - * Consider whether there should be a mode in which QPDFObjectHandle returns nulls for operations on the wrong type instead of asserting the type. The way things are wired up now, this would have to be a diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index d82af11e..70bfac3e 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -64,7 +64,11 @@ class QPDF // those that set parameters. If the input file is not // encrypted,either a null password or an empty password can be // used. If the file is encrypted, either the user password or - // the owner password may be supplied. + // the owner password may be supplied. The method + // setPasswordIsHexKey may be called prior to calling this method + // or any of the other process methods to force the password to be + // interpreted as a raw encryption key. See comments on + // setPasswordIsHexKey for more information. QPDF_DLL void processFile(char const* filename, char const* password = 0); @@ -94,6 +98,18 @@ class QPDF void processInputSource(PointerHolder, char const* password = 0); + // For certain forensic or investigatory purposes, it may + // sometimes be useful to specify the encryption key directly, + // even though regular PDF applications do not provide a way to do + // this. calling setPasswordIsHexKey(true) before calling any of + // the process methods will bypass the normal encryption key + // computation or recovery mechanisms and interpret the bytes in + // the password as a hex-encoded encryption key. Note that we + // hex-encode the key because it may contain null bytes and + // therefore can't be represented in a char const*. + QPDF_DLL + void setPasswordIsHexKey(bool); + // Create a QPDF object for an empty PDF. This PDF has no pages // or objects other than a minimal trailer, a document catalog, // and a /Pages tree containing zero pages. Pages and other @@ -1145,6 +1161,7 @@ class QPDF QPDFTokenizer tokenizer; PointerHolder file; std::string last_object_description; + bool provided_password_is_hex_key; bool encrypted; bool encryption_initialized; bool ignore_xref_streams; diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index 33847a45..51a87d66 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -75,6 +75,7 @@ QPDF::QPDFVersion() } QPDF::Members::Members() : + provided_password_is_hex_key(false), encrypted(false), encryption_initialized(false), ignore_xref_streams(false), @@ -171,6 +172,12 @@ QPDF::processInputSource(PointerHolder source, parse(password); } +void +QPDF::setPasswordIsHexKey(bool val) +{ + this->m->provided_password_is_hex_key = val; +} + void QPDF::emptyPDF() { diff --git a/libqpdf/QPDF_encryption.cc b/libqpdf/QPDF_encryption.cc index a2445b61..fd717c35 100644 --- a/libqpdf/QPDF_encryption.cc +++ b/libqpdf/QPDF_encryption.cc @@ -1007,8 +1007,12 @@ QPDF::initializeEncryption() EncryptionData data(V, R, Length / 8, P, O, U, OE, UE, Perms, id1, this->m->encrypt_metadata); - if (check_owner_password( - this->m->user_password, this->m->provided_password, data)) + if (this->m->provided_password_is_hex_key) + { + // ignore passwords in file + } + else if (check_owner_password( + this->m->user_password, this->m->provided_password, data)) { // password supplied was owner password; user_password has // been initialized for V < 5 @@ -1023,7 +1027,11 @@ QPDF::initializeEncryption() "", 0, "invalid password"); } - if (V < 5) + if (this->m->provided_password_is_hex_key) + { + this->m->encryption_key = QUtil::hex_decode(this->m->provided_password); + } + else if (V < 5) { // For V < 5, the user password is encrypted with the owner // password, and the user password is always used for diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 01debed6..183d6d19 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -61,6 +61,7 @@ struct Options encryption_file(0), encryption_file_password(0), encrypt(false), + password_is_hex_key(false), keylen(0), r2_print(true), r2_modify(true), @@ -95,6 +96,7 @@ struct Options static_aes_iv(false), suppress_original_object_id(false), show_encryption(false), + show_encryption_key(false), check_linearization(false), show_linearization(false), show_xref(false), @@ -120,6 +122,7 @@ struct Options char const* encryption_file; char const* encryption_file_password; bool encrypt; + bool password_is_hex_key; std::string user_password; std::string owner_password; int keylen; @@ -158,6 +161,7 @@ struct Options bool static_aes_iv; bool suppress_original_object_id; bool show_encryption; + bool show_encryption_key; bool check_linearization; bool show_linearization; bool show_xref; @@ -227,6 +231,7 @@ Basic Options\n\ parameters are being copied\n\ --encrypt options -- generate an encrypted file\n\ --decrypt remove any encryption on the file\n\ +--password-is-hex-key treat primary password option as a hex-encoded key\n\ --pages options -- select specific pages from one or more files\n\ --rotate=[+|-]angle:page-range\n\ 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\ if the user password is used to open the other file. This works even if\n\ the owner password is not known.\n\ \n\ +The --password-is-hex-key option overrides the normal computation of\n\ +encryption keys. It only applies to the password used to open the main\n\ +file. This option is not ordinarily useful but can be helpful for forensic\n\ +or investigatory purposes. See manual for further discussion.\n\ +\n\ The --rotate flag can be used to specify pages to rotate pages either\n\ 90, 180, or 270 degrees. The page range is specified in the same\n\ 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\ This is option is not secure! FOR TESTING ONLY!\n\ --no-original-object-ids suppress original object ID comments in qdf mode\n\ --show-encryption quickly show encryption parameters\n\ +--show-encryption-key when showing encryption, reveal the actual key\n\ --check-linearization check file integrity and linearization status\n\ --show-linearization check and show all linearization data\n\ --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) return result; } -static void show_encryption(QPDF& pdf) +static void show_encryption(QPDF& pdf, Options& o) { // Extract /P from /Encrypt int R = 0; @@ -520,8 +531,14 @@ static void show_encryption(QPDF& pdf) std::cout << "R = " << R << std::endl; std::cout << "P = " << P << std::endl; std::string user_password = pdf.getTrimmedUserPassword(); - std::cout << "User password = " << user_password << std::endl - << "extract for accessibility: " + std::string encryption_key = pdf.getEncryptionKey(); + std::cout << "User password = " << user_password << std::endl; + if (o.show_encryption_key) + { + std::cout << "Encryption key = " + << QUtil::hex_encode(encryption_key) << std::endl; + } + std::cout << "extract for accessibility: " << show_bool(pdf.allowAccessibility()) << std::endl << "extract for any purpose: " << show_bool(pdf.allowExtractAll()) << std::endl @@ -1339,6 +1356,10 @@ static void parse_options(int argc, char* argv[], Options& o) o.encrypt = false; o.copy_encryption = false; } + else if (strcmp(arg, "password-is-hex-key") == 0) + { + o.password_is_hex_key = true; + } else if (strcmp(arg, "copy-encryption") == 0) { if (parameter == 0) @@ -1559,6 +1580,10 @@ static void parse_options(int argc, char* argv[], Options& o) o.show_encryption = true; o.require_outfile = false; } + else if (strcmp(arg, "show-encryption-key") == 0) + { + o.show_encryption_key = true; + } else if (strcmp(arg, "check-linearization") == 0) { o.check_linearization = true; @@ -1673,6 +1698,10 @@ static void set_qpdf_options(QPDF& pdf, Options& o) { pdf.setAttemptRecovery(false); } + if (o.password_is_hex_key) + { + pdf.setPasswordIsHexKey(true); + } } 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) << pdf.getExtensionLevel(); } std::cout << std::endl; - show_encryption(pdf); + show_encryption(pdf, o); if (pdf.isLinearized()) { std::cout << "File is linearized\n"; @@ -1877,7 +1906,7 @@ static void do_inspection(QPDF& pdf, Options& o) } if (o.show_encryption) { - show_encryption(pdf); + show_encryption(pdf, o); } if (o.check_linearization) { diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index b6e9905b..ae070735 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -314,7 +314,7 @@ foreach my $file (qw(short-id long-id)) $td->NORMALIZE_NEWLINES); $td->runtest("check $file.pdf", - {$td->COMMAND => "qpdf --check a.pdf"}, + {$td->COMMAND => "qpdf --check --show-encryption-key a.pdf"}, {$td->FILE => "$file-check.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); @@ -2244,6 +2244,21 @@ $td->runtest("copy of unfilterable with crypt", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Raw encryption key +my @enc_key = (['user', '--password=user3'], + ['owner', '--password=owner3'], + ['hex', '--password-is-hex-key --password=35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020']); +$n_tests += scalar(@enc_key); +foreach my $d (@enc_key) +{ + my ($description, $pass) = @$d; + $td->runtest("use/show encryption key ($description)", + {$td->COMMAND => + "qpdf --check --show-encryption-key c-r5-in.pdf $pass"}, + {$td->FILE => "c-r5-key-$description.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +} + show_ntests(); # ---------- $td->notify("--- Content Preservation Tests ---"); diff --git a/qpdf/qtest/qpdf/c-r5-key-hex.out b/qpdf/qtest/qpdf/c-r5-key-hex.out new file mode 100644 index 00000000..b905595e --- /dev/null +++ b/qpdf/qtest/qpdf/c-r5-key-hex.out @@ -0,0 +1,21 @@ +checking c-r5-in.pdf +PDF Version: 1.7 extension level 3 +R = 5 +P = -2052 +User password = +Encryption key = 35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020 +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: not allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv3 +string encryption method: AESv3 +file encryption method: AESv3 +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect diff --git a/qpdf/qtest/qpdf/c-r5-key-owner.out b/qpdf/qtest/qpdf/c-r5-key-owner.out new file mode 100644 index 00000000..b905595e --- /dev/null +++ b/qpdf/qtest/qpdf/c-r5-key-owner.out @@ -0,0 +1,21 @@ +checking c-r5-in.pdf +PDF Version: 1.7 extension level 3 +R = 5 +P = -2052 +User password = +Encryption key = 35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020 +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: not allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv3 +string encryption method: AESv3 +file encryption method: AESv3 +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect diff --git a/qpdf/qtest/qpdf/c-r5-key-user.out b/qpdf/qtest/qpdf/c-r5-key-user.out new file mode 100644 index 00000000..ffc38a2e --- /dev/null +++ b/qpdf/qtest/qpdf/c-r5-key-user.out @@ -0,0 +1,21 @@ +checking c-r5-in.pdf +PDF Version: 1.7 extension level 3 +R = 5 +P = -2052 +User password = user3 +Encryption key = 35ea16a48b6a3045133b69ac0906c2e8fb0a2cc97903ae17b51a5786ebdba020 +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: not allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv3 +string encryption method: AESv3 +file encryption method: AESv3 +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect diff --git a/qpdf/qtest/qpdf/long-id-check.out b/qpdf/qtest/qpdf/long-id-check.out index e2ffcef6..a1dca673 100644 --- a/qpdf/qtest/qpdf/long-id-check.out +++ b/qpdf/qtest/qpdf/long-id-check.out @@ -3,6 +3,7 @@ PDF Version: 1.3 R = 2 P = -4 User password = +Encryption key = 2f382cf6e1 extract for accessibility: allowed extract for any purpose: allowed print low resolution: allowed diff --git a/qpdf/qtest/qpdf/short-id-check.out b/qpdf/qtest/qpdf/short-id-check.out index e2ffcef6..2451b2e2 100644 --- a/qpdf/qtest/qpdf/short-id-check.out +++ b/qpdf/qtest/qpdf/short-id-check.out @@ -3,6 +3,7 @@ PDF Version: 1.3 R = 2 P = -4 User password = +Encryption key = 897d768fbd extract for accessibility: allowed extract for any purpose: allowed print low resolution: allowed