Changeset View
Changeset View
Standalone View
Standalone View
src/crypto/key-export.cpp
- This file was added.
| /* | |||||
| * 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); | |||||
| } | |||||
| } | |||||
| } | |||||