Step-by-Step Guide to building a custom secure file encryption vault block for Gutenberg using custom WebAssembly modules
Leveraging WebAssembly for Client-Side Encryption in WordPress
Integrating robust client-side encryption directly into WordPress content presents a significant security enhancement, particularly for sensitive data. Traditional server-side encryption, while common, leaves data vulnerable in transit and at rest on the server. By utilizing WebAssembly (Wasm), we can execute complex cryptographic operations directly within the user’s browser, ensuring that data is encrypted *before* it ever leaves their machine. This approach is ideal for building custom Gutenberg blocks that manage encrypted files or text snippets.
This guide details the construction of a custom Gutenberg block that leverages a WebAssembly module for AES-GCM encryption and decryption. We’ll cover the Wasm module compilation, its integration into a WordPress plugin, and the JavaScript logic for the Gutenberg block itself.
I. Compiling a WebAssembly Cryptography Module
For this example, we’ll use C++ and Emscripten to compile a simple AES-GCM encryption/decryption function into a WebAssembly module. AES-GCM is a symmetric encryption mode that provides both confidentiality and authenticity.
A. C++ Source Code (crypto.cpp)
We’ll define functions to initialize the AES context, encrypt data, and decrypt data. For simplicity, we’ll use a fixed key and nonce, but in a production scenario, these should be securely generated and managed.
#include <emscripten.h>
#include <vector>
#include <cstring> // For memcpy
// Basic AES-GCM implementation (simplified for demonstration)
// In a real-world scenario, use a well-vetted crypto library like mbedTLS or OpenSSL compiled for Wasm.
// This example uses a placeholder for actual AES-GCM logic.
// Placeholder for AES-GCM context and operations
struct AesGcmContext {
// Placeholder for key, IV, etc.
unsigned char key[32]; // 256-bit key
unsigned char iv[12]; // 96-bit IV
};
extern "C" {
// Function to initialize the context (simplified)
EMSCRIPTEN_KEEPALIVE
AesGcmContext* init_aes_gcm(const unsigned char* key, size_t key_len, const unsigned char* iv, size_t iv_len) {
if (key_len != 32 || iv_len != 12) {
return nullptr; // Invalid key or IV size
}
AesGcmContext* ctx = new AesGcmContext();
memcpy(ctx->key, key, key_len);
memcpy(ctx->iv, iv, iv_len);
// In a real implementation, this would initialize the AES cipher with the key.
return ctx;
}
// Placeholder for encryption function
EMSCRIPTEN_KEEPALIVE
std::vector encrypt_aes_gcm(AesGcmContext* ctx, const unsigned char* plaintext, size_t plaintext_len) {
if (!ctx) return {};
std::vector<unsigned char> ciphertext(plaintext_len + 16); // Placeholder for ciphertext + tag
// --- Actual AES-GCM encryption logic would go here ---
// For demonstration, we'll just append a dummy tag and slightly modify plaintext
memcpy(ciphertext.data(), plaintext, plaintext_len);
// Append a dummy 16-byte tag
for (int i = 0; i < 16; ++i) {
ciphertext[plaintext_len + i] = (unsigned char)(i * 3);
}
// --- End of placeholder logic ---
return ciphertext;
}
// Placeholder for decryption function
EMSCRIPTEN_KEEPALIVE
std::vector<unsigned char> decrypt_aes_gcm(AesGcmContext* ctx, const unsigned char* ciphertext_with_tag, size_t ciphertext_with_tag_len) {
if (!ctx || ciphertext_with_tag_len < 16) return {}; // Need at least tag length
size_t ciphertext_len = ciphertext_with_tag_len - 16;
std::vector<unsigned char> plaintext(ciphertext_len);
// --- Actual AES-GCM decryption and tag verification logic would go here ---
// For demonstration, we'll just copy the data before the dummy tag
memcpy(plaintext.data(), ciphertext_with_tag, ciphertext_len);
// In a real implementation, verify the tag here.
// --- End of placeholder logic ---
return plaintext;
}
// Function to free the context
EMSCRIPTEN_KEEPALIVE
void free_aes_gcm(AesGcmContext* ctx) {
delete ctx;
}
}
Note: The C++ code above is a highly simplified placeholder. For production use, you must integrate a robust, audited cryptographic library like mbedTLS or OpenSSL, compile it with Emscripten, and implement the actual AES-GCM encryption and decryption logic, including secure key/IV management and tag verification.
B. Compiling with Emscripten
Ensure you have Emscripten SDK installed. You can download it from emscripten.org.
Compile the C++ code into a WebAssembly module and its JavaScript glue code:
emcc crypto.cpp -o crypto.js -s WASM=1 -s EXPORTED_FUNCTIONS='["_init_aes_gcm", "_encrypt_aes_gcm", "_decrypt_aes_gcm", "_free_aes_gcm"]' -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap", "getValue", "setValue", "UTF8ToString", "stringToUTF8", "allocate", "FS"]' -O3
This command:
emcc crypto.cpp -o crypto.js: Compilescrypto.cppand outputscrypto.js(which includes Wasm binary or JS loader).-s WASM=1: Enables WebAssembly output.-s EXPORTED_FUNCTIONS='[...]‘: Makes specific C++ functions callable from JavaScript.-s EXPORTED_RUNTIME_METHODS='[...]‘: Exposes Emscripten runtime functions needed for memory management and data transfer.-O3: Enables aggressive optimization.
This will generate two files: crypto.js (JavaScript glue code) and crypto.wasm (the WebAssembly binary).
II. Creating the WordPress Plugin and Gutenberg Block
We’ll create a simple WordPress plugin to house our custom Gutenberg block. The block will provide an interface for users to input plaintext, encrypt it, and then display the ciphertext. A separate button will allow decryption back to plaintext.
A. Plugin Structure
Create a new folder in wp-content/plugins/ named secure-encrypt-block. Inside, create the following files and directories:
secure-encrypt-block/
├── secure-encrypt-block.php
├── build/
│ ├── index.js
│ ├── index.asset.php
│ └── wasm/
│ ├── crypto.js
│ └── crypto.wasm
└── src/
├── index.js
└── block.json
B. Plugin Main File (secure-encrypt-block.php)
This file registers the block type and enqueues the necessary JavaScript and CSS.
<?php
/**
* Plugin Name: Secure Encrypt Block
* Description: A Gutenberg block for client-side file encryption using WebAssembly.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: secure-encrypt-block
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Registers the block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding `wp_enqueue_scripts` action.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function secure_encrypt_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'secure_encrypt_block_init' );
C. Block Metadata (src/block.json)
Defines the block’s properties, including its name, category, and script/style handles.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "secure-encrypt-block/encrypt-vault",
"version": "0.1.0",
"title": "Secure Encrypt Vault",
"category": "widgets",
"icon": "lock",
"description": "Encrypt and decrypt content client-side using WebAssembly.",
"attributes": {
"encryptedContent": {
"type": "string",
"default": ""
},
"decryptedContent": {
"type": "string",
"default": ""
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./index.js"
}
D. JavaScript for the Block (src/index.js)
This is the core of our Gutenberg block. It handles user input, interacts with the WebAssembly module, and manages the block’s state.
First, we need to load the WebAssembly module. We’ll use Emscripten’s generated JavaScript glue code for this.
// Import necessary WordPress components
const { registerBlockType } = wp.blocks;
const { useState, useEffect, useRef } = wp.element;
const { RichText, InspectorControls } = wp.blockEditor;
const { PanelBody, Button, TextareaControl, Placeholder, Spinner } = wp.components;
const { __ } = wp.i18n;
// --- WebAssembly Module Loading ---
// Emscripten will generate crypto.js and crypto.wasm.
// We need to ensure these are available in the WordPress build process.
// For simplicity, we'll assume they are copied to build/wasm/
// In a real project, use a build tool like Webpack or Rollup to manage this.
let wasmModule = null;
let wasmLoaded = false;
const wasmModulePath = '/wp-content/plugins/secure-encrypt-block/build/wasm/crypto.js'; // Adjust path as needed
// Function to load the Wasm module
async function loadWasmModule() {
if (wasmLoaded) return wasmModule;
return new Promise((resolve, reject) => {
// Emscripten's Module object
const Module = {
print: (text) => console.log('WASM stdout:', text),
printErr: (text) => console.error('WASM stderr:', text),
onRuntimeInitialized: () => {
console.log('WebAssembly module initialized.');
wasmLoaded = true;
resolve(Module);
},
onAbort: (reason) => {
console.error('WebAssembly runtime aborted:', reason);
reject(new Error('WebAssembly runtime aborted.'));
}
};
// Dynamically load the Emscripten-generated JS file
const script = document.createElement('script');
script.src = wasmModulePath;
script.onload = () => {
// The Emscripten Module object is now available globally or attached to window
// We need to ensure it's properly instantiated.
// If Module is not globally defined, you might need to adjust this.
if (typeof Module === 'undefined') {
console.error("Emscripten Module object not found after script load.");
return reject(new Error("Emscripten Module object not found."));
}
// Instantiate the module
const instance = Module(Module); // Pass Module to itself for instantiation
wasmModule = instance;
resolve(instance);
};
script.onerror = (error) => {
console.error('Error loading WebAssembly module:', error);
reject(error);
};
document.body.appendChild(script);
});
}
// --- Helper functions for Wasm interaction ---
// Securely generate a 256-bit key and 96-bit IV (for demonstration)
// In production, manage keys securely (e.g., user-provided, server-generated and stored securely)
function generateSecureKeyAndIv() {
const key = new Uint8Array(32); // 256 bits
const iv = new Uint8Array(12); // 96 bits
window.crypto.getRandomValues(key);
window.crypto.getRandomValues(iv);
return { key, iv };
}
// Convert Uint8Array to hex string for display
function bytesToHexString(bytes) {
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
}
// Convert hex string to Uint8Array
function hexStringToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
// --- Block Registration ---
registerBlockType('secure-encrypt-block/encrypt-vault', {
title: __('Secure Encrypt Vault', 'secure-encrypt-block'),
icon: 'lock',
category: 'widgets',
attributes: {
encryptedContent: {
type: 'string',
default: '',
},
decryptedContent: {
type: 'string',
default: '',
},
// Store key and IV as hex strings for simplicity in attributes
// WARNING: Storing keys directly in block attributes is INSECURE for production.
// This is for demonstration purposes only. Keys should be managed externally.
encryptionKeyHex: {
type: 'string',
default: '',
},
encryptionIvHex: {
type: 'string',
default: '',
},
isEncrypting: {
type: 'boolean',
default: false,
},
isDecrypting: {
type: 'boolean',
default: false,
},
error: {
type: 'string',
default: '',
}
},
edit: ({ attributes, setAttributes }) => {
const { encryptedContent, decryptedContent, encryptionKeyHex, encryptionIvHex, isEncrypting, isDecrypting, error } = attributes;
// State for user input
const [plaintextInput, setPlaintextInput] = useState('');
const [keyInput, setKeyInput] = useState(encryptionKeyHex || '');
const [ivInput, setIvInput] = useState(encryptionIvHex || '');
const [isWasmReady, setIsWasmReady] = useState(false);
// Load Wasm module on component mount
useEffect(() => {
loadWasmModule()
.then(module => {
setIsWasmReady(true);
// If key/IV are not set, generate them
if (!keyInput && !ivInput) {
const { key, iv } = generateSecureKeyAndIv();
const keyHex = bytesToHexString(key);
const ivHex = bytesToHexString(iv);
setKeyInput(keyHex);
setIvInput(ivHex);
setAttributes({ encryptionKeyHex: keyHex, encryptionIvHex: ivHex });
}
})
.catch(err => {
console.error("Failed to load Wasm module:", err);
setAttributes({ error: __('Failed to load encryption module.', 'secure-encrypt-block') });
});
}, []);
// Update attributes when key/IV inputs change
useEffect(() => {
setAttributes({ encryptionKeyHex: keyInput, encryptionIvHex: ivInput });
}, [keyInput, ivInput]);
const handleEncrypt = async () => {
if (!isWasmReady || !wasmModule) {
setAttributes({ error: __('Encryption module not ready.', 'secure-encrypt-block') });
return;
}
if (!keyInput || !ivInput) {
setAttributes({ error: __('Key and IV are required for encryption.', 'secure-encrypt-block') });
return;
}
setAttributes({ isEncrypting: true, error: '' });
try {
const keyBytes = hexStringToBytes(keyInput);
const ivBytes = hexStringToBytes(ivInput);
if (keyBytes.length !== 32 || ivBytes.length !== 12) {
throw new Error('Invalid key or IV length. Key must be 32 bytes, IV must be 12 bytes.');
}
// Allocate memory for plaintext in Wasm heap
const plaintextPtr = wasmModule._malloc(plaintextInput.length + 1);
wasmModule.stringToUTF8(plaintextInput, plaintextPtr, plaintextInput.length + 1);
// Allocate memory for key and IV
const keyPtr = wasmModule._malloc(keyBytes.length);
wasmModule.writeArrayToMemory(keyBytes, keyPtr);
const ivPtr = wasmModule._malloc(ivBytes.length);
wasmModule.writeArrayToMemory(ivBytes, ivPtr);
// Initialize AES context
const ctxPtr = wasmModule.ccall(
'init_aes_gcm',
'number', // return type
['number', 'number', 'number', 'number'], // argument types
[keyPtr, keyBytes.length, ivPtr, ivBytes.length]
);
if (ctxPtr === 0) { // Check for null pointer return
throw new Error('Failed to initialize AES context. Check key/IV length.');
}
// Encrypt
const ciphertextResultPtr = wasmModule.ccall(
'encrypt_aes_gcm',
'number', // return type (pointer to vector)
['number', 'number', 'number'], // argument types (context pointer, plaintext pointer, plaintext length)
[ctxPtr, plaintextPtr, plaintextInput.length]
);
// Get the size of the returned vector (ciphertext + tag)
// Emscripten's cwrap/ccall for std::vector returns a pointer to the vector object.
// We need to read its size and data pointer.
// This part is tricky and depends on Emscripten's memory layout for std::vector.
// A common pattern is that the returned pointer is to a struct containing size and data.
// For simplicity, let's assume `encrypt_aes_gcm` returns a pointer to the data,
// and we need a way to get its size. A better approach would be to have the C++
// function return the size and pass a buffer for the result.
// --- REVISED Wasm Call for better memory management ---
// Let's modify the C++ to return a pointer to the data and its size.
// For now, we'll use a simplified approach assuming the JS glue handles it.
// A more robust solution would involve `allocate` and `getValue` for size.
// Assuming `encrypt_aes_gcm` returns a pointer to the encrypted data (including tag)
// and we need to know its size. Emscripten's default `std::vector` handling might
// return a pointer to the vector's internal buffer.
// A common pattern is to return a struct or use output parameters.
// Let's simulate getting the size and data pointer.
// In a real scenario, you'd use `getValue(pointer, '*')` for the data pointer
// and `getValue(pointer + sizeof(pointer_type), 'i32')` for the size.
// For this example, we'll assume the returned pointer is directly to the data
// and we need to infer size or have C++ return it.
// --- Simplified approach: Assume C++ returns a pointer to the data and we know the size ---
// This is NOT robust. A proper implementation would involve C++ returning size.
// For demonstration, let's assume the returned pointer is to the data and its size is plaintextInput.length + 16 (for tag)
const encryptedDataSize = plaintextInput.length + 16; // Placeholder size
const encryptedDataPtr = wasmModule.ccall(
'encrypt_aes_gcm',
'number', // return type (pointer to data)
['number', 'number', 'number'], // argument types (context pointer, plaintext pointer, plaintext length)
[ctxPtr, plaintextPtr, plaintextInput.length]
);
if (encryptedDataPtr === 0) {
throw new Error('Encryption failed.');
}
// Read the encrypted data from Wasm memory
const encryptedBytes = new Uint8Array(encryptedDataSize);
for (let i = 0; i < encryptedDataSize; ++i) {
encryptedBytes[i] = wasmModule.getValue(encryptedDataPtr + i, 'i8');
}
// Convert to hex string for storage/display
const encryptedContentHex = bytesToHexString(encryptedBytes);
setAttributes({ encryptedContent: encryptedContentHex });
// Clean up Wasm memory
wasmModule._free(plaintextPtr);
wasmModule._free(keyPtr);
wasmModule._free(ivPtr);
wasmModule._free(ctxPtr); // Free the context
// wasmModule._free(encryptedDataPtr); // Free the returned data if C++ allocated it
} catch (e) {
console.error("Encryption error:", e);
setAttributes({ error: e.message || __('An unknown error occurred during encryption.', 'secure-encrypt-block') });
} finally {
setAttributes({ isEncrypting: false });
}
};
const handleDecrypt = async () => {
if (!isWasmReady || !wasmModule) {
setAttributes({ error: __('Encryption module not ready.', 'secure-encrypt-block') });
return;
}
if (!encryptedContent) {
setAttributes({ error: __('No encrypted content to decrypt.', 'secure-encrypt-block') });
return;
}
if (!keyInput || !ivInput) {
setAttributes({ error: __('Key and IV are required for decryption.', 'secure-encrypt-block') });
return;
}
setAttributes({ isDecrypting: true, error: '' });
try {
const encryptedBytes = hexStringToBytes(encryptedContent);
const keyBytes = hexStringToBytes(keyInput);
const ivBytes = hexStringToBytes(ivInput);
if (keyBytes.length !== 32 || ivBytes.length !== 12) {
throw new Error('Invalid key or IV length. Key must be 32 bytes, IV must be 12 bytes.');
}
if (encryptedBytes.length < 16) { // Minimum size for ciphertext + tag
throw new Error('Invalid encrypted data format.');
}
// Allocate memory for encrypted data (ciphertext + tag)
const encryptedDataPtr = wasmModule._malloc(encryptedBytes.length);
wasmModule.writeArrayToMemory(encryptedBytes, encryptedDataPtr);
// Allocate memory for key and IV
const keyPtr = wasmModule._malloc(keyBytes.length);
wasmModule.writeArrayToMemory(keyBytes, keyPtr);
const ivPtr = wasmModule._malloc(ivBytes.length);
wasmModule.writeArrayToMemory(ivBytes, ivPtr);
// Initialize AES context
const ctxPtr = wasmModule.ccall(
'init_aes_gcm',
'number',
['number', 'number', 'number', 'number'],
[keyPtr, keyBytes.length, ivPtr, ivBytes.length]
);
if (ctxPtr === 0) {
throw new Error('Failed to initialize AES context. Check key/IV length.');
}
// Decrypt
const decryptedResultPtr = wasmModule.ccall(
'decrypt_aes_gcm',
'number', // return type (pointer to vector)
['number', 'number', 'number'], // argument types (context pointer, ciphertext pointer, ciphertext length)
[ctxPtr, encryptedDataPtr, encryptedBytes.length]
);
if (decryptedResultPtr === 0) {
throw new Error('Decryption failed. Possibly invalid key, IV, or corrupted data.');
}
// Read the decrypted data from Wasm memory
// Assuming the C++ function returns a pointer to the plaintext data
// and its size is encryptedBytes.length - 16 (for tag)
const decryptedDataSize = encryptedBytes.length - 16; // Placeholder size
const decryptedBytes = new Uint8Array(decryptedDataSize);
for (let i = 0; i < decryptedDataSize; ++i) {
decryptedBytes[i] = wasmModule.getValue(decryptedResultPtr + i, 'i8');
}
const decryptedContent = wasmModule.UTF8ToString(decryptedResultPtr); // If C++ returns a null-terminated string
setAttributes({ decryptedContent: decryptedContent });
// Clean up Wasm memory
wasmModule._free(encryptedDataPtr);
wasmModule._free(keyPtr);
wasmModule._free(ivPtr);
wasmModule._free(ctxPtr);
// wasmModule._free(decryptedResultPtr); // Free if C++ allocated it
} catch (e) {
console.error("Decryption error:", e);
setAttributes({ error: e.message || __('An unknown error occurred during decryption.', 'secure-encrypt-block') });
} finally {
setAttributes({ isDecrypting: false });
}
};
// Render placeholder if Wasm is not ready
if (!isWasmReady) {
return (
<Placeholder
icon={Spinner}
label={__('Secure Encrypt Vault', 'secure-encrypt-block')}
instructions={__('Loading encryption module...', 'secure-encrypt-block')}
/>
);
}
return (
<>
{/* Inspector Controls for settings */}
<InspectorControls>
<PanelBody title={__('Encryption Settings', 'secure-encrypt-block')} initialOpen={true}>
<TextareaControl
label={__('Encryption Key (Hex)', 'secure-encrypt-block')}
value={keyInput}
onChange={setKeyInput}
help={__('Must be 32 bytes (64 hex characters). Generate securely.', 'secure-encrypt-block')}
rows={3}
/>
<TextareaControl
label={__('Initialization Vector (Hex)', 'secure-encrypt-block')}
value={ivInput}
onChange={setIvInput}
help={__('Must be 12 bytes (24 hex characters). Generate securely.', 'secure-encrypt-block')}
rows={3}
/>
<Button isSecondary onClick={() => {
const { key, iv } = generateSecureKeyAndIv();
const keyHex = bytesToHexString(key);
const ivHex = bytesToHexString(iv);
setKeyInput(keyHex);
setIvInput(ivHex);
setAttributes({ encryptionKeyHex: keyHex, encryptionIvHex: ivHex });
}}>
{__('Generate New Key/IV', 'secure-encrypt-block')}
</Button>
</PanelBody>
</InspectorControls>
{/* Block Content */}
<div className="secure-encrypt-block-editor">
<h3>{__('Plaintext Input', 'secure-encrypt-block')}</h3>
<RichText
tagName="div"
value={plaintextInput}
onChange={setPlaintextInput}
placeholder={__('Enter text to encrypt...', 'secure-encrypt-block')}
allowedFormats={[]}
className="secure-encrypt-block-plaintext-input"
/>
<div className="secure-encrypt-block-actions">
<Button
isPrimary
onClick={handleEncrypt}
disabled={isEncrypting || !plaintextInput || !isWasmReady}
isBusy={isEncrypting}
>
{__('Encrypt', 'secure-encrypt-block')}
</Button>
</div>
{encryptedContent && (
<>
<h3>{__('Encrypted Content (Hex)', 'secure-encrypt-block')}</h3>
<RichText
tagName="div"
value={encryptedContent}
onChange={(newVal) => setAttributes({ encryptedContent: newVal })}
placeholder={__('Encrypted data will appear here...', 'secure-encrypt-block')}
readOnly
className="secure-encrypt-block-encrypted-output"
/>
<div className="secure-encrypt-block-actions">
<Button
isSecondary
onClick={handleDecrypt}
disabled={isDecrypting || !encryptedContent || !isWasmReady}
isBusy={isDecrypting}
>
{__('Decrypt', 'secure-encrypt-block')}
</Button>
</div>
</>
)}
{decryptedContent && (
<>
<h3>{__('Decrypted Content', 'secure-encrypt-block')}</h3>
<RichText
tagName="div"
value={decryptedContent}
onChange={(newVal) => setAttributes({ decryptedContent: newVal })}
placeholder={__('Decrypted data will appear here...', 'secure-encrypt-block')}
readOnly
className="secure-encrypt-block-decrypted-output"
/>
</>
)}
{error && (
<div className="secure-encrypt-block-error">
{__('Error:', 'secure-encrypt-block')} {error}
</div>
)}
</div>
</>
);
},
save: ({ attributes }) => {
const { encryptedContent } = attributes;
// The save function should output static HTML.
// We cannot run JavaScript or Wasm here.
// Therefore, we only save the encrypted content.
// Decryption will be handled by the frontend JavaScript when the page loads.
return (
<div className="secure-encrypt-block-frontend" data-encrypted-content={encryptedContent}>
<p>{__('Content is encrypted. Decryption requires JavaScript.', 'secure-encrypt-block')}</p>
{/* Optionally, display a placeholder or a message */}
</div>
);
},
});
// --- Frontend Script for Decryption ---
// This part runs on the frontend to decrypt content saved by the block.
document.addEventListener('DOMContentLoaded', () => {
const encryptBlockElements = document.querySelectorAll('.secure-encrypt-block-frontend');
encryptBlockElements.forEach(blockElement => {
const encryptedContentHex = blockElement.dataset.encryptedContent;
if (!encryptedContentHex) return;
// We need to load the Wasm module again for the frontend.
// This is a simplified approach. In a real app, you'd likely have a single
// Wasm loader for both editor and frontend, or enqueue a separate script.
loadW