aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-04-29 00:03:19 -0700
committerPinapelz <yukais@pinapelz.com>2026-04-29 00:03:19 -0700
commit00c46e07ac0fa8a75f5d6e080a14e1da5edf1e4e (patch)
tree02d9a7d6b7be1ce2c65a6e632a47fac39893cb30
parent1eff707e1c23f9922c038fb9fab6264b1b8b791e (diff)
post encoding and decodingHEADmain
this is not encryption but acts as a sort of pseudo deterrent?
-rw-r--r--CMakeLists.txt6
-rw-r--r--include/markdown_translator.hpp2
-rw-r--r--src/main.cpp115
-rw-r--r--styles/carbon.css129
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\">&#128274;</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;
+ }
+}
+
+
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage