use crate::encryption::encrypt_content;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit},
};
use argon2::{Algorithm, Argon2, Params, Version};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use mlua::{Lua, Result, Table, Value};
const ARGON2_MEMORY_COST: u32 = 65536; const ARGON2_TIME_COST: u32 = 3;
const ARGON2_PARALLELISM: u32 = 1;
const KEY_LENGTH: usize = 32;
fn derive_key(password: &str, salt: &[u8]) -> std::result::Result<[u8; KEY_LENGTH], String> {
let params = Params::new(
ARGON2_MEMORY_COST,
ARGON2_TIME_COST,
ARGON2_PARALLELISM,
Some(KEY_LENGTH),
)
.map_err(|e| format!("Failed to create Argon2 params: {}", e))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key = [0u8; KEY_LENGTH];
argon2
.hash_password_into(password.as_bytes(), salt, &mut key)
.map_err(|e| format!("Failed to derive key: {}", e))?;
Ok(key)
}
fn decrypt_content(
ciphertext: &str,
salt: &str,
nonce: &str,
password: &str,
) -> std::result::Result<String, String> {
let salt_bytes = BASE64
.decode(salt)
.map_err(|e| format!("Invalid salt: {}", e))?;
let nonce_bytes = BASE64
.decode(nonce)
.map_err(|e| format!("Invalid nonce: {}", e))?;
let ciphertext_bytes = BASE64
.decode(ciphertext)
.map_err(|e| format!("Invalid ciphertext: {}", e))?;
let key = derive_key(password, &salt_bytes)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = cipher
.decrypt(nonce, ciphertext_bytes.as_ref())
.map_err(|_| "Decryption failed - wrong password or corrupted data".to_string())?;
String::from_utf8(plaintext).map_err(|e| format!("Invalid UTF-8 in decrypted content: {}", e))
}
fn resolve_password(explicit: Option<String>, global: &Option<String>) -> Option<String> {
explicit
.or_else(|| {
std::env::var("SITE_PASSWORD")
.ok()
.filter(|s| !s.is_empty())
})
.or_else(|| global.clone())
}
pub fn create_module(lua: &Lua, global_password: Option<String>) -> Result<Table> {
let crypt_module = lua.create_table()?;
let global_pw = global_password.clone();
let encrypt_fn =
lua.create_function(move |lua, (content, password): (String, Option<String>)| {
let pw = resolve_password(password, &global_pw).ok_or_else(|| {
mlua::Error::RuntimeError(
"No password provided. Set SITE_PASSWORD env var or pass password argument."
.to_string(),
)
})?;
let encrypted = encrypt_content(&content, &pw)
.map_err(|e| mlua::Error::RuntimeError(format!("Encryption failed: {}", e)))?;
let result = lua.create_table()?;
result.set("ciphertext", encrypted.ciphertext)?;
result.set("salt", encrypted.salt)?;
result.set("nonce", encrypted.nonce)?;
Ok(Value::Table(result))
})?;
crypt_module.set("encrypt", encrypt_fn)?;
let global_pw = global_password.clone();
let decrypt_fn = lua.create_function(move |_, (data, password): (Table, Option<String>)| {
let pw = resolve_password(password, &global_pw).ok_or_else(|| {
mlua::Error::RuntimeError(
"No password provided. Set SITE_PASSWORD env var or pass password argument."
.to_string(),
)
})?;
let ciphertext: String = data.get("ciphertext")?;
let salt: String = data.get("salt")?;
let nonce: String = data.get("nonce")?;
let plaintext =
decrypt_content(&ciphertext, &salt, &nonce, &pw).map_err(mlua::Error::RuntimeError)?;
Ok(plaintext)
})?;
crypt_module.set("decrypt", decrypt_fn)?;
let global_pw = global_password.clone();
let encrypt_html_fn =
lua.create_function(move |_, (content, options): (String, Option<Table>)| {
let explicit_pw = options
.as_ref()
.and_then(|t| t.get::<String>("password").ok());
let pw = resolve_password(explicit_pw, &global_pw).ok_or_else(|| {
mlua::Error::RuntimeError(
"No password provided. Set SITE_PASSWORD env var or pass password in options."
.to_string(),
)
})?;
let slug: String = options
.as_ref()
.and_then(|t| t.get("slug").ok())
.unwrap_or_else(|| "page".to_string());
let block_id: String = options
.as_ref()
.and_then(|t| t.get("block_id").ok())
.unwrap_or_else(|| format!("block-{}", rand::random::<u32>()));
let own_password: bool = options
.as_ref()
.and_then(|t| t.get("own_password").ok())
.unwrap_or(false);
let encrypted = encrypt_content(&content, &pw)
.map_err(|e| mlua::Error::RuntimeError(format!("Encryption failed: {}", e)))?;
let own_password_attr = if own_password {
"\n data-own-password=\"true\""
} else {
""
};
let html = format!(
r#"<div class="encrypted-content encrypted-block"
data-encrypted="{}"
data-salt="{}"
data-nonce="{}"
data-slug="{}"
data-block-id="{}"{}>
<div class="decrypt-prompt">
<p class="encrypted-message">This section is encrypted.</p>
<input type="password" placeholder="Enter password..." aria-label="Password">
<label class="remember-label">
<input type="checkbox" class="remember">
Remember for this post
</label>
<button type="button">Decrypt</button>
</div>
</div>"#,
encrypted.ciphertext,
encrypted.salt,
encrypted.nonce,
slug,
block_id,
own_password_attr
);
Ok(html)
})?;
crypt_module.set("encrypt_html", encrypt_html_fn)?;
Ok(crypt_module)
}