From a5d50b8ec6a80cdb4e054256119ac2ae5ac601f2 Mon Sep 17 00:00:00 2001 From: Thomas Fussell Date: Mon, 10 Oct 2016 07:28:49 -0400 Subject: [PATCH] commit in-progress work for reading/writing password protected workbooks, #69 (not working yet) --- .gitmodules | 4 + CMakeLists.txt | 61 +-- README.md | 7 +- cmake/xlnt.cmake | 31 +- include/xlnt/utils/exceptions.hpp | 9 + include/xlnt/utils/optional.hpp | 3 +- include/xlnt/workbook/workbook.hpp | 12 + source/detail/xlsx_consumer.cpp | 1 - source/detail/xlsx_consumer.hpp | 8 + source/detail/xlsx_crypto.cpp | 469 ++++++++++++++++++++ source/detail/xlsx_producer.cpp | 2 +- source/packaging/tests/test_zip_file.hpp | 1 + source/packaging/zip_file.cpp | 6 +- source/utils/exceptions.cpp | 5 + source/utils/path.cpp | 1 - source/workbook/tests/test_consume_xlsx.hpp | 12 +- source/workbook/workbook.cpp | 31 +- tests/data/14_encrypted.xlsx | Bin 0 -> 14848 bytes 18 files changed, 600 insertions(+), 63 deletions(-) create mode 100644 source/detail/xlsx_crypto.cpp create mode 100644 tests/data/14_encrypted.xlsx diff --git a/.gitmodules b/.gitmodules index 66e9825a..1f7674df 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,9 +17,13 @@ path = third-party/pugixml url = https://github.com/zeux/pugixml branch = master + [submodule "third-party/botan"] path = third-party/botan url = https://github.com/randombit/botan + branch = master + [submodule "third-party/pole"] path = third-party/pole url = https://github.com/catlan/pole + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt index 82132b0d..78ac029e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,58 +1,31 @@ -cmake_minimum_required(VERSION 2.8.7) - -if(NOT DEFINED CMAKE_SUPPRESS_DEVELOPER_WARNINGS) - set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE INTERNAL "No dev warnings") -endif() +cmake_minimum_required(VERSION 3.1) project(xlnt) option(SHARED "Set to OFF to not build shared libraries" ON) option(STATIC "Set to ON to build static libraries" OFF) -option(DEBUG "Set to ON to for debug configuration" OFF) -option(EXAMPLES "Build examples" OFF) -option(TESTS "Build tests" OFF) -option(BENCHMARKS "Build performance benchmarks" OFF) option(COVERAGE "Generate coverage data for use in Coveralls" OFF) +option(WITH_EXAMPLES "Build examples" OFF) +option(WITH_TESTS "Build tests" OFF) +option(WITH_BENCHMARKS "Build performance benchmarks" OFF) +option(WITH_CRYPTO "Set to ON to be able to read and write password-protected workbooks; requires botan and pole libraries" OFF) + if(APPLE) - option(FRAMEWORK "Set to ON to package dylib and headers into a .framework, OSX only" OFF) + option(FRAMEWORK "Set to ON to package dylib and headers into a .framework, OSX only" OFF) + execute_process(COMMAND "sw_vers -productVersion | awk -F'.' '{print $1\".\"$2}'" + OUTPUT_VARIABLE OSX_VERSION) + set(CMAKE_OSX_DEPLOYMENT_TARGET ${OSX_VERSION}) endif() if(COVERAGE) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") endif() -if(${CMAKE_GENERATOR} STREQUAL "Unix Makefiles") - if(DEBUG) - set(CMAKE_BUILD_TYPE "Debug") - else() - set(CMAKE_BUILD_TYPE "Release") - endif() -endif() +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(APPLE) - execute_process(COMMAND "sw_vers -productVersion | awk -F'.' '{print $1\".\"$2}'" - OUTPUT_VARIABLE OSX_VERSION - ) - set(CMAKE_OSX_DEPLOYMENT_TARGET ${OSX_VERSION}) -endif(APPLE) - -if(CMAKE_VERSION VERSION_LESS 3.1) - if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") - if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.5) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y") - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") - endif() - elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") - endif() -else() - set(CMAKE_CXX_STANDARD 14) - set(CMAKE_CXX_STANDARD_REQUIRED ON) -endif() - -if(${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC") +if(MSVC) add_definitions(-DUNICODE -D_UNICODE) endif() @@ -65,10 +38,10 @@ foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/bin) -endforeach(OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES) +endforeach() -if(TESTS) - include(cmake/xlnt.test.cmake) +if(WITH_TESTS) + include(cmake/xlnt.test.cmake) endif() include(cmake/xlnt.cmake) diff --git a/README.md b/README.md index c25801fe..c7572722 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The resulting library, libxlnt.dylib or libxlnt.so or xlnt.dll, would be found i ```bash mkdir build cd build -cmake -D TESTS=1 -D SHARED=0 -D STATIC=1 -G Xcode .. +cmake -D WITH_TESTS=1 -D SHARED=0 -D STATIC=1 -G Xcode .. cmake --build . bin/xlnt.test ``` @@ -73,6 +73,9 @@ xlnt uses the following libraries, which are included in the source tree (all bu - [libstudxml v1.1.0](http://www.codesynthesis.com/projects/libstudxml/) (MIT license) - [utfcpp v2.3.4](http://utfcpp.sourceforge.net/) (Boost Software License, Version 1.0) - [cxxtest v4.4](http://cxxtest.com/) (LGPLv3 license [only used for testing, separate from main library assembly]) +- [pugixml v1.7](http://cxxtest.com/) (LGPLv3 license [only used for testing, separate from main library assembly]) +- [botan v1.11](https://botan.randombit.net/) (BSD license [only used for reading/writing password-protected workbooks, WITH_CRYPTO=1]) +- [pole v0.5](https://github.com/catlan/pole) (BSD license [only used for reading/writing password-protected workbooks, WITH_CRYPTO=1]) Initialize the submodules with this command: ```bash @@ -81,7 +84,7 @@ git submodule update --init ## Documentation -More extensive documentation with examples can be found on [Read The Docs](http://xlnt.readthedocs.org/en/latest/). +More detailed documentation with examples can be found on [Read The Docs](http://xlnt.readthedocs.org/en/latest/) (warning: this is somewhat out of date at the moment). ## License xlnt is currently released to the public for free under the terms of the MIT License: diff --git a/cmake/xlnt.cmake b/cmake/xlnt.cmake index b6bae9aa..ddc8d7f9 100644 --- a/cmake/xlnt.cmake +++ b/cmake/xlnt.cmake @@ -24,6 +24,13 @@ if(NOT BIN_DEST_DIR) set(BIN_DEST_DIR ${CMAKE_INSTALL_PREFIX}/bin) endif() +if(WITH_CRYPTO) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCRYPTO_ENABLED -DBOTAN_DLL= -D_ITERATOR_DEBUG_LEVEL=0") + if(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj -DNOMINMAX") + endif() +endif() + include_directories(include) include_directories(include/xlnt) include_directories(source) @@ -31,6 +38,8 @@ include_directories(source/detail) include_directories(third-party/miniz) include_directories(third-party/libstudxml) include_directories(third-party/utfcpp/source) +include_directories(third-party/botan) +include_directories(third-party/pole) FILE(GLOB ROOT_HEADERS include/xlnt/*.hpp) FILE(GLOB CELL_HEADERS include/xlnt/cell/*.hpp) @@ -61,17 +70,29 @@ FILE(GLOB WORKBOOK_SOURCES source/workbook/*.cpp) FILE(GLOB WORKSHEET_SOURCES source/worksheet/*.cpp) FILE(GLOB DETAIL_SOURCES source/detail/*.cpp) -SET(SOURCES ${CELL_SOURCES} ${CHARTS_SOURCES} ${CHARTSHEET_SOURCES} ${DRAWING_SOURCES} ${FORMULA_SOURCES} ${PACKAGING_SOURCES} ${SERIALIZATION_SOURCES} ${STYLES_SOURCES} ${UTILS_SOURCES} ${WORKBOOK_SOURCES} ${WORKSHEET_SOURCES} ${DETAIL_SOURCES}) +set(SOURCES ${CELL_SOURCES} ${CHARTS_SOURCES} ${CHARTSHEET_SOURCES} ${DRAWING_SOURCES} ${FORMULA_SOURCES} ${PACKAGING_SOURCES} ${SERIALIZATION_SOURCES} ${STYLES_SOURCES} ${UTILS_SOURCES} ${WORKBOOK_SOURCES} ${WORKSHEET_SOURCES} ${DETAIL_SOURCES}) -SET(MINIZ ../third-party/miniz/miniz.c ../third-party/miniz/miniz.h) -SET(LIBSTUDXML ../third-party/libstudxml/xml/parser.cxx ../third-party/libstudxml/xml/qname.cxx ../third-party/libstudxml/xml/serializer.cxx ../third-party/libstudxml/xml/value-traits.cxx ../third-party/libstudxml/xml/details/expat/xmlparse.c ../third-party/libstudxml/xml/details/expat/xmlrole.c ../third-party/libstudxml/xml/details/expat/xmltok_impl.c ../third-party/libstudxml/xml/details/expat/xmltok_ns.c ../third-party/libstudxml/xml/details/expat/xmltok.c ../third-party/libstudxml/xml/details/genx/char-props.c ../third-party/libstudxml/xml/details/genx/genx.c) +set(MINIZ ../third-party/miniz/miniz.c ../third-party/miniz/miniz.h) +set(LIBSTUDXML ../third-party/libstudxml/xml/parser.cxx ../third-party/libstudxml/xml/qname.cxx ../third-party/libstudxml/xml/serializer.cxx ../third-party/libstudxml/xml/value-traits.cxx ../third-party/libstudxml/xml/details/expat/xmlparse.c ../third-party/libstudxml/xml/details/expat/xmlrole.c ../third-party/libstudxml/xml/details/expat/xmltok_impl.c ../third-party/libstudxml/xml/details/expat/xmltok_ns.c ../third-party/libstudxml/xml/details/expat/xmltok.c ../third-party/libstudxml/xml/details/genx/char-props.c ../third-party/libstudxml/xml/details/genx/genx.c) +set(SOURCES ${SOURCES} ${MINIZ} ${LIBSTUDXML}) + +if(WITH_CRYPTO) + set(BOTAN ../third-party/botan/botan_all.cpp) + set(POLE ../third-party/pole/pole.cpp) + set(SOURCES ${SOURCES} ${BOTAN} ${POLE}) +endif() if(SHARED) - add_library(xlnt.shared SHARED ${HEADERS} ${SOURCES} ${MINIZ} ${LIBSTUDXML}) + add_library(xlnt.shared SHARED ${HEADERS} ${SOURCES} ${MINIZ} ${LIBSTUDXML} ${BOTAN} ${POLE}) target_compile_definitions(xlnt.shared PRIVATE XLNT_SHARED=1 LIBSTUDXML_STATIC_LIB=1) if(MSVC) target_compile_definitions(xlnt.shared PRIVATE XLNT_EXPORT=1 _CRT_SECURE_NO_WARNINGS=1) set_target_properties(xlnt.shared PROPERTIES COMPILE_FLAGS "/wd\"4251\" /wd\"4275\"") + if(WITH_CRYPTO) + set_target_properties(xlnt.shared PROPERTIES COMPILE_FLAGS "/wd\"4250\" /wd\"4251\" /wd\"4275\"") + else() + set_target_properties(xlnt.shared PROPERTIES COMPILE_FLAGS "/wd\"4251\" /wd\"4275\"") + endif() endif() install(TARGETS xlnt.shared LIBRARY DESTINATION ${LIB_DEST_DIR} @@ -101,7 +122,7 @@ if(SHARED) endif() if(STATIC) - add_library(xlnt.static STATIC ${HEADERS} ${SOURCES} ${MINIZ} ${LIBSTUDXML}) + add_library(xlnt.static STATIC ${HEADERS} ${SOURCES} ${MINIZ} ${LIBSTUDXML} ${BOTAN} ${POLE}) target_compile_definitions(xlnt.static PUBLIC XLNT_STATIC=1) target_compile_definitions(xlnt.static PRIVATE LIBSTUDXML_STATIC_LIB=1) if(MSVC) diff --git a/include/xlnt/utils/exceptions.hpp b/include/xlnt/utils/exceptions.hpp index bb50975d..02c82c74 100644 --- a/include/xlnt/utils/exceptions.hpp +++ b/include/xlnt/utils/exceptions.hpp @@ -155,4 +155,13 @@ public: unhandled_switch_case(); }; +/// +/// Exception for attempting to use a feature which is not supported +/// +class XLNT_CLASS unsupported : public exception +{ +public: + unsupported(const std::string &message); +}; + } // namespace xlnt diff --git a/include/xlnt/utils/optional.hpp b/include/xlnt/utils/optional.hpp index 6924be61..020a86e5 100644 --- a/include/xlnt/utils/optional.hpp +++ b/include/xlnt/utils/optional.hpp @@ -1,11 +1,12 @@ #pragma once +#include #include namespace xlnt { template -class optional +class XLNT_CLASS optional { public: optional() : has_value_(false), value_(T()) diff --git a/include/xlnt/workbook/workbook.hpp b/include/xlnt/workbook/workbook.hpp index e67a3c4f..de95b017 100644 --- a/include/xlnt/workbook/workbook.hpp +++ b/include/xlnt/workbook/workbook.hpp @@ -360,6 +360,18 @@ public: void load(const xlnt::path &filename); void load(std::istream &stream); +#ifdef CRYPTO_ENABLED + void save(const std::string &filename, const std::string &password); + void save(const xlnt::path &filename, const std::string &password); + void save(std::istream &stream, const std::string &password); + void save(const std::vector &data, const std::string &password); + + void load(const std::string &filename, const std::string &password); + void load(const xlnt::path &filename, const std::string &password); + void load(std::istream &stream, const std::string &password); + void load(const std::vector &data, const std::string &password); +#endif + bool has_view() const; workbook_view get_view() const; void set_view(const workbook_view &view); diff --git a/source/detail/xlsx_consumer.cpp b/source/detail/xlsx_consumer.cpp index 2294cf8a..fd815d06 100644 --- a/source/detail/xlsx_consumer.cpp +++ b/source/detail/xlsx_consumer.cpp @@ -1,7 +1,6 @@ #include #include - #include #include #include diff --git a/source/detail/xlsx_consumer.hpp b/source/detail/xlsx_consumer.hpp index 695487da..abce4e41 100644 --- a/source/detail/xlsx_consumer.hpp +++ b/source/detail/xlsx_consumer.hpp @@ -54,6 +54,14 @@ public: void read(const std::vector &source); +#ifdef CRYPTO_ENABLED + void read(const path &source, const std::string &password); + + void read(std::istream &source, const std::string &password); + + void read(const std::vector &source, const std::string &password); +#endif + private: /// /// Read all the files needed from the XLSX archive and initialize all of diff --git a/source/detail/xlsx_crypto.cpp b/source/detail/xlsx_crypto.cpp new file mode 100644 index 00000000..20fe6d38 --- /dev/null +++ b/source/detail/xlsx_crypto.cpp @@ -0,0 +1,469 @@ +#ifdef CRYPTO_ENABLED + +#include +#include +#include +#include + +#include +#include + +namespace xlnt { +namespace detail { + +enum class cipher_algorithm +{ + AES, + RC2, + RC4, + DES, + DESX, + TripleDES, + TripleDES_112 +}; + +enum class cipher_chaining +{ + ECB, + CBC, + CFB +}; + +enum class hash_algorithm +{ + SHA1, + SHA256, + SHA384, + SHA512, + MD5, + MD4, + MD2, + RIPEMD128, + RIPEMD160, + WHIRLPOOL +}; + +enum class encryption_algorithm +{ + aes_128, + aes_192, + aes_256, + sha_1, + sha_512 +}; + +struct agile_encryption_info +{ + // key data + struct + { + std::size_t salt_size; + std::size_t block_size; + std::size_t key_bits; + std::size_t hash_size; + std::string cipher_algorithm; + std::string cipher_chaining; + std::string hash_algorithm; + std::vector salt_value; + } key_data; + + struct + { + std::vector hmac_key; + std::vector hmac_value; + } data_integrity; + + struct + { + std::size_t spin_count; + std::size_t salt_size; + std::size_t block_size; + std::size_t key_bits; + std::size_t hash_size; + std::string cipher_algorithm; + std::string cipher_chaining; + std::string hash_algorithm; + std::vector salt_value; + std::vector verifier_hash_input; + std::vector verifier_hash_value; + std::vector encrypted_key_value; + } key_encryptor; +}; + +struct standard_encryption_info +{ + std::vector salt; +}; + +std::vector get_file(POLE::Storage &storage, const std::string &name) +{ + POLE::Stream stream(&storage, name.c_str()); + if (stream.fail()) return {}; + std::vector bytes; + + for (std::size_t i = 0; i < stream.size(); ++i) + { + bytes.push_back(stream.getch()); + } + + return bytes; +} + +template +void hash(const std::string &algorithm, InIter begin, InIter end, OutIter out) +{ + Botan::Pipe pipe(new Botan::Hash_Filter(algorithm)); + + pipe.start_msg(); + std::for_each(begin, end, [&pipe](std::uint8_t b) { pipe.write(b); }); + pipe.end_msg(); + + for (auto i : pipe.read_all()) + { + *(out++) = i; + } +} + +template +auto read_int(std::size_t &index, const std::vector &raw_data) +{ + auto result = *reinterpret_cast(&raw_data[index]); + index += sizeof(T); + return result; +}; + +Botan::SymmetricKey generate_standard_encryption_key(const std::vector &raw_data, + std::size_t offset, const std::string &password) +{ + auto header_length = read_int(offset, raw_data); + auto index_at_start = offset; + auto skip_flags = read_int(offset, raw_data); + auto size_extra = read_int(offset, raw_data); + auto alg_id = read_int(offset, raw_data); + auto alg_hash_id = read_int(offset, raw_data); + auto key_bits = read_int(offset, raw_data); + auto provider_type = read_int(offset, raw_data); + /*auto reserved1 = */read_int(offset, raw_data); + /*auto reserved2 = */read_int(offset, raw_data); + + const auto csp_name_length = header_length - (offset - index_at_start); + std::vector csp_name_wide( + reinterpret_cast(&*(raw_data.begin() + offset)), + reinterpret_cast(&*(raw_data.begin() + offset + csp_name_length))); + std::string csp_name(csp_name_wide.begin(), csp_name_wide.end()); + offset += csp_name_length; + + const auto salt_size = read_int(offset, raw_data); + std::vector salt(raw_data.begin() + offset, raw_data.begin() + offset + salt_size); + offset += salt_size; + + static const auto verifier_size = std::size_t(16); + std::vector verifier_hash_input(raw_data.begin() + offset, raw_data.begin() + offset + verifier_size); + offset += verifier_size; + + const auto verifier_hash_size = read_int(offset, raw_data); + std::vector verifier_hash_value(raw_data.begin() + offset, raw_data.begin() + offset + 32); + offset += verifier_hash_size; + + std::string hash_algorithm = "SHA-1"; + + const auto key_size = key_bits / 8; + const auto hash_out_size = hash_algorithm == "SHA-512" ? 64 : 20; + + auto salted_password = salt; + + for (auto c : password) + { + salted_password.push_back(c); + salted_password.push_back(0); + } + + std::vector hash_result(hash_out_size, 0); + hash(hash_algorithm, salted_password.begin(), salted_password.end(), hash_result.begin()); + + std::vector iterator_with_hash(4 + hash_out_size, 0); + std::copy(hash_result.begin(), hash_result.end(), iterator_with_hash.begin() + 4); + + std::uint32_t &iterator = *reinterpret_cast(iterator_with_hash.data()); + static const std::size_t spin_count = 50000; + + for (iterator = 0; iterator < spin_count; ++iterator) + { + hash(hash_algorithm, iterator_with_hash.begin(), iterator_with_hash.end(), hash_result.begin()); + } + + auto hash_with_block_key = hash_result; + std::vector block_key(4, 0); + hash_with_block_key.insert(hash_with_block_key.end(), block_key.begin(), block_key.end()); + hash(hash_algorithm, hash_with_block_key.begin(), hash_with_block_key.end(), hash_result.begin()); + + std::vector key = hash_result; + key.resize(key_size); + + for (std::size_t i = 0; i < key_size; ++i) + { + key[i] = static_cast(i < hash_out_size ? 0x36 ^ key[i] : 0x36); + } + + hash(hash_algorithm, key.begin(), key.end(), key.begin()); + + if (verifier_hash_value.size() <= key_size) + { + std::vector first_part(key.begin(), key.begin() + key_size); + + for (int i = 0; i < key.size(); ++i) + { + key[i] = static_cast(i < 20 ? 0x5C ^ key[i] : 0x5C); + } + + hash(hash_algorithm, key.begin(), key.end(), key.begin() + key_size); + std::copy(first_part.begin(), first_part.end(), key.begin()); + } + + key.resize(key_size, 0); + + //todo: verify here + + return Botan::SymmetricKey(key); +} + +Botan::SymmetricKey generate_agile_encryption_key(const std::vector &raw_data, + std::size_t offset, const std::string &password) +{ + static const auto xmlns = std::string("http://schemas.microsoft.com/office/2006/encryption"); + static const auto xmlns_p = std::string("http://schemas.microsoft.com/office/2006/keyEncryptor/password"); + static const auto xmlns_c = std::string("http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"); + + auto from_base64 = [](const std::string &s) + { + Botan::Pipe base64_pipe(new Botan::Base64_Decoder()); + base64_pipe.process_msg(s); + auto decoded = base64_pipe.read_all(); + return std::vector(decoded.begin(), decoded.end()); + }; + + agile_encryption_info result; + + xml::parser parser(raw_data.data() + offset, raw_data.size() - offset, "EncryptionInfo"); + + parser.next_expect(xml::parser::event_type::start_element, xmlns, "encryption"); + + parser.next_expect(xml::parser::event_type::start_element, xmlns, "keyData"); + result.key_data.salt_size = parser.attribute("saltSize"); + result.key_data.block_size = parser.attribute("blockSize"); + result.key_data.key_bits = parser.attribute("keyBits"); + result.key_data.hash_size = parser.attribute("hashSize"); + result.key_data.cipher_algorithm = parser.attribute("cipherAlgorithm"); + result.key_data.cipher_chaining = parser.attribute("cipherChaining"); + result.key_data.hash_algorithm = parser.attribute("hashAlgorithm"); + result.key_data.salt_value = from_base64(parser.attribute("saltValue")); + parser.next_expect(xml::parser::event_type::end_element, xmlns, "keyData"); + + parser.next_expect(xml::parser::event_type::start_element, xmlns, "dataIntegrity"); + result.data_integrity.hmac_key = from_base64(parser.attribute("encryptedHmacKey")); + result.data_integrity.hmac_value = from_base64(parser.attribute("encryptedHmacValue")); + parser.next_expect(xml::parser::event_type::end_element, xmlns, "dataIntegrity"); + + parser.next_expect(xml::parser::event_type::start_element, xmlns, "keyEncryptors"); + parser.next_expect(xml::parser::event_type::start_element, xmlns, "keyEncryptor"); + parser.attribute("uri"); + bool any_password_key = false; + + while (parser.peek() != xml::parser::event_type::end_element) + { + parser.next_expect(xml::parser::event_type::start_element); + + if (parser.namespace_() == xmlns_p && parser.name() == "encryptedKey") + { + any_password_key = true; + result.key_encryptor.spin_count = parser.attribute("spinCount"); + result.key_encryptor.salt_size = parser.attribute("saltSize"); + result.key_encryptor.block_size = parser.attribute("blockSize"); + result.key_encryptor.key_bits = parser.attribute("keyBits"); + result.key_encryptor.hash_size = parser.attribute("hashSize"); + result.key_encryptor.cipher_algorithm = parser.attribute("cipherAlgorithm"); + result.key_encryptor.cipher_chaining = parser.attribute("cipherChaining"); + result.key_encryptor.hash_algorithm = parser.attribute("hashAlgorithm"); + result.key_encryptor.salt_value = from_base64(parser.attribute("saltValue")); + result.key_encryptor.verifier_hash_input = from_base64(parser.attribute("encryptedVerifierHashInput")); + result.key_encryptor.verifier_hash_value = from_base64(parser.attribute("encryptedVerifierHashValue")); + result.key_encryptor.encrypted_key_value = from_base64(parser.attribute("encryptedKeyValue")); + } + else + { + throw "other encryption key types not supported"; + } + + parser.next_expect(xml::parser::event_type::end_element); + } + + if (!any_password_key) + { + throw "no password key in keyEncryptors"; + } + + parser.next_expect(xml::parser::event_type::end_element, xmlns, "keyEncryptor"); + parser.next_expect(xml::parser::event_type::end_element, xmlns, "keyEncryptors"); + + parser.next_expect(xml::parser::event_type::end_element, xmlns, "encryption"); + + const auto key_size = result.key_encryptor.key_bits / 8; + const auto hash_out_size = result.key_encryptor.hash_algorithm == "SHA512" ? 64 : 20; + + auto salted_password = result.key_encryptor.salt_value; + + for (auto c : password) + { + salted_password.push_back(c); + salted_password.push_back(0); + } + + std::vector hash_result(hash_out_size, 0); + hash(result.key_encryptor.hash_algorithm, salted_password.begin(), salted_password.end(), hash_result.begin()); + + std::vector iterator_with_hash(4 + hash_out_size, 0); + std::copy(hash_result.begin(), hash_result.end(), iterator_with_hash.begin() + 4); + + std::uint32_t &iterator = *reinterpret_cast(iterator_with_hash.data()); + + for (iterator = 0; iterator < result.key_encryptor.spin_count; ++iterator) + { + hash(result.key_encryptor.hash_algorithm, iterator_with_hash.begin(), iterator_with_hash.end(), hash_result.begin()); + } + + auto hash_with_block_key = hash_result; + std::vector block_key(4, 0); + hash_with_block_key.insert(hash_with_block_key.end(), block_key.begin(), block_key.end()); + hash(result.key_encryptor.hash_algorithm, hash_with_block_key.begin(), hash_with_block_key.end(), hash_result.begin()); + + std::vector key = hash_result; + key.resize(key_size); + + for (std::size_t i = 0; i < key_size; ++i) + { + key[i] = static_cast(i < hash_out_size ? 0x36 ^ key[i] : 0x36); + } + + hash(result.key_encryptor.hash_algorithm, key.begin(), key.end(), key.begin()); + + if (result.key_encryptor.verifier_hash_value.size() <= key_size) + { + std::vector first_part(key.begin(), key.begin() + key_size); + + for (int i = 0; i < key.size(); ++i) + { + key[i] = static_cast(i < 20 ? 0x5C ^ key[i] : 0x5C); + } + + hash(result.key_encryptor.hash_algorithm, key.begin(), key.end(), key.begin() + key_size); + std::copy(first_part.begin(), first_part.end(), key.begin()); + } + + key.resize(key_size, 0); + + //todo: verify here + + return Botan::SymmetricKey(key); +} + +Botan::SymmetricKey generate_encryption_key(const std::vector &raw_data, const std::string &password) +{ + std::size_t index = 0; + + auto version_major = read_int(index, raw_data); + auto version_minor = read_int(index, raw_data); + auto encryption_flags = read_int(index, raw_data); + + // version 4.4 is agile + if (version_major == 4 && version_minor == 4) + { + if (encryption_flags != 0x40) + { + throw "bad header"; + } + + return generate_agile_encryption_key(raw_data, index, password); + } + else + { + // not agile, only try to decrypt versions 3.2 and 4.2 + if (version_minor != 2 + || (version_major != 2 && version_major != 3 && version_major != 4)) + { + throw "unsupported encryption version"; + } + + if (encryption_flags & 0b00000011) // Reserved1 and Reserved2, MUST be 0 + { + throw "bad header"; + } + + if ((encryption_flags & 0b00000100) != 0 // fCryptoAPI + || (encryption_flags & 0b00010000) == 0) // fExternal + { + throw "extensible encryption is not supported"; + } + + if ((encryption_flags & 0b00100000) == 0) // fAES + { + throw "not an OOXML document"; + } + + return generate_standard_encryption_key(raw_data, index, password); + } +} + +std::vector decrypt_xlsx(const std::vector &bytes, const std::string &password) +{ + std::vector as_chars(bytes.begin(), bytes.end()); + POLE::Storage storage(as_chars.data(), static_cast(bytes.size())); + + if (!storage.open()) + { + throw "error"; + } + + auto key = generate_encryption_key(get_file(storage, "EncryptionInfo"), password); + auto encrypted_package = get_file(storage, "EncryptedPackage"); + auto size = *reinterpret_cast(encrypted_package.data()); + + Botan::InitializationVector iv; + auto cipher_name = std::string("AES-128/ECB/NoPadding"); + auto cipher = Botan::get_cipher(cipher_name, key, iv, Botan::DECRYPTION); + + Botan::Pipe pipe(cipher); + pipe.process_msg(encrypted_package.data() + 8, + 16 * static_cast(std::ceil(size / 16))); + Botan::secure_vector c1 = pipe.read_all(0); + + return std::vector(c1.begin(), c1.begin() + size); +} + +void xlsx_consumer::read(const std::vector &source, const std::string &password) +{ + destination_.clear(); + auto decrypted = decrypt_xlsx(source, password); + std::ofstream out("a.zip", std::ostream::binary); + for (auto b : decrypted) out << b; + out.close(); + source_.load(decrypted); + populate_workbook(); +} + +void xlsx_consumer::read(std::istream &source, const std::string &password) +{ + std::vector data((std::istreambuf_iterator(source)), + std::istreambuf_iterator()); + return read(data, password); +} + +void xlsx_consumer::read(const path &source, const std::string &password) +{ + std::ifstream file_stream(source.string(), std::iostream::binary); + return read(file_stream, password); +} + +} // namespace detail +} // namespace xlnt + +#endif diff --git a/source/detail/xlsx_producer.cpp b/source/detail/xlsx_producer.cpp index db9fe63e..78fdf7a2 100644 --- a/source/detail/xlsx_producer.cpp +++ b/source/detail/xlsx_producer.cpp @@ -402,7 +402,7 @@ void xlsx_producer::write_workbook(const relationship &rel) for (const auto ws : source_) { auto sheet_rel_id = source_.d_->sheet_title_rel_id_map_[ws.get_title()]; - auto sheet_rel = source_.d_->manifest_.get_relationship(rel.get_source().get_path(), sheet_rel_id); + auto sheet_rel = source_.d_->manifest_.get_relationship(rel.get_target().get_path(), sheet_rel_id); serializer().start_element(xmlns, "sheet"); serializer().attribute("name", ws.get_title()); diff --git a/source/packaging/tests/test_zip_file.hpp b/source/packaging/tests/test_zip_file.hpp index 0e7b9087..4e834c0d 100644 --- a/source/packaging/tests/test_zip_file.hpp +++ b/source/packaging/tests/test_zip_file.hpp @@ -1,5 +1,6 @@ #include #include +#include #include #include diff --git a/source/packaging/zip_file.cpp b/source/packaging/zip_file.cpp index 50ecbf11..6d33f271 100644 --- a/source/packaging/zip_file.cpp +++ b/source/packaging/zip_file.cpp @@ -153,7 +153,7 @@ zip_file::zip_file(std::istream &stream) : zip_file() load(stream); } -zip_file::zip_file(const std::vector &bytes) : zip_file() +zip_file::zip_file(const std::vector &bytes) : zip_file() { load(bytes); } @@ -204,7 +204,7 @@ void zip_file::load(const path &filename) start_read(); } -void zip_file::load(const std::vector &bytes) +void zip_file::load(const std::vector &bytes) { if (bytes.empty()) { @@ -245,7 +245,7 @@ void zip_file::save(std::ostream &stream) stream.write(buffer_.data(), static_cast(buffer_.size())); } -void zip_file::save(std::vector &bytes) +void zip_file::save(std::vector &bytes) { if (archive_->m_zip_mode == MZ_ZIP_MODE_WRITING) { diff --git a/source/utils/exceptions.cpp b/source/utils/exceptions.cpp index 0e147de9..18e0cc7a 100644 --- a/source/utils/exceptions.cpp +++ b/source/utils/exceptions.cpp @@ -93,4 +93,9 @@ no_visible_worksheets::no_visible_worksheets() { } +unsupported::unsupported(const std::string &message) + : exception(message) +{ +} + } // namespace xlnt diff --git a/source/utils/path.cpp b/source/utils/path.cpp index 8f7b3795..7dda5427 100644 --- a/source/utils/path.cpp +++ b/source/utils/path.cpp @@ -133,7 +133,6 @@ char path::system_separator() path::path() { - } path::path(const std::string &path_string) : internal_(path_string) diff --git a/source/workbook/tests/test_consume_xlsx.hpp b/source/workbook/tests/test_consume_xlsx.hpp index 6ab8db1e..ea1f1e36 100644 --- a/source/workbook/tests/test_consume_xlsx.hpp +++ b/source/workbook/tests/test_consume_xlsx.hpp @@ -5,10 +5,16 @@ #include #include -#include -#include -#include +#include class test_consume_xlsx : public CxxTest::TestSuite { +public: + void test_consume_password_protected() + { +#ifdef CRYPTO_ENABLED + xlnt::workbook wb; + wb.load(path_helper::get_data_directory("14_encrypted.xlsx"), "password"); +#endif + } }; diff --git a/source/workbook/workbook.cpp b/source/workbook/workbook.cpp index 96313fcb..6804602f 100644 --- a/source/workbook/workbook.cpp +++ b/source/workbook/workbook.cpp @@ -612,7 +612,7 @@ void workbook::load(std::istream &stream) consumer.read(stream); } -void workbook::load(const std::vector &data) +void workbook::load(const std::vector &data) { clear(); detail::xlsx_consumer consumer(*this); @@ -631,7 +631,34 @@ void workbook::load(const path &filename) consumer.read(filename); } -void workbook::save(std::vector &data) const +#ifdef CRYPTO_ENABLED +void workbook::load(const std::string &filename, const std::string &password) +{ + return load(path(filename), password); +} + +void workbook::load(const path &filename, const std::string &password) +{ + std::ifstream file_stream(filename.string(), std::iostream::binary); + return load(file_stream, password); +} + +void workbook::load(const std::vector &data, const std::string &password) +{ + clear(); + detail::xlsx_consumer consumer(*this); + consumer.read(data, password); +} + +void workbook::load(std::istream &stream, const std::string &password) +{ + clear(); + detail::xlsx_consumer consumer(*this); + consumer.read(stream, password); +} +#endif + +void workbook::save(std::vector &data) const { detail::xlsx_producer producer(*this); producer.write(data); diff --git a/tests/data/14_encrypted.xlsx b/tests/data/14_encrypted.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e6aa111775f3f4a3e97bd820f19356f5f70f11e5 GIT binary patch literal 14848 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 literal 0 HcmV?d00001