Step-by-Step Guide to building a custom secure file encryption vault block for Gutenberg using HTMX dynamic attributes
Gutenberg Block Structure and Initialization
We’ll begin by defining the core structure of our Gutenberg block. This involves registering the block type in PHP and defining its JavaScript components for the editor interface. For a custom file encryption vault, we need fields for file uploads, encryption keys, and potentially a password for key protection. The block’s attributes will store the state of these elements.
First, let’s set up the PHP registration. This typically resides in your plugin’s main file or a dedicated `blocks.php` file.
PHP Block Registration
The `register_block_type` function is our entry point. We’ll point it to a `block.json` file which will define our block’s metadata and script dependencies.
`my-encryption-vault.php` (Plugin Main File)
<?php
/**
* Plugin Name: My Encryption Vault Block
* Description: A custom Gutenberg block for secure file encryption.
* Version: 1.0.0
* Author: Antigravity
* License: GPL-2.0-or-later
* Text Domain: my-encryption-vault
*/
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 context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function my_encryption_vault_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'my_encryption_vault_block_init' );
`block.json` Metadata and Script Dependencies
The `block.json` file is crucial. It defines the block’s name, category, attributes, and importantly, the entry points for our JavaScript and CSS assets. We’ll specify our main JavaScript file for the editor and frontend, and a separate file for the editor-only scripts.
{
"apiVersion": 2,
"name": "my-encryption-vault/vault",
"version": "0.1.0",
"title": "Secure File Vault",
"category": "widgets",
"icon": "lock",
"description": "A secure vault for encrypting and storing files.",
"keywords": ["encryption", "security", "vault", "file"],
"attributes": {
"fileUrl": {
"type": "string",
"default": ""
},
"fileName": {
"type": "string",
"default": ""
},
"encryptionKey": {
"type": "string",
"default": ""
},
"keyPassword": {
"type": "string",
"default": ""
}
},
"supports": {
"html": false
},
"textdomain": "my-encryption-vault",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
JavaScript: Editor Interface and State Management
The JavaScript side is where the magic happens for the Gutenberg editor. We’ll use React components to build the UI for our block. This includes file upload controls, input fields for keys and passwords, and buttons for actions. We’ll leverage the `useBlockProps` hook for standard block wrapper props and `RichText` for editable fields if necessary, though for sensitive data like keys, standard input fields are preferred.
Editor Component (`src/edit.js`)
This file defines how the block appears and behaves within the Gutenberg editor. We’ll use `useState` for managing local component state and `setAttributes` to update the block’s stored attributes.
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, FileUpload } from '@wordpress/components';
import { useState } from '@wordpress/element';
import './editor.scss';
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
const { fileUrl, fileName, encryptionKey, keyPassword } = attributes;
const [selectedFile, setSelectedFile] = useState(null);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file) {
setSelectedFile(file);
// In a real-world scenario, you'd likely upload this file via AJAX
// and store its URL or a reference. For this example, we'll simulate.
setAttributes({ fileName: file.name });
// We won't set fileUrl directly here as it's a local file.
// The actual upload mechanism would handle this.
}
};
const handleKeyChange = (newKey) => {
setAttributes({ encryptionKey: newKey });
};
const handlePasswordChange = (newPassword) => {
setAttributes({ keyPassword: newPassword });
};
return (
<>
<InspectorControls>
<PanelBody title={__('File Settings', 'my-encryption-vault')} initialOpen={true}>
<div>
<label>{__('Upload File:', 'my-encryption-vault')}</label>
<input type="file" onChange={handleFileChange} />
{selectedFile && (
<p>{__('Selected: ', 'my-encryption-vault')}{selectedFile.name}</p>
)}
{!selectedFile && fileUrl && (
<p>{__('Current File: ', 'my-encryption-vault')}{fileName}</p>
)}
</div>
</PanelBody>
<PanelBody title={__('Encryption Settings', 'my-encryption-vault')}>
<TextControl
label={__('Encryption Key (Base64)', 'my-encryption-vault')}
value={encryptionKey}
onChange={handleKeyChange}
help={__('Enter your AES-256 encryption key in Base64 format.', 'my-encryption-vault')}
/>
<TextControl
label={__('Key Password (Optional)', 'my-encryption-vault')}
type="password"
value={keyPassword}
onChange={handlePasswordChange}
help={__('Password to encrypt/decrypt the encryption key itself.', 'my-encryption-vault')}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<p>{__('Secure File Vault Block', 'my-encryption-vault')}</p>
{selectedFile ? (
<p>{__('File ready for upload: ', 'my-encryption-vault')}{selectedFile.name}</p>
) : fileUrl ? (
<p>{__('Stored File: ', 'my-encryption-vault')}{fileName}</p>
) : (
<p>{__('No file selected.', 'my-encryption-vault')}</p>
)}
<p>{__('Encryption Key: ', 'my-encryption-vault')}{encryptionKey ? '********' : __('Not set', 'my-encryption-vault')}</p>
<p>{__('Key Password: ', 'my-encryption-vault')}{keyPassword ? '********' : __('Not set', 'my-encryption-vault')}</p>
</div>
</>
);
}
Save Component (`src/save.js`)
The `save` function determines how the block’s content is rendered on the frontend. For a secure vault, we don’t want to expose sensitive data like the encryption key directly. Instead, we’ll render a placeholder or a download link that triggers a server-side process for decryption and download.
import { useBlockProps } from '@wordpress/block-editor';
export default function save({ attributes }) {
const blockProps = useBlockProps.save();
const { fileUrl, fileName, encryptionKey, keyPassword } = attributes;
// On the frontend, we don't render the key or password directly.
// We'll use HTMX to trigger a server-side download/decryption.
// The fileUrl attribute would point to a REST API endpoint or a custom URL.
return (
<div {...blockProps}>
<p>{__('Secure File Vault', 'my-encryption-vault')}</p>
{fileName ? (
<p
hx-get={`/wp-json/my-encryption-vault/v1/download?file=${encodeURIComponent(fileUrl)}&filename=${encodeURIComponent(fileName)}`}
hx-target="this"
hx-swap="outerHTML"
style="cursor: pointer; text-decoration: underline; color: blue;"
>
{__('Download encrypted file: ', 'my-encryption-vault')}{fileName}
</p>
) : (
<p>{__('No file configured.', 'my-encryption-vault')}</p>
)}
{/* The encryptionKey and keyPassword are NOT saved here. */}
</div>
);
}
Index File (`src/index.js`)
This is the main JavaScript entry point that registers the block with Gutenberg, linking the `edit` and `save` components.
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';
import metadata from '../block.json';
registerBlockType(metadata.name, {
edit: Edit,
save,
});
Frontend Rendering with HTMX
The real power for dynamic, server-driven interactions without full page reloads comes from HTMX. In our `save.js` component, we’ve added HTMX attributes to a paragraph element. When this element is clicked, it will trigger an AJAX request to a WordPress REST API endpoint.
HTMX Attributes Explained
hx-get: Specifies the URL to fetch via a GET request. We’re pointing to a custom REST API endpoint.hx-target="this": Tells HTMX to replace the content of the element itself with the response from the server.hx-swap="outerHTML": Defines how the content is swapped. `outerHTML` means the entire element (the `<p>` tag and its contents) will be replaced.
This setup allows us to present a clickable link on the frontend. Upon clicking, HTMX will send a request to our backend, which will then handle the file retrieval, decryption, and return the actual file content for download.
Backend: REST API Endpoint for File Download and Decryption
We need a WordPress REST API endpoint to handle the download request initiated by HTMX. This endpoint will receive the encrypted file’s identifier (e.g., its URL or ID), retrieve it, decrypt it using the provided key (which must be securely managed server-side or passed via a secure mechanism), and then serve the decrypted file for download.
Registering the REST API Route
We’ll use the `rest_api_init` action hook to register our custom route.
<?php
/**
* Register REST API route for file download.
*/
function my_encryption_vault_register_routes() {
register_rest_route( 'my-encryption-vault/v1', '/download', array(
'methods' => WP_REST_Server::READABLE, // Equivalent to GET
'callback' => 'my_encryption_vault_handle_download',
'permission_callback' => '__return_true', // For simplicity, allow all. Implement proper auth in production.
'args' => array(
'file' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
),
'filename' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
) );
}
add_action( 'rest_api_init', 'my_encryption_vault_register_routes' );
Handling the Download and Decryption
The callback function `my_encryption_vault_handle_download` will perform the core logic. For security, the encryption key should ideally not be passed directly from the frontend. A more robust solution would involve server-side key management or a mechanism to securely retrieve the key based on user authentication or a pre-shared secret.
For this example, we’ll assume the encryption key is available server-side. We’ll use PHP’s OpenSSL extension for AES-256 encryption/decryption. You’ll need to install a PHP library for Base64 decoding and potentially a robust crypto library if OpenSSL is not sufficient.
/**
* Handles the file download and decryption.
*
* @param WP_REST_Request $request Full data.
* @return WP_REST_Response|WP_Error JSON response.
*/
function my_encryption_vault_handle_download( WP_REST_Request $request ) {
$file_url = $request->get_param( 'file' );
$filename = $request->get_param( 'filename' );
// --- SECURITY NOTE ---
// The encryption key and password should NOT be passed from the frontend in a real application.
// They should be securely stored server-side or derived through a secure authentication process.
// For demonstration purposes, we'll simulate retrieving them.
// In a production environment, fetch these from secure storage (e.g., wp-config.php constants, a secure vault service).
// Example: Fetching from constants (ensure these are defined securely)
$encryption_key_base64 = defined('MY_ENCRYPTION_VAULT_KEY') ? MY_ENCRYPTION_VAULT_KEY : null;
$key_password = defined('MY_ENCRYPTION_VAULT_PASSWORD') ? MY_ENCRYPTION_VAULT_PASSWORD : null; // If key is password-protected
if ( ! $encryption_key_base64 ) {
return new WP_Error( 'missing_key', __( 'Encryption key is not configured server-side.', 'my-encryption-vault' ), array( 'status' => 500 ) );
}
// Decode the key if it's Base64 encoded
$encryption_key = base64_decode($encryption_key_base64);
if ($encryption_key === false) {
return new WP_Error( 'invalid_key_format', __( 'Invalid encryption key format.', 'my-encryption-vault' ), array( 'status' => 500 ) );
}
// --- Key Password Handling (Optional, for PGP-like key encryption) ---
// If your encryption key itself is encrypted with a password, you'd decrypt it here.
// This example assumes $encryption_key is the raw AES key.
// If $encryption_key_base64 was a password-protected key, you'd need a library like OpenSSL's EVP_Decrypt or similar.
// --- File Retrieval ---
// This is a critical part. How do you get the encrypted file?
// It could be stored on the server's filesystem, in an S3 bucket, etc.
// For this example, we'll assume $file_url is a path to a file on the server.
// In a real scenario, you'd validate $file_url to prevent directory traversal.
$encrypted_file_path = ABSPATH . 'uploads/encrypted_files/' . basename($file_url); // Example path
if ( ! file_exists( $encrypted_file_path ) ) {
return new WP_Error( 'file_not_found', __( 'Encrypted file not found.', 'my-encryption-vault' ), array( 'status' => 404 ) );
}
$encrypted_data = file_get_contents( $encrypted_file_path );
if ( $encrypted_data === false ) {
return new WP_Error( 'read_error', __( 'Could not read encrypted file.', 'my-encryption-vault' ), array( 'status' => 500 ) );
}
// --- Decryption ---
// Assuming AES-256-CBC encryption. The IV must be stored with the ciphertext.
// A common method is to prepend the IV to the ciphertext.
$iv_length = openssl_cipher_iv_length('aes-256-cbc');
if ($iv_length === false) {
return new WP_Error( 'cipher_error', __( 'Could not determine IV length.', 'my-encryption-vault' ), array( 'status' => 500 ) );
}
$iv = substr($encrypted_data, 0, $iv_length);
$ciphertext = substr($encrypted_data, $iv_length);
if (strlen($iv) !== $iv_length) {
return new WP_Error( 'invalid_data', __( 'Invalid encrypted data format (IV too short).', 'my-encryption-vault' ), array( 'status' => 400 ) );
}
$decrypted_data = openssl_decrypt( $ciphertext, 'aes-256-cbc', $encryption_key, OPENSSL_RAW_DATA, $iv );
if ( $decrypted_data === false ) {
// Check for OpenSSL errors
$openssl_error = openssl_error_string();
return new WP_Error( 'decryption_failed', __( 'Decryption failed.', 'my-encryption-vault' ) . ($openssl_error ? " ({$openssl_error})" : ''), array( 'status' => 500 ) );
}
// --- Prepare for Download ---
$decrypted_filename = $filename; // Use the original filename
$content_type = mime_content_type($encrypted_file_path); // Try to guess content type
// Set headers for download
header( 'Content-Description: File Transfer' );
header( 'Content-Type: ' . ($content_type ?: 'application/octet-stream') );
header( 'Content-Disposition: attachment; filename="' . $decrypted_filename . '"' );
header( 'Expires: 0' );
header( 'Cache-Control: must-revalidate' );
header( 'Pragma: public' );
header( 'Content-Length: ' . strlen( $decrypted_data ) );
// Output the decrypted file content
echo $decrypted_data;
exit; // Important to stop further WordPress execution
}
Security Considerations and Best Practices
Handling encryption keys and sensitive files requires extreme caution. The provided backend code is a simplified example. In a production environment, you must:
- Never pass encryption keys or passwords from the frontend to the backend via GET parameters or request bodies. Use secure server-side storage for keys (e.g., environment variables, dedicated secrets management systems, or encrypted configuration files).
- Implement robust authentication and authorization. Ensure only authorized users can access the download endpoint and decrypt files.
- Validate and sanitize all inputs rigorously. Prevent directory traversal attacks and other vulnerabilities.
- Use strong, modern encryption algorithms. AES-256-CBC is a good start, but consider authenticated encryption modes like AES-GCM for added integrity checks.
- Manage IVs securely. The Initialization Vector (IV) must be unique for each encryption operation and is typically prepended to the ciphertext.
- Consider key rotation and secure key deletion policies.
- For file storage, use secure locations outside the web root if possible, or implement strict file permissions.
- Use a dedicated PHP crypto library if OpenSSL’s capabilities are insufficient or if you need more advanced features.
Build Process and Deployment
To build the JavaScript and CSS assets, you’ll need a Node.js environment and the WordPress `@wordpress/scripts` package. Navigate to your plugin’s root directory in your terminal and run:
npm install npm run build
This will compile your React components and SCSS files into the `build` directory, as specified in `block.json`. After building, you can activate the plugin in WordPress and use the “Secure File Vault” block in the Gutenberg editor.