diff options
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| -rw-r--r-- | include/markdown_translator.hpp | 2 | ||||
| -rw-r--r-- | src/main.cpp | 115 | ||||
| -rw-r--r-- | styles/carbon.css | 129 |
4 files changed, 250 insertions, 2 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 107ea02..b45c67a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,9 @@ endif() message(STATUS "Source directory: ${CMAKE_SOURCE_DIR}") message(STATUS "Binary directory: ${CMAKE_BINARY_DIR}") +# Find OpenSSL for AES-GCM / PBKDF2 usage +find_package(OpenSSL REQUIRED) + add_executable(${PROJECT_NAME} src/main.cpp src/markdown_translator.cpp @@ -22,6 +25,9 @@ add_executable(${PROJECT_NAME} target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include) +# Link OpenSSL libraries +target_link_libraries(${PROJECT_NAME} PRIVATE OpenSSL::Crypto OpenSSL::SSL) + add_custom_target(copy_styles ALL COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/styles diff --git a/include/markdown_translator.hpp b/include/markdown_translator.hpp index 34efefb..2f55292 100644 --- a/include/markdown_translator.hpp +++ b/include/markdown_translator.hpp @@ -83,7 +83,7 @@ private: <p>Last updated: )" + getCurrentDateTime() + R"(</p> </div> </div> - </div> + </div><!-- WIKI_CONTENT_END --> <script src="https://unpkg.com/prismjs@1.30.0/prism.js"></script> <script src="https://unpkg.com/prismjs@1.30.0/plugins/autoloader/prism-autoloader.min.js"></script> </body> diff --git a/src/main.cpp b/src/main.cpp index 576eb27..2d94889 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,13 +3,17 @@ #include <string> #include <sstream> #include <unordered_map> +#include <vector> +#include <openssl/evp.h> +#include <openssl/rand.h> +#include <openssl/err.h> #include "markdown_translator.hpp" #include "file_uploader.hpp" #include "rclone_uploader.hpp" bool parseArguments(int argc, char* argv[], std::unordered_map<std::string, std::string>& params, std::string& inputFile) { if (argc < 2) { - std::cerr << "Usage: COMMAND <input_file> [-o <output_file>] [-css <css_file_path>] [other options]\n"; + std::cerr << "Usage: COMMAND <input_file> [-o <output_file>] [-css <css_file_path>] [--encode <passphrase>] [other options]\n"; return false; } params["-o"] = "index.html"; @@ -42,6 +46,69 @@ bool parseArguments(int argc, char* argv[], std::unordered_map<std::string, std: return true; } +// Encrypt using AES-256-GCM with PBKDF2-HMAC-SHA256 key derivation. +static std::string base64Encode(const std::vector<unsigned char>& data) { + if (data.empty()) return std::string(); + size_t out_len = 4 * ((data.size() + 2) / 3) + 1; + std::vector<unsigned char> out(out_len); + int written = EVP_EncodeBlock(out.data(), data.data(), (int)data.size()); + return std::string(reinterpret_cast<char*>(out.data()), written); +} + +static std::vector<unsigned char> encryptAESGCM(const std::string& plaintext, const std::string& passphrase) { + const int SALT_LEN = 16; + const int IV_LEN = 12; + const int TAG_LEN = 16; + const int KEY_LEN = 32; + const int PBKDF2_ITER = 100000; + + std::vector<unsigned char> salt(SALT_LEN); + if (RAND_bytes(salt.data(), SALT_LEN) != 1) { + throw std::runtime_error("RAND_bytes for salt failed"); + } + + std::vector<unsigned char> iv(IV_LEN); + if (RAND_bytes(iv.data(), IV_LEN) != 1) { + throw std::runtime_error("RAND_bytes for iv failed"); + } + + std::vector<unsigned char> key(KEY_LEN); + if (PKCS5_PBKDF2_HMAC(passphrase.c_str(), (int)passphrase.size(), salt.data(), SALT_LEN, PBKDF2_ITER, EVP_sha256(), KEY_LEN, key.data()) != 1) { + throw std::runtime_error("PBKDF2 key derivation failed"); + } + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) throw std::runtime_error("EVP_CIPHER_CTX_new failed"); + + int rc = EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); + if (rc != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("EVP_EncryptInit_ex failed"); } + rc = EVP_EncryptInit_ex(ctx, NULL, NULL, key.data(), iv.data()); + if (rc != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("EVP_EncryptInit_ex set key/iv failed"); } + + std::vector<unsigned char> ciphertext(plaintext.size()); + int outlen = 0; + rc = EVP_EncryptUpdate(ctx, ciphertext.data(), &outlen, reinterpret_cast<const unsigned char*>(plaintext.data()), (int)plaintext.size()); + if (rc != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("EVP_EncryptUpdate failed"); } + int tmplen = 0; + rc = EVP_EncryptFinal_ex(ctx, ciphertext.data() + outlen, &tmplen); + if (rc != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("EVP_EncryptFinal_ex failed"); } + int cipher_len = outlen + tmplen; + ciphertext.resize(cipher_len); + + std::vector<unsigned char> tag(TAG_LEN); + rc = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, TAG_LEN, tag.data()); + if (rc != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("EVP_CIPHER_CTX_ctrl GET_TAG failed"); } + + EVP_CIPHER_CTX_free(ctx); + std::vector<unsigned char> out; + out.reserve(salt.size() + iv.size() + ciphertext.size() + tag.size()); + out.insert(out.end(), salt.begin(), salt.end()); + out.insert(out.end(), iv.begin(), iv.end()); + out.insert(out.end(), ciphertext.begin(), ciphertext.end()); + out.insert(out.end(), tag.begin(), tag.end()); + return out; +} + int main(int argc, char* argv[]) { std::unordered_map<std::string, std::string> params; std::string inputFile; @@ -77,6 +144,52 @@ int main(int argc, char* argv[]) { std::string htmlOutput = translator.translate(markdownContent); + if (params.find("--encode") != params.end() && !params["--encode"].empty()) { + try { + std::string startMarker = " <div class=\"nav-sidebar\">"; + size_t startPos = htmlOutput.find(startMarker); + if (startPos != std::string::npos) { + const std::string endMarker = "<!-- WIKI_CONTENT_END -->"; + size_t endPos = htmlOutput.find(endMarker, startPos); + if (endPos != std::string::npos) { + std::string contentBlock = htmlOutput.substr(startPos, endPos - startPos); + + std::vector<unsigned char> encrypted = encryptAESGCM(contentBlock, params["--encode"]); + std::string b64 = base64Encode(encrypted); + + std::stringstream repl; + repl << "<div id=\"lock-screen\">\n"; + repl << " <div id=\"lock-box\">\n"; + repl << " <div class=\"lock-icon\">🔒</div>\n"; + repl << " <h2>Encoded Content</h2>\n"; + repl << " <p>Enter the passphrase to decode this post.</p>\n"; + repl << " <input id=\"enc-input\" type=\"password\" placeholder=\"Enter passphrase...\"/>\n"; + repl << " </div>\n"; + repl << "</div>\n"; + repl << "<div id=\"encrypted-content\" data-enc=\"" << b64 << "\"></div>\n"; + repl << "<script>\n"; + repl << "(function(){\n"; + repl << "function b64ToArr(b64){var bin=atob(b64);var len=bin.length;var arr=new Uint8Array(len);for(var i=0;i<len;i++)arr[i]=bin.charCodeAt(i);return arr;}\n"; + repl << "async function tryDecrypt(pass){try{var c=document.getElementById('encrypted-content');if(!c)return;var data=b64ToArr(c.dataset.enc);var salt=data.slice(0,16);var iv=data.slice(16,28);var tag=data.slice(data.length-16);var ct=data.slice(28,data.length-16);var ek=new TextEncoder().encode(pass);var km=await crypto.subtle.importKey('raw',ek,{name:'PBKDF2'},false,['deriveKey']);var key=await crypto.subtle.deriveKey({name:'PBKDF2',salt:salt,iterations:100000,hash:'SHA-256'},km,{name:'AES-GCM',length:256},false,['decrypt']);var full=new Uint8Array(ct.length+tag.length);full.set(ct,0);full.set(tag,ct.length);var plain=await crypto.subtle.decrypt({name:'AES-GCM',iv:iv},key,full);var decoded=new TextDecoder().decode(plain);var ls=document.getElementById('lock-screen');if(ls)ls.remove();c.outerHTML=decoded;if(window.Prism)Prism.highlightAll();}catch(e){}}\n"; + repl << "document.addEventListener('DOMContentLoaded',function(){var inp=document.getElementById('enc-input');if(inp)inp.addEventListener('input',function(e){tryDecrypt(e.target.value);});});\n"; + repl << "})();\n"; + repl << "</script>\n"; + htmlOutput = htmlOutput.substr(0, startPos) + repl.str() + + htmlOutput.substr(endPos + endMarker.length()); + } + } + } catch (const std::exception& ex) { + } + } + + // Always strip the WIKI_CONTENT_END marker when not encrypted + { + const std::string marker = "<!-- WIKI_CONTENT_END -->"; + size_t pos = htmlOutput.find(marker); + if (pos != std::string::npos) + htmlOutput.erase(pos, marker.length()); + } + // Write to output file std::string outputFile = params["-o"]; std::ofstream outFile(outputFile); diff --git a/styles/carbon.css b/styles/carbon.css index 42e5743..d5c173b 100644 --- a/styles/carbon.css +++ b/styles/carbon.css @@ -626,3 +626,132 @@ img { .main-content > * { animation: fadeIn 0.6s ease-out; } + +/* ============================================================ + Lock screen — encrypted posts + ============================================================ */ + +/* The data holder is invisible; only the lock UI is shown */ +#encrypted-content { + display: none; +} + +/* Full-viewport overlay */ +#lock-screen { + position: fixed; + inset: 0; + background-color: var(--primary-bg); + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 10px, + rgba(255, 255, 255, 0.01) 10px, + rgba(255, 255, 255, 0.01) 20px + ); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + animation: fadeIn 0.4s ease-out; +} + +/* Card */ +#lock-box { + background-color: var(--secondary-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 2.75rem 3rem; + width: 90%; + max-width: 400px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.1rem; + box-shadow: + 0 12px 50px rgba(0, 0, 0, 0.7), + 0 0 0 1px rgba(201, 170, 113, 0.07); + position: relative; + overflow: hidden; +} + +/* Gradient top-accent bar */ +#lock-box::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--accent-gold), var(--accent-blue)); +} + +/* Lock emoji */ +.lock-icon { + font-size: 2.6rem; + line-height: 1; + margin-bottom: 0.15rem; +} + +/* Override the global h2 blue/border-left styles inside the lock box */ +#lock-box h2 { + font-family: "Jupiter Pro", "Cinzel", "Georgia", serif; + font-size: 1.35rem; + font-weight: 500; + letter-spacing: 1px; + color: var(--accent-gold); + -webkit-text-fill-color: var(--accent-gold); + background: none; + -webkit-background-clip: unset; + background-clip: unset; + text-shadow: none; + border-left: none; + padding-left: 0; + margin-left: 0; + margin-top: 0; + margin-bottom: 0; + position: static; +} + +#lock-box p { + color: var(--text-dim); + font-size: 0.87rem; + margin: 0; + text-align: center; +} + +/* Password input */ +#enc-input { + background-color: var(--tertiary-bg); + color: var(--text-primary); + border: 1px solid var(--border-color); + padding: 0.65rem 1rem; + border-radius: 6px; + width: 100%; + font-size: 0.95rem; + font-family: inherit; + text-align: center; + letter-spacing: 0.05em; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +#enc-input::placeholder { + color: var(--text-dim); + letter-spacing: 0; +} + +#enc-input:focus { + outline: none; + border-color: var(--accent-blue); + box-shadow: + 0 0 0 3px rgba(91, 143, 199, 0.12), + inset 0 1px 3px rgba(0, 0, 0, 0.3); +} + +@media (max-width: 480px) { + #lock-box { + padding: 2rem 1.5rem; + } +} + + |
