Page MenuHomePhorge

No OneTemporary

Size
28 KB
Referenced Files
None
Subscribers
None
diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt
index b356936..157ea09 100644
--- a/src/crypto/CMakeLists.txt
+++ b/src/crypto/CMakeLists.txt
@@ -1,27 +1,28 @@
set(kazvcrypto_SRCS
crypto.cpp
session.cpp
inbound-group-session.cpp
outbound-group-session.cpp
aes-256-ctr.cpp
base64.cpp
sha256.cpp
+ key-export.cpp
)
add_library(kazvcrypto ${kazvcrypto_SRCS})
add_library(libkazv::kazvcrypto ALIAS kazvcrypto)
set_target_properties(kazvcrypto PROPERTIES VERSION ${libkazv_VERSION_STRING} SOVERSION ${libkazv_SOVERSION})
target_link_libraries(kazvcrypto PUBLIC kazvbase)
target_link_libraries(kazvcrypto PRIVATE vodozemac::vodozemac ${CRYPTOPP_TARGET_NAME})
target_include_directories(kazvcrypto PRIVATE .)
target_include_directories(kazvcrypto
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include/kazv/crypto>
)
install(TARGETS kazvcrypto EXPORT libkazvTargets LIBRARY)
diff --git a/src/crypto/aes-256-ctr.hpp b/src/crypto/aes-256-ctr.hpp
index 3ec4b2b..c8b5fca 100644
--- a/src/crypto/aes-256-ctr.hpp
+++ b/src/crypto/aes-256-ctr.hpp
@@ -1,193 +1,212 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <memory>
#include <copy-helper.hpp>
#include "crypto-util.hpp"
namespace Kazv
{
namespace
{
using std::begin;
using std::end;
}
class AES256CTRDesc
{
public:
/// The optimal block size for this cipher.
inline static const int optimalBlockSize{16};
/// The key size for this cipher.
inline static const int keySize{32};
/// The iv size for this cipher.
inline static const int ivSize{16}; // AES block size
/// The size used for initialization of the iv for this cipher.
/// In CTR mode, there should be zero padding in the lower part of the iv, so the counter can increase from 0 to (the number of blocks - 1).
/// We use 8 bytes for random data, and the lower 8 bytes for the counter. This allows us to encrypt 2**(8*8) blocks, which is 2**(8*8) * 16 bytes = 268,435,456 TiB data
inline static const int ivSizeInit{8};
/// Random size to generate a new cipher, provided in fromRandom().
inline static const int randomSize{keySize + ivSizeInit};
using DataT = std::string;
private:
struct RawTag {};
/**
* Constructs an AES-256-CTR cipher using `key` and `iv`.
*
* `key` and `iv` are provided as-is.
*
* `key` must be of size `keySize` and `iv` must be of size `ivSize`.
* Otherwise, the constructed cipher is invalid.
*/
AES256CTRDesc(RawTag, DataT key, DataT iv);
public:
/**
* Constructs an AES-256-CTR cipher using `key` and `iv`.
*
* `key` is provided as urlsafe unpadded base64.
* `iv` is provided as unpadded base64.
*
* One must check with `valid()` before actual processing with
* the object if the key and iv comes from a remote Matrix event.
*/
AES256CTRDesc(std::string key, std::string iv);
/**
* Generate a new AES-256-CTR cipher from random data.
*
* @param random The random data provided as-is. `random.size()`
* must be at least `randomSize`.
*
* The first keySize bytes will be used as the key, and the
* next ivSize bytes will be used as the iv.
*
* @return An AES-256-CTR cipher generated with the random data.
*/
template<class RangeT>
static AES256CTRDesc fromRandom(RangeT random) {
if (random.size() < randomSize) {
// Not enough random, return an invalid one
return AES256CTRDesc(RawTag{}, DataT(), DataT());
}
auto iv = DataT(ivSize, 0);
std::copy(random.begin() + keySize, random.begin() + keySize + ivSizeInit, iv.begin());
return AES256CTRDesc(RawTag{},
DataT(random.begin(), random.begin() + keySize),
iv);
}
+ /**
+ * Generate a new AES-256-CTR cipher from raw data.
+ *
+ * @param key The unencoded key.
+ * @prama iv The unencoded iv.
+ * @return An AES-256-CTR cipher generated with the raw data.
+ */
+ template<class RangeT1, class RangeT2>
+ static AES256CTRDesc fromRaw(RangeT1 key, RangeT2 iv)
+ {
+ if (key.size() < keySize || iv.size() < ivSize) {
+ // Not enough random, return an invalid one
+ return AES256CTRDesc(RawTag{}, DataT(), DataT());
+ }
+ return AES256CTRDesc(RawTag{},
+ DataT(key.begin(), key.begin() + keySize),
+ DataT(iv.begin(), iv.begin() + ivSize));
+ }
+
KAZV_DECLARE_COPYABLE(AES256CTRDesc)
~AES256CTRDesc();
/**
* @return whether this cipher is valid. If the cipher is invalid, you
* must not use any method except destructor or operator=().
*/
bool valid() const;
/// @return The key encoded as urlsafe unpadded base64.
std::string key() const;
/// @return The iv encoded as unpadded base64.
std::string iv() const;
template<class RangeT>
using ResultBase = std::pair<AES256CTRDesc, RangeT>;
using Result = ResultBase<DataT>;
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* @param data The data to encrypt or decrypt.
*
* Example:
*
* auto cipher = Kazv::AES256CTRDesc(
* Kazv::genRandom(Kazv::AES256CTRDesc::keySize),
* Kazv::genRandom(Kazv::AES256CTRDesc::ivSize)
* );
* std::string original = "Some test here";
* auto [next, encrypted] = cipher.process(original);
* auto [next2, encrypted2] = cipher.process(original);
* REQUIRE(encrypted == encrypted2);
*
* @return an object that can be destructured into the next cipher state
* (as AES256CTRDesc) and the encrypted or decrypted data (as DataT).
*/
Result process(DataT data) const &;
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* This function only differs from the above in that it takes any range type
* and the second component of returned object is of the same type as the argument.
*/
template<class RangeT,
class = std::enable_if_t<!std::is_same_v<std::decay_t<RangeT>, DataT>, int>>
ResultBase<std::decay_t<RangeT>> process(RangeT data) const & {
using ActualRangeT = std::decay_t<RangeT>;
auto [next, res] = process(DataT(begin(data), end(data)));
return { next, ActualRangeT(res.begin(), res.end()) };
}
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* This function differs from the above two only in that its calling object
* is an rvalue reference (a.k.a casted using std::move()).
*/
Result process(DataT data) &&;
/**
* Encrypts or decrypts `data` and derives the next cipher state.
*
* This function differs from the above only in that it takes any range type
* and the second component of returned object is of the same type as the argument.
*/
template<class RangeT,
class = std::enable_if_t<!std::is_same_v<std::decay_t<RangeT>, DataT>, int>>
ResultBase<std::decay_t<RangeT>> process(RangeT data) && {
using ActualRangeT = std::decay_t<RangeT>;
auto [next, res] = std::move(*this).process(DataT(begin(data), end(data)));
return { next, ActualRangeT(res.begin(), res.end()) };
}
/**
* Encrypt or decrypt `data` and modify the state of the cipher in-place.
*
* @param data The data to encrypt or decrypt.
*/
DataT processInPlace(DataT data);
/**
* Encrypt or decrypt `data` and modify the state of the cipher in-place.
*
* This function differs from the above only in that it takes any range type
* and returns the processed data as the same range type.
*/
template<class RangeT,
class = std::enable_if_t<!std::is_same_v<std::decay_t<RangeT>, DataT>, int>>
std::decay_t<RangeT> processInPlace(RangeT data) {
using ActualRangeT = std::decay_t<RangeT>;
auto res = processInPlace(DataT(begin(data), end(data)));
return ActualRangeT(res.begin(), res.end());
}
private:
struct Private;
std::unique_ptr<Private> m_d;
};
}
diff --git a/src/crypto/key-export.cpp b/src/crypto/key-export.cpp
new file mode 100644
index 0000000..53c3fe8
--- /dev/null
+++ b/src/crypto/key-export.cpp
@@ -0,0 +1,137 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include "key-export.hpp"
+#include <cryptopp/pwdbased.h>
+#include <cryptopp/sha.h>
+#include <cryptopp/hmac.h>
+#include <cryptopp/filters.h>
+#include <cryptopp/hex.h>
+#include <debug.hpp>
+#include "base64.hpp"
+#include "aes-256-ctr.hpp"
+
+namespace Kazv
+{
+ template<class T>
+ std::string toHex(T &&v)
+ {
+ std::string res;
+ CryptoPP::ArraySource(reinterpret_cast<const CryptoPP::byte *>(v.data()), v.size(), /* pumpAll = */ true,
+ new CryptoPP::HexEncoder(new CryptoPP::StringSink(res)));
+ return res;
+ }
+
+ std::pair<ByteArray, ByteArray> deriveKeyExportKey(
+ std::string password,
+ ByteArray salt,
+ std::size_t iterations
+ )
+ {
+ // https://spec.matrix.org/v1.15/client-server-api/#key-export-format
+ CryptoPP::PKCS5_PBKDF2_HMAC<CryptoPP::SHA512> pbkdf;
+ const std::size_t totalSize = 64;
+ const std::size_t oneKeySize = 32;
+ ByteArray res(totalSize, 0);
+
+ pbkdf.DeriveKey(res.data(), res.size(), /* unused purpose */ 0,
+ reinterpret_cast<const CryptoPP::byte *>(password.data()), password.size(),
+ salt.data(), salt.size(),
+ iterations, /* time unused */ 0);
+
+ ByteArray encryptionKey(res.begin(), res.begin() + oneKeySize);
+ ByteArray hmacKey(res.begin() + oneKeySize, res.end());
+ return {encryptionKey, hmacKey};
+ }
+
+ static bool verifyKeyExportHmac(const ByteArray &hmacKey, const std::string &content)
+ {
+ auto hmac = CryptoPP::HMAC<CryptoPP::SHA256>(hmacKey.data(), hmacKey.size());
+ try {
+ CryptoPP::StringSource(content, /* pumpAll = */ true,
+ new CryptoPP::HashVerificationFilter(
+ hmac,
+ nullptr,
+ CryptoPP::HashVerificationFilter::HASH_AT_END
+ | CryptoPP::HashVerificationFilter::THROW_EXCEPTION
+ ));
+ } catch (const CryptoPP::Exception& e) {
+ kzo.crypto.warn() << "verifyKeyExportHmac: failed: " << e.what() << std::endl;
+ return false;
+ }
+
+ return true;
+ }
+
+ static const std::string keyExportHeader{"-----BEGIN MEGOLM SESSION DATA-----\n"};
+ static const std::string keyExportFooter{"\n-----END MEGOLM SESSION DATA-----"};
+
+ static const std::size_t exportFileDecodedSizeLowerBound{
+ 1 // version
+ + 16 // salt
+ + 16 // IV
+ + 4 // iterations
+ + 32 // HMAC
+ };
+
+ Maybe<nlohmann::json> decryptKeyExport(std::string exportContent, std::string password)
+ {
+ auto startPos = exportContent.find(keyExportHeader);
+ if (startPos == std::string::npos) {
+ return NotBut(DecryptKeyExportErrorCodes::FILE_MALFORMED);
+ }
+ startPos += keyExportHeader.size();
+ auto endPos = exportContent.rfind(keyExportFooter);
+ if (endPos == std::string::npos) {
+ return NotBut(DecryptKeyExportErrorCodes::FILE_MALFORMED);
+ }
+ auto contentToDecode = std::move(exportContent).substr(startPos, endPos - startPos);
+ auto decoded = decodeBase64(std::move(contentToDecode));
+ if (decoded.size() <= exportFileDecodedSizeLowerBound) {
+ return NotBut(DecryptKeyExportErrorCodes::FILE_MALFORMED);
+ }
+ if (decoded[0] != '\x01') {
+ return NotBut(DecryptKeyExportErrorCodes::VERSION_UNSUPPORTED);
+ }
+
+ auto saltBegin = decoded.begin() + 1;
+ auto saltEnd = saltBegin + 16;
+ ByteArray salt(saltBegin, saltEnd);
+ auto ivBegin = saltEnd;
+ auto ivEnd = ivBegin + 16;
+ std::string iv(ivBegin, ivEnd);
+ auto nBegin = ivEnd;
+ auto nEnd = nBegin + 4;
+ std::vector<std::uint8_t> nSeq(nBegin, nEnd);
+ std::size_t n =
+ (static_cast<std::size_t>(nSeq[0]) << 24)
+ + (static_cast<std::size_t>(nSeq[1]) << 16)
+ + (static_cast<std::size_t>(nSeq[2]) << 8)
+ + static_cast<std::size_t>(nSeq[3]);
+
+ auto hmacEnd = decoded.end();
+ auto hmacBegin = hmacEnd - 32;
+ auto contentBegin = nEnd;
+ auto contentEnd = hmacBegin;
+ auto [encryptionKey, hmacKey] = deriveKeyExportKey(password, salt, n);
+
+ if (!verifyKeyExportHmac(hmacKey, decoded)) {
+ return NotBut(DecryptKeyExportErrorCodes::HMAC_FAILED);
+ }
+
+ auto content = std::string(contentBegin, contentEnd);
+ auto cipher = AES256CTRDesc::fromRaw(encryptionKey, iv);
+ auto decrypted = cipher.processInPlace(std::move(content));
+
+ try {
+ return nlohmann::json::parse(decrypted);
+ } catch (const std::exception &e) {
+ kzo.crypto.dbg() << "decryptKeyExport: cannot parse decrypted as json: " << e.what() << std::endl;
+ return NotBut(DecryptKeyExportErrorCodes::NOT_JSON);
+ }
+ }
+}
diff --git a/src/crypto/key-export.hpp b/src/crypto/key-export.hpp
new file mode 100644
index 0000000..9f9bb2e
--- /dev/null
+++ b/src/crypto/key-export.hpp
@@ -0,0 +1,55 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+#include <vector>
+#include <nlohmann/json.hpp>
+#include <maybe.hpp>
+#include "crypto-util.hpp"
+
+namespace Kazv
+{
+ /**
+ * Derive the key-export key from the user-inputted password.
+ *
+ * @param password The user-inputted password.
+ * @param salt The salt from the key export file, or randomly generated when
+ * exporting.
+ * @param iterations The number of iterations from the key export file, or
+ * manually specified when exporting.
+ * @return A pair of [K, K'] specified in the Key Export section in
+ * the matrix spec: <https://spec.matrix.org/v1.15/client-server-api/#key-export-format>.
+ */
+ std::pair<ByteArray, ByteArray> deriveKeyExportKey(
+ std::string password,
+ ByteArray salt,
+ std::size_t iterations
+ );
+
+ namespace DecryptKeyExportErrorCodes
+ {
+ /// The file content does not conform to the spec.
+ static const std::string FILE_MALFORMED{"FILE_MALFORMED"};
+ /// The version of the export file is not supported.
+ static const std::string VERSION_UNSUPPORTED{"VERSION_UNSUPPORTED"};
+ /// The decrypted content cannot be parsed as json.
+ static const std::string NOT_JSON{"NOT_JSON"};
+ /// The HMAC verification failed. This usually means the password is not correct, or the backup file was corrupted.
+ static const std::string HMAC_FAILED{"HMAC_FAILED"};
+ }
+
+ /**
+ * Decrypt the key-export file with the user-inputted password.
+ *
+ * @param exportContent The file content.
+ * @param password The password inputted by the user.
+ * @return A Maybe containing the json of the keys contained in the file,
+ * if the operation is successful. Otherwise, it contains the error code
+ * defined in the namespace Kazv::DecryptKeyExportErrorCodes.
+ */
+ Maybe<nlohmann::json> decryptKeyExport(std::string exportContent, std::string password);
+}
diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt
index 8a21eef..0b78009 100644
--- a/src/examples/CMakeLists.txt
+++ b/src/examples/CMakeLists.txt
@@ -1,14 +1,20 @@
add_executable(basicexample
basic/main.cpp
basic/commands.cpp)
target_link_libraries(basicexample
PRIVATE kazvjob
PRIVATE kazv
PRIVATE immer
PRIVATE zug
PRIVATE lager
PRIVATE ${LIBHTTPSERVER_LINK_LIBRARIES})
target_include_directories(basicexample
PRIVATE ${LIBHTTPSERVER_INCLUDE_DIRS})
+
+add_executable(key-export-example
+ key-export-example.cpp
+)
+
+target_link_libraries(key-export-example PRIVATE kazvcrypto)
diff --git a/src/examples/key-export-example.cpp b/src/examples/key-export-example.cpp
new file mode 100644
index 0000000..14484a7
--- /dev/null
+++ b/src/examples/key-export-example.cpp
@@ -0,0 +1,54 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include "key-export.hpp"
+#include <iostream>
+#include <fstream>
+
+using namespace Kazv;
+
+std::string readWhole(std::ifstream &stream)
+{
+ // https://en.cppreference.com/w/cpp/io/basic_istream/read.html
+ auto size = stream.tellg();
+ std::string res(size, '\0');
+ stream.seekg(0);
+ stream.read(res.data(), size);
+ return res;
+}
+
+int main(int argc, char *argv[])
+{
+ if (argc < 2) {
+ std::cerr << "Usage: " << argv[0] << " FILE" << std::endl;
+ return 1;
+ }
+
+ auto stream = std::ifstream(argv[1], std::ios_base::ate | std::ios_base::binary);
+
+ if (!stream) {
+ std::cerr << "Cannot open file for reading." << std::endl;
+ return 1;
+ }
+
+ auto content = readWhole(stream);
+
+ std::cout << "Enter password:" << std::endl;
+
+ auto password = std::string();
+ std::getline(std::cin, password);
+
+ auto decrypted = decryptKeyExport(content, password);
+
+ if (decrypted.has_value()) {
+ std::cout << decrypted.value().dump() << std::endl;
+ return 0;
+ } else {
+ std::cerr << "Error: " << decrypted.reason() << std::endl;
+ return 1;
+ }
+}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 0001036..d24a830 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,122 +1,123 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
function(libkazv_add_tests)
set(options "")
set(oneValueArgs "")
set(multiValueArgs EXTRA_LINK_LIBRARIES EXTRA_INCLUDE_DIRECTORIES)
cmake_parse_arguments(PARSE_ARGV 0 libkazv_add_tests "${options}" "${oneValueArgs}" "${multiValueArgs}")
foreach(test_source ${libkazv_add_tests_UNPARSED_ARGUMENTS})
string(REGEX REPLACE "\\.cpp$" "" test_executable "${test_source}")
string(REGEX REPLACE "/|\\\\" "--" test_executable "${test_executable}")
message(STATUS "Test ${test_executable} added")
add_executable("${test_executable}" "${test_source}")
target_link_libraries("${test_executable}"
PRIVATE Catch2::Catch2WithMain
Threads::Threads
${libkazv_add_tests_EXTRA_LINK_LIBRARIES}
)
target_include_directories(
"${test_executable}"
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${libkazv_add_tests_EXTRA_INCLUDE_DIRECTORIES}
)
target_compile_definitions("${test_executable}" PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
add_test(NAME "${test_executable}" COMMAND "${test_executable}" "--allow-running-no-tests" "~[needs-internet]")
endforeach()
endfunction()
libkazv_add_tests(
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
base/immer-utils-test.cpp
base/json-utils-test.cpp
EXTRA_LINK_LIBRARIES kazvbase
)
add_library(client-test-lib SHARED client/client-test-util.cpp)
target_link_libraries(client-test-lib PUBLIC kazvjob kazvclient)
libkazv_add_tests(
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
client/sdk-test.cpp
client/thread-safety-test.cpp
client/room-test.cpp
client/random-generator-test.cpp
client/profile-test.cpp
client/kick-test.cpp
client/ban-test.cpp
client/join-test.cpp
client/keys-test.cpp
client/device-ops-test.cpp
client/send-test.cpp
client/encryption-test.cpp
client/redact-test.cpp
client/tagging-test.cpp
client/account-data-test.cpp
client/room/room-actions-test.cpp
client/room/local-echo-test.cpp
client/room/event-relationships-test.cpp
client/room/member-membership-test.cpp
client/push-rules-desc-test.cpp
client/notification-handler-test.cpp
client/validator-test.cpp
client/power-levels-desc-test.cpp
client/client-test.cpp
client/create-room-test.cpp
client/device-list-tracker-test.cpp
client/device-list-tracker-benchmark-test.cpp
client/room/read-receipt-test.cpp
client/room/undecrypted-events-test.cpp
client/encryption-benchmark-test.cpp
client/logout-test.cpp
client/room/pinned-events-test.cpp
client/get-versions-test.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
libkazv_add_tests(
basejobtest.cpp
kazvjobtest.cpp
file-desc-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob
)
libkazv_add_tests(
promise-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob kazvstore
)
libkazv_add_tests(
event-emitter-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazveventemitter
)
libkazv_add_tests(
crypto-test.cpp
crypto/inbound-group-session-test.cpp
crypto/outbound-group-session-test.cpp
crypto/session-test.cpp
+ crypto/key-export-test.cpp
EXTRA_LINK_LIBRARIES kazvcrypto
)
libkazv_add_tests(
store-test.cpp
EXTRA_LINK_LIBRARIES kazvstore kazvjob
)
diff --git a/src/tests/crypto/key-export-test.cpp b/src/tests/crypto/key-export-test.cpp
new file mode 100644
index 0000000..4709d84
--- /dev/null
+++ b/src/tests/crypto/key-export-test.cpp
@@ -0,0 +1,120 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include <catch2/catch_test_macros.hpp>
+#include <key-export.hpp>
+
+using namespace Kazv;
+
+static const std::string password = "test";
+static const std::string salt = "\x5e\x91\xe3\x75\xcc\x7b\xaf\x04\x91\xfe\xe3\x07\x89\x58\x1c\xb8";
+static const std::size_t iterations = 1000000;
+static const ByteArray iv(16, 0);
+static const ByteArray expEncryptionKey = {5, 60, 69, 180, 233, 146, 45, 161, 158, 115, 248, 156, 6, 242, 138, 45, 103, 156, 226, 255, 176, 37, 30, 182, 138, 0, 240, 157, 217, 116, 141, 128};
+static const ByteArray expHmacKey = {182, 206, 253, 28, 253, 171, 93, 193, 200, 217, 248, 152, 204, 174, 180, 159, 155, 28, 176, 208, 169, 99, 170, 121, 180, 4, 129, 245, 19, 47, 75, 182};
+static const std::string backupFile = R"(-----BEGIN MEGOLM SESSION DATA-----
+AV6R43XMe68Ekf7jB4lYHLgAAAAAAAAAAAAAAAAAAAAAAA9CQG628kByLP7LApTtbvhnpgFnUUJ
++tRMkpw4zcGoTOJya9/lawfRWKjd8LZeuHKdNLkEhfIAE16Xmqv+uU8oEASPxjLDOMjsgBKLMRx
+/iwUR7Aoe4wjuwEcdEEOW+T6ffjUz5LmEJcI14qZ1wXUPk1pnNmz+4nX8+a9UxgEpAN7vsmilwz
+P4PXNubhvGsqtZpy44pP6Td0alYgwVfTXqWB1KokMjuQE+2q6/Jb6U/z5D5nv8ArcJL04cD0U6r
+ySsRWI9Jra2OcKFQxgLeVpRAiP6/sRyl9k1n6eiSOfmGkZ+qnvOfZsQh7Wupgh6zRe8LNEtrYZh
+FpSaCE+0U8I5hZJrWNBDFfHg+rtzB4BEk0YwpD3rVcWEsk8kKqHmEulEqIXckd1SbSG7y7H1ADB
+7mjAY7qWetMizPXD+I8MDUnU1TF3Jv3CIZfZY7BHh2WukmiORlpN4H5s/Wwq2oCIk7qXhCHFvaF
+uj+XytIz6TmkEVZfXK9zqUCwCU+VYSGl9GVAezO8CZ6aEJes95yYqRxfADdJG2Vtd0oXwrpR0xV
+1GO+0JJ3xKicVX6U77iMtJbL1Lge32QvbAcv8o6mcaW28xeeYPrccMIRa3vLtuSDDqKC79S9bIP
+2U5F+MHn+5dqMeXcG9K2hS91gsBQMAvX6
+-----END MEGOLM SESSION DATA-----)";
+static const std::string nonJsonFile = R"(-----BEGIN MEGOLM SESSION DATA-----
+AV6R43XMe68Ekf7jB4lYHLgAAAAAAAAAAAAAAAAAAAAAAA9CQE628kByLP7LApTtbvhnpgFnUUJ
++tRMkpw4zcGoTOJya9/lawfRWKjd8LZeuHKdNLkEhfIAE16Xmqv+uU8oEASPxjLDOMjsgBKLMRx
+/iwUR7Aoe4wjuwEcdEEOW+T6ffjUz5LmEJcI14qZ1wXUPk1pnNmz+4nX8+a9UxgEpAN7vsmilwz
+P4PXNubhvGsqtZpy44pP6Td0alYgwVfTXqWB1KokMjuQE+2q6/Jb6U/z5D5nv8ArcJL04cD0U6r
+ySsRWI9Jra2OcKFQxgLeVpRAiP6/sRyl9k1n6eiSOfmGkZ+qnvOfZsQh7Wupgh6zRe8LNEtrYZh
+FpSaCE+0U8I5hZJrWNBDFfHg+rtzB4BEk0YwpD3rVcWEsk8kKqHmEulEqIXckd1SbSG7y7H1ADB
+7mjAY7qWetMizPXD+I8MDUnU1TF3Jv3CIZfZY7BHh2WukmiORlpN4H5s/Wwq2oCIk7qXhCHFvaF
+uj+XytIz6TmkEVZfXK9zqUCwCU+VYSGl9GVAezO8CZ6aEJes95yYqRxfADdJG2Vtd0oXwrpR0xV
+1GO+0JJ3xKicVX6U77iMtJbL1Lge32QvbAcv8o6mcaW28xeeYPrccMIRa3vLtuSDDrgHHUiZDSE
+kuZ4FJ70pzD12mQoT/0L5yVCaKP8ibtRd
+-----END MEGOLM SESSION DATA-----)";
+static const nlohmann::json expectedJson = R"([{
+ "algorithm": "m.megolm.v1.aes-sha2",
+ "forwarding_curve25519_key_chain": [
+ "hPQNcabIABgGnx3/ACv/jmMmiQHoeFfuLB17tzWp6Hw"
+ ],
+ "room_id": "!Cuyf34gef24t:localhost",
+ "sender_claimed_keys": {
+ "ed25519": "aj40p+aw64yPIdsxoog8jhPu9i7l7NcFRecuOQblE3Y"
+ },
+ "sender_key": "RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU",
+ "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
+ "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8Llf..."
+}])"_json;
+
+TEST_CASE("deriveKeyExportKey", "[crypto][key-export]")
+{
+ auto [encryptionKey, hmacKey] = deriveKeyExportKey(password, ByteArray(salt.begin(), salt.end()), iterations);
+ REQUIRE(encryptionKey == expEncryptionKey);
+ REQUIRE(hmacKey == expHmacKey);
+}
+
+TEST_CASE("decryptKeyExport", "[crypto][key-export]")
+{
+ auto res = decryptKeyExport(backupFile, password);
+ REQUIRE(res.has_value());
+ REQUIRE(res.value() == expectedJson);
+}
+
+TEST_CASE("decryptKeyExport error handling", "[crypto][key-export]")
+{
+ WHEN("no header")
+ {
+ auto res = decryptKeyExport(backupFile.substr(1), password);
+ REQUIRE(!res.has_value());
+ REQUIRE(res.reason() == DecryptKeyExportErrorCodes::FILE_MALFORMED);
+ }
+
+ WHEN("no footer")
+ {
+ auto res = decryptKeyExport(backupFile.substr(0, backupFile.size() - 1), password);
+ REQUIRE(!res.has_value());
+ REQUIRE(res.reason() == DecryptKeyExportErrorCodes::FILE_MALFORMED);
+ }
+
+ WHEN("file too small")
+ {
+ auto res = decryptKeyExport(R"(-----BEGIN MEGOLM SESSION DATA-----
+AV6R43XMe68Ekf7jB4lYHLgAAAAAAAAAAAA
+-----END MEGOLM SESSION DATA-----)", password);
+ REQUIRE(!res.has_value());
+ REQUIRE(res.reason() == DecryptKeyExportErrorCodes::FILE_MALFORMED);
+ }
+
+ WHEN("version is not 1")
+ {
+ auto modifiedBackupFile = backupFile;
+ modifiedBackupFile[37] = '1';
+ auto res = decryptKeyExport(modifiedBackupFile, password);
+ REQUIRE(!res.has_value());
+ REQUIRE(res.reason() == DecryptKeyExportErrorCodes::VERSION_UNSUPPORTED);
+ }
+
+ WHEN("does not pass HMAC")
+ {
+ auto modifiedBackupFile = backupFile;
+ modifiedBackupFile[400] = '1';
+ auto res = decryptKeyExport(modifiedBackupFile, password);
+ REQUIRE(!res.has_value());
+ REQUIRE(res.reason() == DecryptKeyExportErrorCodes::HMAC_FAILED);
+ }
+
+ WHEN("decrypted content is not json")
+ {
+ auto res = decryptKeyExport(nonJsonFile, password);
+ REQUIRE(!res.has_value());
+ REQUIRE(res.reason() == DecryptKeyExportErrorCodes::NOT_JSON);
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 9, 8:05 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1783763
Default Alt Text
(28 KB)

Event Timeline