From 23f98c22f512b43eda44875cf49c756c8fb2f474 Mon Sep 17 00:00:00 2001 From: elpatron Date: Thu, 28 Aug 2025 10:32:16 +0200 Subject: [PATCH] feat: v1.3-konforme API, HTTP-Fixes, Base64, Example erweitert; Makefile & README Setup --- README.md | 70 +++++++++++++++++++------------ build.bat | 2 +- example/CMakeLists.txt | 24 ++++++----- example/example.cpp | 36 +++++++++++++++- src/crypto.cpp | 14 +++---- src/http_client.cpp | 44 ++++++++++++++++---- src/json_parser.cpp | 94 +++++++++++++++++++++++++----------------- src/privatebinapi.cpp | 20 ++++++++- 8 files changed, 209 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index e835673..c2fadc7 100644 --- a/README.md +++ b/README.md @@ -14,42 +14,60 @@ This library provides a simple C++ interface for interacting with PrivateBin ser - Cross-platform compatibility (Windows and Linux) - Support for PrivateBin API versions 1.3 and later -## Building +## Entwicklung & Build -### Prerequisites +### Voraussetzungen -- CMake 3.10 or later -- C++17 compatible compiler -- For Windows: Windows SDK -- For Linux: libcurl development headers +- CMake 3.10+ +- C++17-fähiger Compiler (MSVC 2022 bzw. GCC/Clang) +- Git +- vcpkg (wird bei Makefile-Nutzung automatisch gebootstrapped) -### Dependencies +### Abhängigkeiten -The library depends on the following components: +- cryptopp (Crypto++) +- nlohmann-json +- Windows: WinHTTP (SDK) +- Linux: libcurl (wird automatisch über vcpkg genutzt) -1. **HTTP Client**: - - Windows: WinHTTP - - Linux: libcurl +### Schnellstart mit Makefile (Windows & Linux) -2. **JSON Processing**: - - nlohmann/json +1) Abhängigkeiten installieren, konfigurieren und bauen: -### Building on Windows - -```cmd -mkdir build -cd build -cmake .. -cmake --build . +``` +make ``` -### Building on Linux +2) Beispiel bauen und ausführen: -```bash -mkdir build -cd build -cmake .. -make +``` +make example +``` + +Das Makefile erledigt: +- vcpkg klonen & bootstrappen +- Pakete aus `vcpkg.json` installieren +- CMake mit vcpkg-Toolchain konfigurieren +- Bibliothek und Beispiel bauen + +### Manuell mit CMake + +``` +# vcpkg klonen & bootstrappen +git clone https://github.com/microsoft/vcpkg.git "$HOME/vcpkg" +"$HOME/vcpkg/bootstrap-vcpkg.sh" # Linux/macOS +# Windows (PowerShell): +# powershell -NoProfile -ExecutionPolicy Bypass -Command "& '$env:USERPROFILE\vcpkg\bootstrap-vcpkg.bat'" + +# Konfigurieren +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$HOME/vcpkg/scripts/buildsystems/vcpkg.cmake" + +# Bauen +cmake --build build --config Release + +# Beispiel +cmake -S example -B example/build -DCMAKE_BUILD_TYPE=Release +cmake --build example/build --config Release ``` ## Usage diff --git a/build.bat b/build.bat index abc4556..8576a78 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,7 @@ REM Create build directory if not exist "build" mkdir build cd build -REM Generate build files with CMake and vcpkg toolchain +REM Generate build files with CMake and vcpkg manifest cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=../vcpkg/scripts/buildsystems/vcpkg.cmake REM Build the project diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index c57cb39..157ac69 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -3,23 +3,25 @@ project(PrivateBinAPIExample) set(CMAKE_CXX_STANDARD 17) -# Find the privatebinapi library -find_library(PRIVATEBINAPI_LIB privatebinapi - PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../build) +# Use the prebuilt library from the parent build directory +set(PRIVATEBINAPI_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../build") +set(PRIVATEBINAPI_RELEASE_LIB "${PRIVATEBINAPI_BUILD_DIR}/Release/privatebinapi.lib") -# If not found, build it as part of the project -if(NOT PRIVATEBINAPI_LIB) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/privatebinapi) - set(PRIVATEBINAPI_LIB privatebinapi) +if(EXISTS "${PRIVATEBINAPI_RELEASE_LIB}") + set(PRIVATEBINAPI_LIB "${PRIVATEBINAPI_RELEASE_LIB}") +else() + # Fallback: try the build root (multi-config generators may place libs differently) + find_library(PRIVATEBINAPI_LIB privatebinapi PATHS "${PRIVATEBINAPI_BUILD_DIR}") +endif() + +if(NOT PRIVATEBINAPI_LIB) + message(FATAL_ERROR "privatebinapi library not found. Please run build.bat in the project root first.") endif() -# Create example executable add_executable(example example.cpp) -# Link with the privatebinapi library -target_link_libraries(example ${PRIVATEBINAPI_LIB}) +target_link_libraries(example PRIVATE ${PRIVATEBINAPI_LIB} winhttp) -# Include directories target_include_directories(example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ) \ No newline at end of file diff --git a/example/example.cpp b/example/example.cpp index 4010e20..9b8b180 100644 --- a/example/example.cpp +++ b/example/example.cpp @@ -31,7 +31,41 @@ int main() { std::cout << "Paste created successfully!" << std::endl; std::cout << "URL: " << paste_url << std::endl; std::cout << "Delete token: " << delete_token << std::endl; - + + // Parse paste_id and key from URL (format: "/?{pasteID}#{key}") + std::string full_url = paste_url ? paste_url : ""; + std::string paste_id; + std::string key; + auto qpos = full_url.find('?'); + auto hpos = full_url.find('#'); + if (qpos != std::string::npos) { + if (hpos != std::string::npos && hpos > qpos + 1) { + paste_id = full_url.substr(qpos + 1, hpos - (qpos + 1)); + key = full_url.substr(hpos + 1); + } else if (qpos + 1 < full_url.size()) { + paste_id = full_url.substr(qpos + 1); + } + } + + // Try to fetch paste content back + if (!paste_id.empty() && !key.empty()) { + std::cout << "Fetching paste..." << std::endl; + char* content = nullptr; + int gr = get_paste("https://privatebin.medisoftware.org", paste_id.c_str(), key.c_str(), &content); + std::cout << "get_paste returned: " << gr << std::endl; + if (gr == 0 && content) { + std::cout << "Content: " << content << std::endl; + free_string(content); + } + } + + // Try to delete paste + if (!paste_id.empty() && delete_token) { + std::cout << "Deleting paste..." << std::endl; + int dr = delete_paste("https://privatebin.medisoftware.org", paste_id.c_str(), delete_token); + std::cout << "delete_paste returned: " << dr << std::endl; + } + // Clean up allocated memory free_string(paste_url); free_string(delete_token); diff --git a/src/crypto.cpp b/src/crypto.cpp index 1edf16f..472ff59 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -4,13 +4,13 @@ #include // Crypto++ includes -#include "cryptlib.h" -#include "osrng.h" // AutoSeededRandomPool -#include "aes.h" // AES encryption -#include "gcm.h" // GCM mode -#include "pwdbased.h" // PBKDF2 -#include "sha.h" // SHA256 -#include "zlib.h" // Zlib compression +#include +#include // AutoSeededRandomPool +#include // AES encryption +#include // GCM mode +#include // PBKDF2 +#include // SHA256 +#include // Zlib compression using namespace CryptoPP; diff --git a/src/http_client.cpp b/src/http_client.cpp index f000e23..38dd78b 100644 --- a/src/http_client.cpp +++ b/src/http_client.cpp @@ -6,6 +6,14 @@ #include #include #pragma comment(lib, "winhttp.lib") + +static std::wstring utf8_to_wide(const std::string& s) { + if (s.empty()) return std::wstring(); + int needed = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0); + std::wstring ws(needed, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &ws[0], needed); + return ws; +} #elif LINUX #include #endif @@ -36,7 +44,8 @@ bool HttpClient::get(const std::string& url, std::string& response) { urlComp.dwExtraInfoLength = (DWORD)-1; // Parse the URL - if (!WinHttpCrackUrl((LPCWSTR)url.c_str(), 0, 0, &urlComp)) { + std::wstring wurl = utf8_to_wide(url); + if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &urlComp)) { return false; } @@ -59,13 +68,25 @@ bool HttpClient::get(const std::string& url, std::string& response) { return false; } + // Build object name = path + extra info (query) + std::wstring pathPart = (urlComp.lpszUrlPath && urlComp.dwUrlPathLength > 0) + ? std::wstring(urlComp.lpszUrlPath, urlComp.dwUrlPathLength) + : std::wstring(L"/"); + std::wstring extraPart = (urlComp.lpszExtraInfo && urlComp.dwExtraInfoLength > 0) + ? std::wstring(urlComp.lpszExtraInfo, urlComp.dwExtraInfoLength) + : std::wstring(); + std::wstring objectName = pathPart + extraPart; + // Create an HTTP request handle HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", - urlComp.lpszUrlPath, + objectName.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0); + // Set headers per API requirement + LPCWSTR headers = L"X-Requested-With: JSONHttpRequest\r\nAccept: application/json"; + WinHttpAddRequestHeaders(hRequest, headers, -1L, WINHTTP_ADDREQ_FLAG_ADD); if (!hRequest) { WinHttpCloseHandle(hConnect); @@ -156,7 +177,8 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri urlComp.dwExtraInfoLength = (DWORD)-1; // Parse the URL - if (!WinHttpCrackUrl((LPCWSTR)url.c_str(), 0, 0, &urlComp)) { + std::wstring wurl = utf8_to_wide(url); + if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &urlComp)) { return false; } @@ -179,9 +201,10 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri return false; } - // Create an HTTP request handle + // Create an HTTP request handle (POST per API 1.3) + LPCWSTR postPath = (urlComp.lpszUrlPath && urlComp.dwUrlPathLength > 0) ? urlComp.lpszUrlPath : L"/"; HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", - urlComp.lpszUrlPath, + postPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? @@ -208,6 +231,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri } // Send a request + // Send UTF-8 bytes as body (raw) if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)data.c_str(), (DWORD)data.length(), @@ -290,7 +314,8 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std urlComp.dwExtraInfoLength = (DWORD)-1; // Parse the URL - if (!WinHttpCrackUrl((LPCWSTR)url.c_str(), 0, 0, &urlComp)) { + std::wstring wurlDel = utf8_to_wide(url); + if (!WinHttpCrackUrl(wurlDel.c_str(), 0, 0, &urlComp)) { return false; } @@ -314,8 +339,9 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std } // Create an HTTP request handle + LPCWSTR delPath = (urlComp.lpszUrlPath && urlComp.dwUrlPathLength > 0) ? urlComp.lpszUrlPath : L"/"; HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", - urlComp.lpszUrlPath, + delPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? @@ -341,9 +367,9 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std return false; } - // Send a request with DELETE method + // Send a request body with DELETE method if (!WinHttpSendRequest(hRequest, - L"DELETE", 6, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)data.c_str(), (DWORD)data.length(), (DWORD)data.length(), 0)) { WinHttpCloseHandle(hRequest); diff --git a/src/json_parser.cpp b/src/json_parser.cpp index aed5da5..62d9fa4 100644 --- a/src/json_parser.cpp +++ b/src/json_parser.cpp @@ -1,6 +1,28 @@ #include "json_parser.h" #include #include +#include +#include +#include + +// Helper: Base64 encode without line breaks +static std::string encode_base64(const std::vector& data) { + std::string result; + CryptoPP::StringSource ss( + data.data(), data.size(), true, + new CryptoPP::Base64Encoder(new CryptoPP::StringSink(result), false /* insertLineBreaks */) + ); + return result; +} + +// Helper: Base64 decode +static std::vector decode_base64(const std::string& b64) { + std::string decoded; + CryptoPP::StringSource ss( + b64, true, new CryptoPP::Base64Decoder(new CryptoPP::StringSink(decoded)) + ); + return std::vector(decoded.begin(), decoded.end()); +} json JsonParser::create_paste_json(const std::vector& cipher_text, const std::vector& auth_tag, @@ -11,17 +33,16 @@ json JsonParser::create_paste_json(const std::vector& cipher_text bool burn_after_reading, bool open_discussion) { - // Convert binary data to base64 strings (stub implementation) - std::string cipher_text_b64(cipher_text.begin(), cipher_text.end()); - std::string auth_tag_b64(auth_tag.begin(), auth_tag.end()); - std::string iv_b64(iv.begin(), iv.end()); - std::string salt_b64(salt.begin(), salt.end()); + // Convert binary data to base64 strings + std::string cipher_text_b64 = encode_base64(cipher_text); + std::string iv_b64 = encode_base64(iv); + std::string salt_b64 = encode_base64(salt); // Get current timestamp auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); - // Create the metadata array + // Create the metadata array according to PrivateBin v2 json metadata = { { iv_b64, // base64(cipher_iv) @@ -33,21 +54,19 @@ json JsonParser::create_paste_json(const std::vector& cipher_text "gcm", // mode "zlib" // compression }, - format, // format + format, // formatter key burn_after_reading ? 1 : 0, // burn after reading open_discussion ? 1 : 0 // open discussion }; // Create the main JSON structure + // JSON close to v1.3 JSON-LD: meta.expire used by server json paste_json = { - {"v", 2}, // version - {"adata", metadata}, // metadata - {"ct", cipher_text_b64}, // cipher text + {"v", 2}, // version + {"adata", metadata}, // metadata + {"ct", cipher_text_b64}, // ciphertext (base64) {"meta", { - {"expire", expiration}, - {"created", now}, - {"time_to_live", 300}, // This would be calculated based on expiration - {"icon", "data:image/png;base64,..."} // Placeholder + {"expire", expiration} }} }; @@ -62,29 +81,32 @@ bool JsonParser::parse_paste_json(const json& json_data, std::string& expiration) { try { - // Extract cipher text - std::string ct = json_data["ct"]; - cipher_text = std::vector(ct.begin(), ct.end()); + // Extract and decode cipher text + std::string ct_b64 = json_data.at("ct"); + cipher_text = decode_base64(ct_b64); - // Extract metadata - json adata = json_data["adata"]; + // Optional explicit tag field + if (json_data.contains("tag")) { + std::string tag_b64 = json_data.at("tag"); + auth_tag = decode_base64(tag_b64); + } else { + auth_tag.clear(); + } + + // Extract metadata and decode IV & salt + json adata = json_data.at("adata"); if (adata.size() >= 1) { json first_element = adata[0]; if (first_element.is_array() && first_element.size() >= 2) { - // Extract IV and salt (from base64 in real implementation) - std::string iv_str = first_element[0]; - std::string salt_str = first_element[1]; - - iv = std::vector(iv_str.begin(), iv_str.end()); - salt = std::vector(salt_str.begin(), salt_str.end()); + std::string iv_b64 = first_element[0]; + std::string salt_b64 = first_element[1]; + iv = decode_base64(iv_b64); + salt = decode_base64(salt_b64); } } // Extract expiration - expiration = json_data["meta"]["expire"]; - - // Auth tag would be extracted from the ciphertext in a real implementation - auth_tag.resize(16, 0); + expiration = json_data.at("meta").at("expire"); return true; } catch (...) { @@ -101,17 +123,13 @@ bool JsonParser::parse_response(const std::string& response, try { json json_response = json::parse(response); - - status = json_response["status"]; - + status = json_response.value("status", 1); if (status == 0) { - // Success response - paste_id = json_response["id"]; - url = json_response["url"]; - delete_token = json_response["deletetoken"]; + paste_id = json_response.value("id", ""); + url = json_response.value("url", ""); + delete_token = json_response.value("deletetoken", ""); } else { - // Error response - message = json_response["message"]; + message = json_response.value("message", ""); } return true; diff --git a/src/privatebinapi.cpp b/src/privatebinapi.cpp index 372b60c..84d3693 100644 --- a/src/privatebinapi.cpp +++ b/src/privatebinapi.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #define PRIVATEBIN_API_VERSION "1.3" @@ -54,6 +55,7 @@ int create_paste(const char* server_url, const char* content, std::vector plaintext(content, content + strlen(content)); // Compress the plaintext + std::cerr << "[privatebinapi] compress..." << std::endl; std::vector compressed_data = Crypto::compress(plaintext); // Generate random salt and IV @@ -61,10 +63,12 @@ int create_paste(const char* server_url, const char* content, std::vector iv = Crypto::generate_key(16); // Derive key using PBKDF2 + std::cerr << "[privatebinapi] pbkdf2..." << std::endl; std::vector derived_key = Crypto::pbkdf2_hmac_sha256( paste_passphrase, salt, 100000, 32); // Encrypt the data + std::cerr << "[privatebinapi] encrypt..." << std::endl; std::vector auth_tag; std::vector cipher_text = Crypto::encrypt( compressed_data, derived_key, iv, auth_tag); @@ -91,8 +95,12 @@ int create_paste(const char* server_url, const char* content, int status; std::string message, paste_id, url, del_token; if (!JsonParser::parse_response(response, status, message, paste_id, url, del_token)) { + std::cerr << "[privatebinapi] raw response (unparsed): " << response << std::endl; return ERROR_JSON_PARSE; } + if (status != 0) { + std::cerr << "[privatebinapi] server status=" << status << ", message= " << message << std::endl; + } if (status != 0) { return ERROR_SERVER; @@ -109,7 +117,11 @@ int create_paste(const char* server_url, const char* content, copy_string_to_output(del_token, delete_token); return ERROR_SUCCESS; + } catch (const std::exception& e) { + std::cerr << "[privatebinapi] crypto error: " << e.what() << std::endl; + return ERROR_CRYPTO; } catch (...) { + std::cerr << "[privatebinapi] unknown crypto error" << std::endl; return ERROR_CRYPTO; } } @@ -122,15 +134,17 @@ int get_paste(const char* server_url, const char* paste_id, } try { - // Construct the URL - std::string url = std::string(server_url) + "/" + paste_id; + // Construct the URL with query per API: base?pasteID + std::string url = std::string(server_url) + "?" + paste_id; // Send GET request HttpClient client; std::string response; + std::cerr << "[privatebinapi] GET " << url << std::endl; if (!client.get(url, response)) { return ERROR_NETWORK; } + std::cerr << "[privatebinapi] GET response: " << response << std::endl; // Parse the JSON response json json_data = json::parse(response); @@ -188,9 +202,11 @@ int delete_paste(const char* server_url, const char* paste_id, // Send DELETE request HttpClient client; std::string response; + std::cerr << "[privatebinapi] DELETE payload: " << json_data << std::endl; if (!client.delete_req(server_url, json_data, response)) { return ERROR_NETWORK; } + std::cerr << "[privatebinapi] DELETE response: " << response << std::endl; // Parse response int status;