Page MenuHomePhorge

D224.1753739086.diff
No OneTemporary

Size
16 KB
Referenced Files
None
Subscribers
None

D224.1753739086.diff

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

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)

Event Timeline