commit in-progress work for reading/writing password protected workbooks, #69 (not working yet)

This commit is contained in:
Thomas Fussell 2016-10-10 07:28:49 -04:00
parent eee47c267c
commit a5d50b8ec6
18 changed files with 600 additions and 63 deletions

4
.gitmodules vendored
View File

@ -17,9 +17,13 @@
path = third-party/pugixml path = third-party/pugixml
url = https://github.com/zeux/pugixml url = https://github.com/zeux/pugixml
branch = master branch = master
[submodule "third-party/botan"] [submodule "third-party/botan"]
path = third-party/botan path = third-party/botan
url = https://github.com/randombit/botan url = https://github.com/randombit/botan
branch = master
[submodule "third-party/pole"] [submodule "third-party/pole"]
path = third-party/pole path = third-party/pole
url = https://github.com/catlan/pole url = https://github.com/catlan/pole
branch = master

View File

@ -1,58 +1,31 @@
cmake_minimum_required(VERSION 2.8.7) cmake_minimum_required(VERSION 3.1)
if(NOT DEFINED CMAKE_SUPPRESS_DEVELOPER_WARNINGS)
set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE INTERNAL "No dev warnings")
endif()
project(xlnt) project(xlnt)
option(SHARED "Set to OFF to not build shared libraries" ON) option(SHARED "Set to OFF to not build shared libraries" ON)
option(STATIC "Set to ON to build static libraries" OFF) 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(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) 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() endif()
if(COVERAGE) 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() endif()
if(${CMAKE_GENERATOR} STREQUAL "Unix Makefiles") set(CMAKE_CXX_STANDARD 14)
if(DEBUG) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE "Debug")
else()
set(CMAKE_BUILD_TYPE "Release")
endif()
endif()
if(APPLE) if(MSVC)
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")
add_definitions(-DUNICODE -D_UNICODE) add_definitions(-DUNICODE -D_UNICODE)
endif() endif()
@ -65,10 +38,10 @@ foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_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) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/bin)
endforeach(OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES) endforeach()
if(TESTS) if(WITH_TESTS)
include(cmake/xlnt.test.cmake) include(cmake/xlnt.test.cmake)
endif() endif()
include(cmake/xlnt.cmake) include(cmake/xlnt.cmake)

View File

