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 00000000..e6aa1117 Binary files /dev/null and b/tests/data/14_encrypted.xlsx differ