Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F3345015
D224.1753739086.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
D224.1753739086.diff
View Options
diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt
--- a/src/crypto/CMakeLists.txt
+++ b/src/crypto/CMakeLists.txt
@@ -7,6 +7,7 @@
aes-256-ctr.cpp
base64.cpp
sha256.cpp
+ key-export.cpp
)
add_library(kazvcrypto ${kazvcrypto_SRCS})
diff --git a/src/crypto/aes-256-ctr.hpp b/src/crypto/aes-256-ctr.hpp
--- a/src/crypto/aes-256-ctr.hpp
+++ b/src/crypto/aes-256-ctr.hpp
@@ -89,6 +89,25 @@
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();
diff --git a/src/crypto/key-export.hpp b/src/crypto/key-export.hpp
new file mode 100644
--- /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/crypto/key-export.cpp b/src/crypto/key-export.cpp
new file mode 100644
--- /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/examples/CMakeLists.txt b/src/examples/CMakeLists.txt
--- a/src/examples/CMakeLists.txt
+++ b/src/examples/CMakeLists.txt
@@ -12,3 +12,9 @@
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
--- /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
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -113,6 +113,7 @@
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
)
diff --git a/src/tests/crypto/key-export-test.cpp b/src/tests/crypto/key-export-test.cpp
new file mode 100644
--- /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
Details
Attached
Mime Type
text/plain
Expires
Mon, Jul 28, 2:44 PM (6 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
323860
Default Alt Text
D224.1753739086.diff (16 KB)
Attached To
Mode
D224: Support decrypting key export file
Attached
Detach File
Event Timeline
Log In to Comment