@ -62,7 +62,7 @@ The resulting library, libxlnt.dylib or libxlnt.so or xlnt.dll, would be found i
```bash ```bash
mkdir build mkdir build
cd 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 . cmake --build .
bin/xlnt.test 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) - [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) - [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]) - [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: Initialize the submodules with this command:
```bash ```bash
@ -81,7 +84,7 @@ git submodule update --init
## Documentation ## 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 ## License
xlnt is currently released to the public for free under the terms of the MIT License: xlnt is currently released to the public for free under the terms of the MIT License:

View File

@ -24,6 +24,13 @@ if(NOT BIN_DEST_DIR)
set(BIN_DEST_DIR ${CMAKE_INSTALL_PREFIX}/bin) set(BIN_DEST_DIR ${CMAKE_INSTALL_PREFIX}/bin)
endif() 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)
include_directories(include/xlnt) include_directories(include/xlnt)
include_directories(source) include_directories(source)
@ -31,6 +38,8 @@ include_directories(source/detail)
include_directories(third-party/miniz) include_directories(third-party/miniz)
include_directories(third-party/libstudxml) include_directories(third-party/libstudxml)
include_directories(third-party/utfcpp/source) 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 ROOT_HEADERS include/xlnt/*.hpp)
FILE(GLOB CELL_HEADERS include/xlnt/cell/*.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 WORKSHEET_SOURCES source/worksheet/*.cpp)
FILE(GLOB DETAIL_SOURCES source/detail/*.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(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(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) 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) target_compile_definitions(xlnt.shared PRIVATE XLNT_SHARED=1 LIBSTUDXML_STATIC_LIB=1)
if(MSVC) if(MSVC)
target_compile_definitions(xlnt.shared PRIVATE XLNT_EXPORT=1 _CRT_SECURE_NO_WARNINGS=1) 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\"") 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() endif()
install(TARGETS xlnt.shared install(TARGETS xlnt.shared
LIBRARY DESTINATION ${LIB_DEST_DIR} LIBRARY DESTINATION ${LIB_DEST_DIR}
@ -101,7 +122,7 @@ if(SHARED)
endif() endif()
if(STATIC) 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 PUBLIC XLNT_STATIC=1)
target_compile_definitions(xlnt.static PRIVATE LIBSTUDXML_STATIC_LIB=1) target_compile_definitions(xlnt.static PRIVATE LIBSTUDXML_STATIC_LIB=1)
if(MSVC) if(MSVC)

View File

@ -155,4 +155,13 @@ public:
unhandled_switch_case(); unhandled_switch_case();
}; };
/// <summary>
/// Exception for attempting to use a feature which is not supported
/// </summary>
class XLNT_CLASS unsupported : public exception
{
public:
unsupported(const std::string &message);
};
} // namespace xlnt } // namespace xlnt

View File

@ -1,11 +1,12 @@
#pragma once #pragma once
#include <xlnt/xlnt_config.hpp>
#include <xlnt/utils/exceptions.hpp> #include <xlnt/utils/exceptions.hpp>
namespace xlnt { namespace xlnt {
template<typename T> template<typename T>
class optional class XLNT_CLASS optional
{ {
public: public:
optional() : has_value_(false), value_(T()) optional() : has_value_(false), value_(T())

View File

@ -360,6 +360,18 @@ public:
void load(const xlnt::path &filename); void load(const xlnt::path &filename);
void load(std::istream &stream); 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<std::uint8_t> &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<std::uint8_t> &data, const std::string &password);
#endif
bool has_view() const; bool has_view() const;
workbook_view get_view() const; workbook_view get_view() const;
void set_view(const workbook_view &view); void set_view(const workbook_view &view);

View File

@ -1,7 +1,6 @@
#include <cctype> #include <cctype>
#include <detail/xlsx_consumer.hpp> #include <detail/xlsx_consumer.hpp>
#include <detail/constants.hpp> #include <detail/constants.hpp>
#include <detail/custom_value_traits.hpp> #include <detail/custom_value_traits.hpp>
#include <detail/workbook_impl.hpp> #include <detail/workbook_impl.hpp>

View File

@ -54,6 +54,14 @@ public:
void read(const std::vector<std::uint8_t> &source); void read(const std::vector<std::uint8_t> &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<std::uint8_t> &source, const std::string &password);
#endif
private: private:
/// <summary> /// <summary>
/// Read all the files needed from the XLSX archive and initialize all of /// Read all the files needed from the XLSX archive and initialize all of

View File

@ -0,0 +1,469 @@
#ifdef CRYPTO_ENABLED
#include <array>
#include <pole.h>
#include <botan_all.h>
#include <include_libstudxml.hpp>
#include <detail/xlsx_consumer.hpp>
#include <xlnt/workbook/workbook.hpp>
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<std::uint8_t> salt_value;
} key_data;
struct
{
std::vector<std::uint8_t> hmac_key;
std::vector<std::uint8_t> 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<std::uint8_t> salt_value;
std::vector<std::uint8_t> verifier_hash_input;
std::vector<std::uint8_t> verifier_hash_value;
std::vector<std::uint8_t> encrypted_key_value;
} key_encryptor;
};
struct standard_encryption_info
{
std::vector<std::uint8_t> salt;
};
std::vector<std::uint8_t> get_file(POLE::Storage &storage, const std::string &name)
{
POLE::Stream stream(&storage, name.c_str());
if (stream.fail()) return {};
std::vector<Botan::byte> bytes;
for (std::size_t i = 0; i < stream.size(); ++i)
{
bytes.push_back(stream.getch());
}
return bytes;
}
template<typename InIter, typename OutIter>
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<typename T>
auto read_int(std::size_t &index, const std::vector<std::uint8_t> &raw_data)
{
auto result = *reinterpret_cast<const T *>(&raw_data[index]);
index += sizeof(T);
return result;
};
Botan::SymmetricKey generate_standard_encryption_key(const std::vector<std::uint8_t> &raw_data,
std::size_t offset, const std::string &password)
{
auto header_length = read_int<std::uint32_t>(offset, raw_data);
auto index_at_start = offset;
auto skip_flags = read_int<std::uint32_t>(offset, raw_data);
auto size_extra = read_int<std::uint32_t>(offset, raw_data);
auto alg_id = read_int<std::uint32_t>(offset, raw_data);
auto alg_hash_id = read_int<std::uint32_t>(offset, raw_data);
auto key_bits = read_int<std::uint32_t>(offset, raw_data);
auto provider_type = read_int<std::uint32_t>(offset, raw_data);
/*auto reserved1 = */read_int<std::uint32_t>(offset, raw_data);
/*auto reserved2 = */read_int<std::uint32_t>(offset, raw_data);
const auto csp_name_length = header_length - (offset - index_at_start);
std::vector<std::uint16_t> csp_name_wide(
reinterpret_cast<const std::uint16_t *>(&*(raw_data.begin() + offset)),
reinterpret_cast<const std::uint16_t *>(&*(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<std::uint32_t>(offset, raw_data);
std::vector<std::uint8_t> 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<std::uint8_t> verifier_hash_input(raw_data.begin() + offset, raw_data.begin() + offset + verifier_size);
offset += verifier_size;
const auto verifier_hash_size = read_int<std::uint32_t>(offset, raw_data);
std::vector<std::uint8_t> 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<std::uint8_t> hash_result(hash_out_size, 0);
hash(hash_algorithm, salted_password.begin(), salted_password.end(), hash_result.begin());
std::vector<std::uint8_t> 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<std::uint32_t *>(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<std::uint8_t> 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<std::uint8_t> key = hash_result;
key.resize(key_size);
for (std::size_t i = 0; i < key_size; ++i)
{
key[i] = static_cast<std::uint8_t>(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<std::uint8_t> first_part(key.begin(), key.begin() + key_size);
for (int i = 0; i < key.size(); ++i)
{
key[i] = static_cast<std::uint8_t>(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<std::uint8_t> &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<std::uint8_t>(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<std::size_t>("saltSize");
result.key_data.block_size = parser.attribute<std::size_t>("blockSize");
result.key_data.key_bits = parser.attribute<std::size_t>("keyBits");
result.key_data.hash_size = parser.attribute<std::size_t>("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<std::size_t>("spinCount");
result.key_encryptor.salt_size = parser.attribute<std::size_t>("saltSize");
result.key_encryptor.block_size = parser.attribute<std::size_t>("blockSize");
result.key_encryptor.key_bits = parser.attribute<std::size_t>("keyBits");
result.key_encryptor.hash_size = parser.attribute<std::size_t>("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<std::uint8_t> hash_result(hash_out_size, 0);
hash(result.key_encryptor.hash_algorithm, salted_password.begin(), salted_password.end(), hash_result.begin());
std::vector<std::uint8_t> 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<std::uint32_t *>(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<std::uint8_t> 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<std::uint8_t> key = hash_result;
key.resize(key_size);
for (std::size_t i = 0; i < key_size; ++i)
{
key[i] = static_cast<std::uint8_t>(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<std::uint8_t> first_part(key.begin(), key.begin() + key_size);
for (int i = 0; i < key.size(); ++i)
{
key[i] = static_cast<std::uint8_t>(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<std::uint8_t> &raw_data, const std::string &password)
{
std::size_t index = 0;
auto version_major = read_int<std::uint16_t>(index, raw_data);
auto version_minor = read_int<std::uint16_t>(index, raw_data);
auto encryption_flags = read_int<std::uint32_t>(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<std::uint8_t> decrypt_xlsx(const std::vector<std::uint8_t> &bytes, const std::string &password)
{
std::vector<char> as_chars(bytes.begin(), bytes.end());
POLE::Storage storage(as_chars.data(), static_cast<unsigned long>(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<std::uint64_t *>(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::size_t>(std::ceil(size / 16)));
Botan::secure_vector<Botan::byte> c1 = pipe.read_all(0);
return std::vector<std::uint8_t>(c1.begin(), c1.begin() + size);
}
void xlsx_consumer::read(const std::vector<std::uint8_t> &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<std::uint8_t> data((std::istreambuf_iterator<char>(source)),
std::istreambuf_iterator<char>());
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

View File

@ -402,7 +402,7 @@ void xlsx_producer::write_workbook(const relationship &rel)
for (const auto ws : source_) for (const auto ws : source_)
{ {
auto sheet_rel_id = source_.d_->sheet_title_rel_id_map_[ws.get_title()]; 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().start_element(xmlns, "sheet");
serializer().attribute("name", ws.get_title()); serializer().attribute("name", ws.get_title());

View File

@ -1,5 +1,6 @@
#include <cassert> #include <cassert>
#include <fstream> #include <fstream>
#include <iostream>
#include <cxxtest/TestSuite.h> #include <cxxtest/TestSuite.h>
#include <xlnt/xlnt.hpp> #include <xlnt/xlnt.hpp>

View File

@ -153,7 +153,7 @@ zip_file::zip_file(std::istream &stream) : zip_file()
load(stream); load(stream);
} }
zip_file::zip_file(const std::vector<unsigned char> &bytes) : zip_file() zip_file::zip_file(const std::vector<std::uint8_t> &bytes) : zip_file()
{ {
load(bytes); load(bytes);
} }
@ -204,7 +204,7 @@ void zip_file::load(const path &filename)
start_read(); start_read();
} }
void zip_file::load(const std::vector<unsigned char> &bytes) void zip_file::load(const std::vector<std::uint8_t> &bytes)
{ {
if (bytes.empty()) if (bytes.empty())
{ {
@ -245,7 +245,7 @@ void zip_file::save(std::ostream &stream)
stream.write(buffer_.data(), static_cast<long>(buffer_.size())); stream.write(buffer_.data(), static_cast<long>(buffer_.size()));
} }
void zip_file::save(std::vector<unsigned char> &bytes) void zip_file::save(std::vector<std::uint8_t> &bytes)
{ {
if (archive_->m_zip_mode == MZ_ZIP_MODE_WRITING) if (archive_->m_zip_mode == MZ_ZIP_MODE_WRITING)
{ {

View File

@ -93,4 +93,9 @@ no_visible_worksheets::no_visible_worksheets()
{ {
} }
unsupported::unsupported(const std::string &message)
: exception(message)
{
}
} // namespace xlnt } // namespace xlnt

View File

@ -133,7 +133,6 @@ char path::system_separator()
path::path() path::path()
{ {
} }
path::path(const std::string &path_string) : internal_(path_string) path::path(const std::string &path_string) : internal_(path_string)

View File

@ -5,10 +5,16 @@
#include <cxxtest/TestSuite.h> #include <cxxtest/TestSuite.h>
#include <helpers/path_helper.hpp> #include <helpers/path_helper.hpp>
#include <xlnt/cell/text.hpp> #include <xlnt/workbook/workbook.hpp>
#include <xlnt/cell/text_run.hpp>
#include <xlnt/packaging/manifest.hpp>
class test_consume_xlsx : public CxxTest::TestSuite 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
}
}; };

View File

@ -612,7 +612,7 @@ void workbook::load(std::istream &stream)
consumer.read(stream); consumer.read(stream);
} }
void workbook::load(const std::vector<unsigned char> &data) void workbook::load(const std::vector<std::uint8_t> &data)
{ {
clear(); clear();
detail::xlsx_consumer consumer(*this); detail::xlsx_consumer consumer(*this);
@ -631,7 +631,34 @@ void workbook::load(const path &filename)
consumer.read(filename); consumer.read(filename);
} }
void workbook::save(std::vector<unsigned char> &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<std::uint8_t> &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<std::uint8_t> &data) const
{ {
detail::xlsx_producer producer(*this); detail::xlsx_producer producer(*this);
producer.write(data); producer.write(data);

Binary file not shown.