Commit e25910b59ab84c7aada0b651e7dbb47f6830ad3d
1 parent
c13bc66d
reading crypt filters is largely implemented but not fully tested
git-svn-id: svn+q:///qpdf/trunk@812 71b93d88-0707-0410-a8cf-f5a4172ac649
Showing
7 changed files
with
195 additions
and
26 deletions
include/qpdf/QPDF.hh
| ... | ... | @@ -87,6 +87,7 @@ class DLL_EXPORT QPDF |
| 87 | 87 | |
| 88 | 88 | // Encryption support |
| 89 | 89 | |
| 90 | + enum encryption_method_e { e_none, e_unknown, e_rc4, e_aes }; | |
| 90 | 91 | struct EncryptionData |
| 91 | 92 | { |
| 92 | 93 | // This class holds data read from the encryption dictionary. |
| ... | ... | @@ -397,6 +398,7 @@ class DLL_EXPORT QPDF |
| 397 | 398 | std::vector<QPDFObjectHandle>& result); |
| 398 | 399 | |
| 399 | 400 | // methods to support encryption -- implemented in QPDF_encryption.cc |
| 401 | + encryption_method_e interpretCF(QPDFObjectHandle); | |
| 400 | 402 | void initializeEncryption(); |
| 401 | 403 | std::string getKeyForObject(int objid, int generation, bool use_aes); |
| 402 | 404 | void decryptString(std::string&, int objid, int generation); |
| ... | ... | @@ -739,7 +741,10 @@ class DLL_EXPORT QPDF |
| 739 | 741 | bool attempt_recovery; |
| 740 | 742 | int encryption_V; |
| 741 | 743 | bool encrypt_metadata; |
| 742 | - QPDFObjectHandle encryption_dictionary; | |
| 744 | + std::map<std::string, encryption_method_e> crypt_filters; | |
| 745 | + encryption_method_e cf_stream; | |
| 746 | + encryption_method_e cf_string; | |
| 747 | + encryption_method_e cf_file; | |
| 743 | 748 | std::string provided_password; |
| 744 | 749 | std::string user_password; |
| 745 | 750 | std::string encryption_key; | ... | ... |
libqpdf/Pl_AES_PDF.cc
| ... | ... | @@ -9,7 +9,7 @@ |
| 9 | 9 | #include <stdlib.h> |
| 10 | 10 | |
| 11 | 11 | Pl_AES_PDF::Pl_AES_PDF(char const* identifier, Pipeline* next, |
| 12 | - bool encrypt, unsigned char key[key_size]) : | |
| 12 | + bool encrypt, unsigned char const key[key_size]) : | |
| 13 | 13 | Pipeline(identifier, next), |
| 14 | 14 | encrypt(encrypt), |
| 15 | 15 | cbc_mode(true), | ... | ... |
libqpdf/QPDF.cc
| ... | ... | @@ -255,6 +255,9 @@ QPDF::QPDF() : |
| 255 | 255 | attempt_recovery(true), |
| 256 | 256 | encryption_V(0), |
| 257 | 257 | encrypt_metadata(true), |
| 258 | + cf_stream(e_none), | |
| 259 | + cf_string(e_none), | |
| 260 | + cf_file(e_none), | |
| 258 | 261 | cached_key_objid(0), |
| 259 | 262 | cached_key_generation(0), |
| 260 | 263 | first_xref_item_offset(0), | ... | ... |
libqpdf/QPDF_Stream.cc
| ... | ... | @@ -136,6 +136,11 @@ QPDF_Stream::filterable(std::vector<std::string>& filters, |
| 136 | 136 | filterable = false; |
| 137 | 137 | } |
| 138 | 138 | } |
| 139 | + else if (key == "/Crypt") | |
| 140 | + { | |
| 141 | + // XXX untested | |
| 142 | + // we handle this in decryptStream | |
| 143 | + } | |
| 139 | 144 | else |
| 140 | 145 | { |
| 141 | 146 | filterable = false; |
| ... | ... | @@ -212,7 +217,8 @@ QPDF_Stream::filterable(std::vector<std::string>& filters, |
| 212 | 217 | iter != filters.end(); ++iter) |
| 213 | 218 | { |
| 214 | 219 | std::string const& filter = *iter; |
| 215 | - if (! ((filter == "/FlateDecode") || | |
| 220 | + if (! ((filter == "/Crypt") || | |
| 221 | + (filter == "/FlateDecode") || | |
| 216 | 222 | (filter == "/LZWDecode") || |
| 217 | 223 | (filter == "/ASCII85Decode") || |
| 218 | 224 | (filter == "/ASCIIHexDecode"))) |
| ... | ... | @@ -266,7 +272,11 @@ QPDF_Stream::pipeStreamData(Pipeline* pipeline, bool filter, |
| 266 | 272 | iter != filters.rend(); ++iter) |
| 267 | 273 | { |
| 268 | 274 | std::string const& filter = *iter; |
| 269 | - if (filter == "/FlateDecode") | |
| 275 | + if (filter == "/Crypt") | |
| 276 | + { | |
| 277 | + // Ignore -- handled by pipeStreamData | |
| 278 | + } | |
| 279 | + else if (filter == "/FlateDecode") | |
| 270 | 280 | { |
| 271 | 281 | if (predictor == 12) |
| 272 | 282 | { | ... | ... |
libqpdf/QPDF_encryption.cc
| ... | ... | @@ -9,6 +9,7 @@ |
| 9 | 9 | #include <qpdf/QUtil.hh> |
| 10 | 10 | #include <qpdf/Pl_RC4.hh> |
| 11 | 11 | #include <qpdf/Pl_AES_PDF.hh> |
| 12 | +#include <qpdf/Pl_Buffer.hh> | |
| 12 | 13 | #include <qpdf/RC4.hh> |
| 13 | 14 | #include <qpdf/MD5.hh> |
| 14 | 15 | |
| ... | ... | @@ -281,6 +282,27 @@ check_owner_password(std::string& user_password, |
| 281 | 282 | return result; |
| 282 | 283 | } |
| 283 | 284 | |
| 285 | +QPDF::encryption_method_e | |
| 286 | +QPDF::interpretCF(QPDFObjectHandle cf) | |
| 287 | +{ | |
| 288 | + if (cf.isName()) | |
| 289 | + { | |
| 290 | + std::string filter = cf.getName(); | |
| 291 | + if (this->crypt_filters.count(filter) != 0) | |
| 292 | + { | |
| 293 | + return this->crypt_filters[filter]; | |
| 294 | + } | |
| 295 | + else | |
| 296 | + { | |
| 297 | + return e_unknown; | |
| 298 | + } | |
| 299 | + } | |
| 300 | + else | |
| 301 | + { | |
| 302 | + return e_none; | |
| 303 | + } | |
| 304 | +} | |
| 305 | + | |
| 284 | 306 | void |
| 285 | 307 | QPDF::initializeEncryption() |
| 286 | 308 | { |
| ... | ... | @@ -322,8 +344,7 @@ QPDF::initializeEncryption() |
| 322 | 344 | "incorrect length"); |
| 323 | 345 | } |
| 324 | 346 | |
| 325 | - this->encryption_dictionary = this->trailer.getKey("/Encrypt"); | |
| 326 | - QPDFObjectHandle& encryption_dict = this->encryption_dictionary; | |
| 347 | + QPDFObjectHandle encryption_dict = this->trailer.getKey("/Encrypt"); | |
| 327 | 348 | if (! encryption_dict.isDictionary()) |
| 328 | 349 | { |
| 329 | 350 | throw QPDFExc(this->file.getName(), this->file.getLastOffset(), |
| ... | ... | @@ -336,6 +357,12 @@ QPDF::initializeEncryption() |
| 336 | 357 | throw QPDFExc(this->file.getName(), this->file.getLastOffset(), |
| 337 | 358 | "unsupported encryption filter"); |
| 338 | 359 | } |
| 360 | + if (! encryption_dict.getKey("/SubFilter").isNull()) | |
| 361 | + { | |
| 362 | + warn(QPDFExc(this->file.getName(), this->file.getLastOffset(), | |
| 363 | + "file uses encryption SubFilters," | |
| 364 | + " which qpdf does not support")); | |
| 365 | + } | |
| 339 | 366 | |
| 340 | 367 | if (! (encryption_dict.getKey("/V").isInteger() && |
| 341 | 368 | encryption_dict.getKey("/R").isInteger() && |
| ... | ... | @@ -388,10 +415,55 @@ QPDF::initializeEncryption() |
| 388 | 415 | encryption_dict.getKey("/EncryptMetadata").getBoolValue(); |
| 389 | 416 | } |
| 390 | 417 | |
| 391 | - // XXX warn if /SubFilter is present | |
| 392 | 418 | if (V == 4) |
| 393 | 419 | { |
| 394 | - // XXX get CF | |
| 420 | + QPDFObjectHandle CF = encryption_dict.getKey("/CF"); | |
| 421 | + std::set<std::string> keys = CF.getKeys(); | |
| 422 | + for (std::set<std::string>::iterator iter = keys.begin(); | |
| 423 | + iter != keys.end(); ++iter) | |
| 424 | + { | |
| 425 | + std::string const& filter = *iter; | |
| 426 | + QPDFObjectHandle cdict = CF.getKey(filter); | |
| 427 | + if (cdict.isDictionary()) | |
| 428 | + { | |
| 429 | + encryption_method_e method = e_none; | |
| 430 | + if (cdict.getKey("/CFM").isName()) | |
| 431 | + { | |
| 432 | + std::string method_name = cdict.getKey("/CFM").getName(); | |
| 433 | + if (method_name == "/V2") | |
| 434 | + { | |
| 435 | + // XXX coverage | |
| 436 | + method = e_rc4; | |
| 437 | + } | |
| 438 | + else if (method_name == "/AESV2") | |
| 439 | + { | |
| 440 | + // XXX coverage | |
| 441 | + method = e_aes; | |
| 442 | + } | |
| 443 | + else | |
| 444 | + { | |
| 445 | + // Don't complain now -- maybe we won't need | |
| 446 | + // to reference this type. | |
| 447 | + method = e_unknown; | |
| 448 | + } | |
| 449 | + } | |
| 450 | + this->crypt_filters[filter] = method; | |
| 451 | + } | |
| 452 | + } | |
| 453 | + | |
| 454 | + QPDFObjectHandle StmF = encryption_dict.getKey("/StmF"); | |
| 455 | + QPDFObjectHandle StrF = encryption_dict.getKey("/StrF"); | |
| 456 | + QPDFObjectHandle EFF = encryption_dict.getKey("/EFF"); | |
| 457 | + this->cf_stream = interpretCF(StmF); | |
| 458 | + this->cf_string = interpretCF(StrF); | |
| 459 | + if (EFF.isName()) | |
| 460 | + { | |
| 461 | + this->cf_file = interpretCF(EFF); | |
| 462 | + } | |
| 463 | + else | |
| 464 | + { | |
| 465 | + this->cf_file = this->cf_stream; | |
| 466 | + } | |
| 395 | 467 | } |
| 396 | 468 | EncryptionData data(V, R, Length / 8, P, O, U, id1, this->encrypt_metadata); |
| 397 | 469 | if (check_owner_password( |
| ... | ... | @@ -440,15 +512,50 @@ QPDF::decryptString(std::string& str, int objid, int generation) |
| 440 | 512 | { |
| 441 | 513 | return; |
| 442 | 514 | } |
| 443 | - bool use_aes = false; // XXX | |
| 515 | + bool use_aes = false; | |
| 516 | + if (this->encryption_V == 4) | |
| 517 | + { | |
| 518 | + switch (this->cf_string) | |
| 519 | + { | |
| 520 | + case e_none: | |
| 521 | + return; | |
| 522 | + | |
| 523 | + case e_aes: | |
| 524 | + use_aes = true; | |
| 525 | + break; | |
| 526 | + | |
| 527 | + case e_rc4: | |
| 528 | + break; | |
| 529 | + | |
| 530 | + default: | |
| 531 | + warn(QPDFExc(this->file.getName(), this->file.getLastOffset(), | |
| 532 | + "unknown encryption filter for strings" | |
| 533 | + " (check /StrF in /Encrypt dictionary);" | |
| 534 | + " strings may be decrypted improperly")); | |
| 535 | + // To avoid repeated warnings, reset cf_string. Assume | |
| 536 | + // we'd want to use AES if V == 4. | |
| 537 | + this->cf_string = e_aes; | |
| 538 | + break; | |
| 539 | + } | |
| 540 | + } | |
| 541 | + | |
| 444 | 542 | std::string key = getKeyForObject(objid, generation, use_aes); |
| 445 | 543 | if (use_aes) |
| 446 | 544 | { |
| 447 | - // XXX | |
| 448 | - throw std::logic_error("XXX"); | |
| 545 | + // XXX coverage | |
| 546 | + assert(key.length() == Pl_AES_PDF::key_size); | |
| 547 | + Pl_Buffer bufpl("decrypted string"); | |
| 548 | + Pl_AES_PDF pl("aes decrypt string", &bufpl, false, | |
| 549 | + (unsigned char const*)key.c_str()); | |
| 550 | + pl.write((unsigned char*)str.c_str(), str.length()); | |
| 551 | + pl.finish(); | |
| 552 | + Buffer* buf = bufpl.getBuffer(); | |
| 553 | + str = std::string((char*)buf->getBuffer(), (size_t)buf->getSize()); | |
| 554 | + delete buf; | |
| 449 | 555 | } |
| 450 | 556 | else |
| 451 | 557 | { |
| 558 | + QTC::TC("qpdf", "QPDF_encryption rc4 decode string"); | |
| 452 | 559 | unsigned int vlen = str.length(); |
| 453 | 560 | char* tmp = QUtil::copy_string(str); |
| 454 | 561 | RC4 rc4((unsigned char const*)key.c_str(), key.length()); |
| ... | ... | @@ -463,7 +570,6 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, |
| 463 | 570 | QPDFObjectHandle& stream_dict, |
| 464 | 571 | std::vector<PointerHolder<Pipeline> >& heap) |
| 465 | 572 | { |
| 466 | - bool decrypt = true; | |
| 467 | 573 | std::string type; |
| 468 | 574 | if (stream_dict.getKey("/Type").isName()) |
| 469 | 575 | { |
| ... | ... | @@ -471,34 +577,77 @@ QPDF::decryptStream(Pipeline*& pipeline, int objid, int generation, |
| 471 | 577 | } |
| 472 | 578 | if (type == "/XRef") |
| 473 | 579 | { |
| 474 | - QTC::TC("qpdf", "QPDF piping xref stream from encrypted file"); | |
| 475 | - decrypt = false; | |
| 580 | + QTC::TC("qpdf", "QPDF_encryption xref stream from encrypted file"); | |
| 581 | + return; | |
| 476 | 582 | } |
| 477 | 583 | bool use_aes = false; |
| 478 | 584 | if (this->encryption_V == 4) |
| 479 | 585 | { |
| 480 | - if ((! this->encrypt_metadata) && (type == "/Metadata")) | |
| 586 | + encryption_method_e method = e_unknown; | |
| 587 | + std::string method_source = "/StmF from /Encrypt dictionary"; | |
| 588 | + | |
| 589 | + if (stream_dict.getKey("/DecodeParms").isDictionary()) | |
| 481 | 590 | { |
| 482 | - // XXX no test case for this | |
| 483 | - decrypt = false; | |
| 591 | + QPDFObjectHandle decode_parms = stream_dict.getKey("/DecodeParms"); | |
| 592 | + if (decode_parms.getKey("/Crypt").isDictionary()) | |
| 593 | + { | |
| 594 | + // XXX coverage | |
| 595 | + QPDFObjectHandle crypt = decode_parms.getKey("/Crypt"); | |
| 596 | + method = interpretCF(crypt.getKey("/Name")); | |
| 597 | + method_source = "stream's Crypt decode parameters"; | |
| 598 | + } | |
| 484 | 599 | } |
| 485 | - // XXX check crypt filter; if not found, use StmF; see TODO | |
| 486 | - use_aes = true; // XXX | |
| 487 | - } | |
| 488 | - if (! decrypt) | |
| 489 | - { | |
| 490 | - return; | |
| 491 | - } | |
| 492 | 600 | |
| 601 | + if (method == e_unknown) | |
| 602 | + { | |
| 603 | + if ((! this->encrypt_metadata) && (type == "/Metadata")) | |
| 604 | + { | |
| 605 | + // XXX coverage | |
| 606 | + method = e_none; | |
| 607 | + } | |
| 608 | + else | |
| 609 | + { | |
| 610 | + method = this->cf_stream; | |
| 611 | + } | |
| 612 | + // XXX What about embedded file streams? | |
| 613 | + } | |
| 614 | + use_aes = false; | |
| 615 | + switch (this->cf_stream) | |
| 616 | + { | |
| 617 | + case e_none: | |
| 618 | + return; | |
| 619 | + break; | |
| 620 | + | |
| 621 | + case e_aes: | |
| 622 | + use_aes = true; | |
| 623 | + break; | |
| 624 | + | |
| 625 | + case e_rc4: | |
| 626 | + break; | |
| 627 | + | |
| 628 | + default: | |
| 629 | + // filter local to this stream. | |
| 630 | + warn(QPDFExc(this->file.getName(), this->file.getLastOffset(), | |
| 631 | + "unknown encryption filter for streams" | |
| 632 | + " (check " + method_source + ");" | |
| 633 | + " streams may be decrypted improperly")); | |
| 634 | + // To avoid repeated warnings, reset cf_stream. Assume | |
| 635 | + // we'd want to use AES if V == 4. | |
| 636 | + this->cf_stream = e_aes; | |
| 637 | + break; | |
| 638 | + } | |
| 639 | + } | |
| 493 | 640 | std::string key = getKeyForObject(objid, generation, use_aes); |
| 494 | 641 | if (use_aes) |
| 495 | 642 | { |
| 643 | + // XXX coverage | |
| 496 | 644 | assert(key.length() == Pl_AES_PDF::key_size); |
| 497 | 645 | pipeline = new Pl_AES_PDF("AES stream decryption", pipeline, |
| 498 | 646 | false, (unsigned char*) key.c_str()); |
| 499 | 647 | } |
| 500 | 648 | else |
| 501 | 649 | { |
| 650 | + QTC::TC("qpdf", "QPDF_encryption rc4 decode stream"); | |
| 502 | 651 | pipeline = new Pl_RC4("RC4 stream decryption", pipeline, |
| 503 | 652 | (unsigned char*) key.c_str(), key.length()); |
| 504 | 653 | } | ... | ... |
libqpdf/qpdf/Pl_AES_PDF.hh
| ... | ... | @@ -12,7 +12,7 @@ class DLL_EXPORT Pl_AES_PDF: public Pipeline |
| 12 | 12 | // key_data should be a pointer to key_size bytes of data |
| 13 | 13 | static unsigned int const key_size = 16; |
| 14 | 14 | Pl_AES_PDF(char const* identifier, Pipeline* next, |
| 15 | - bool encrypt, unsigned char key[key_size]); | |
| 15 | + bool encrypt, unsigned char const key[key_size]); | |
| 16 | 16 | virtual ~Pl_AES_PDF(); |
| 17 | 17 | |
| 18 | 18 | virtual void write(unsigned char* data, int len); | ... | ... |
qpdf/qpdf.testcov
| ... | ... | @@ -112,7 +112,7 @@ QPDF found wrong endstream in recovery 0 |
| 112 | 112 | QPDFObjectHandle indirect to unknown 0 |
| 113 | 113 | QPDF_Stream pipeStreamData with null pipeline 0 |
| 114 | 114 | QPDFWriter not recompressing /FlateDecode 0 |
| 115 | -QPDF piping xref stream from encrypted file 0 | |
| 115 | +QPDF_encryption xref stream from encrypted file 0 | |
| 116 | 116 | unable to filter 0 |
| 117 | 117 | QPDF_String non-trivial UTF-16 0 |
| 118 | 118 | QPDF xref overwrite object 0 |
| ... | ... | @@ -158,3 +158,5 @@ QPDFWriter using forced PDF version 0 |
| 158 | 158 | qpdf-c called qpdf_set_minimum_pdf_version 0 |
| 159 | 159 | qpdf-c called qpdf_force_pdf_version 0 |
| 160 | 160 | qpdf-c called qpdf_init_write multiple times 0 |
| 161 | +QPDF_encryption rc4 decode string 0 | |
| 162 | +QPDF_encryption rc4 decode stream 0 | ... | ... |