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