Example file: selfdec.html -- passphrase is hello
Generated by
python encode.py cover.html payload.html PassPhrase output.html
Python source
import base64
import os
import sys
import urllib.request
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
CRYPTOJS_FILENAME = "crypto-js.min.js"
CRYPTOJS_URL = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"
def get_cryptojs_source():
"""Fetches CryptoJS once from CDN and caches it locally for offline inlining."""
if not os.path.exists(CRYPTOJS_FILENAME):
print("[*] Downloading CryptoJS library for offline embedding (requires internet once)...")
try:
urllib.request.urlretrieve(CRYPTOJS_URL, CRYPTOJS_FILENAME)
except Exception as e:
print(f"[-] Error downloading CryptoJS: {e}")
sys.exit(1)
with open(CRYPTOJS_FILENAME, "r", encoding="utf-8") as f:
return f.read()
def derive_key(passphrase: str, salt: bytes) -> bytes:
"""Derive a 256-bit AES key from the passphrase and salt using PBKDF2-HMAC-SHA256."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000
)
return kdf.derive(passphrase.encode('utf-8'))
def encrypt_html(cover_path: str, payload_path: str, passphrase: str, output_path: str):
# 1. Read files
with open(payload_path, "r", encoding="utf-8") as f:
payload_html = f.read()
with open(cover_path, "r", encoding="utf-8") as f:
cover_html = f.read()
# Get the inlineable CryptoJS source code
cryptojs_code = get_cryptojs_source()
# 2. Generate cryptographic parameters
salt = os.urandom(16)
iv = os.urandom(16) # AES block size is 16 bytes for CBC
key = derive_key(passphrase, salt)
# 3. Apply PKCS7 Padding and Encrypt using AES-CBC
padder = padding.PKCS7(128).padder()
padded_data = padder.update(payload_html.encode('utf-8')) + padder.finalize()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
# Pack salt, iv, and ciphertext together, then base64 encode
blob = salt + iv + ciphertext
b64_blob = base64.b64encode(blob).decode('utf-8')
# 4. Generate the fully offline, self-contained HTML wrapper
template = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Protected Page</title>
<!-- Inlined CryptoJS Library for 100% Offline Capability -->
<script>
__CRYPTOJS_SOURCE__
</script>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 0; }
#decryption-gate {
max-width: 400px;
margin: 100px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
text-align: center;
}
#decryption-gate input[type="password"] {
width: 80%;
padding: 10px;
margin: 15px 0;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
#decryption-gate button {
padding: 10px 20px;
background-color: #0070f3;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
#decryption-gate button:hover { background-color: #0051a2; }
#error-msg { color: #e00; margin-top: 10px; font-size: 14px; }
</style>
</head>
<body>
<!-- Cover Page / Front Door Container -->
<div id="front-door">
__COVER_HTML__
<div id="decryption-gate">
<h3>Protected Content</h3>
<p>Please enter the passphrase to view this page.</p>
<input type="password" id="passphrase-input" placeholder="Enter passphrase..." autofocus />
<br>
<button onclick="attemptDecrypt()">Decrypt</button>
<div id="error-msg"></div>
</div>
</div>
<!-- Encrypted Payload Storage -->
<script id="encrypted-payload" type="application/json">__B64_BLOB__</script>
<script>
function attemptDecrypt() {
const passphrase = document.getElementById('passphrase-input').value;
const errorDiv = document.getElementById('error-msg');
errorDiv.textContent = "";
try {
const b64Data = document.getElementById('encrypted-payload').textContent.trim();
const rawData = Uint8Array.from(atob(b64Data), c => c.charCodeAt(0));
// Extract components: Salt (16 bytes) + IV (16 bytes) + Ciphertext
const salt = rawData.slice(0, 16);
const iv = rawData.slice(16, 32);
const ciphertext = rawData.slice(32);
// Derive key using PBKDF2 (100k iterations, matching Python side)
const saltWordArr = CryptoJS.lib.WordArray.create(salt);
const key = CryptoJS.PBKDF2(passphrase, saltWordArr, {
keySize: 256 / 32,
iterations: 100000,
hasher: CryptoJS.algo.SHA256
});
// Convert IV and ciphertext to CryptoJS WordArrays
const ivWordArr = CryptoJS.lib.WordArray.create(iv);
const cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: CryptoJS.lib.WordArray.create(ciphertext)
});
// Decrypt using AES-CBC
const decrypted = CryptoJS.AES.decrypt(cipherParams, key, {
iv: ivWordArr,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedText) {
throw new Error("Decryption failed");
}
// Replace entire document with decrypted payload
document.open();
document.write(decryptedText);
document.close();
} catch (err) {
errorDiv.textContent = "Decryption failed. Incorrect passphrase.";
}
}
// Allow pressing Enter key to submit
document.getElementById('passphrase-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
attemptDecrypt();
}
});
</script>
</body>
</html>
"""
html_output = (template
.replace("__CRYPTOJS_SOURCE__", cryptojs_code)
.replace("__COVER_HTML__", cover_html)
.replace("__B64_BLOB__", b64_blob))
with open(output_path, "w", encoding="utf-8") as f:
f.write(html_output)
print(f"[+] Successfully generated offline self-decrypting file: {output_path}")
if __name__ == "__main__":
if len(sys.argv) < 5:
print("Usage: python encrypt.py <cover.html> <payload.html> <passphrase> <output.html>")
sys.exit(1)
encrypt_html(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])