Step-by-Step Guide to building a custom secure file encryption vault block for Gutenberg using Tailwind CSS isolated elements
Gutenberg Block Architecture for Secure File Encryption
Developing a custom Gutenberg block for a secure file encryption vault necessitates a robust architectural approach, prioritizing security, user experience, and maintainability. This guide outlines the construction of such a block, focusing on isolated Tailwind CSS elements for a clean, modern UI and leveraging PHP for backend encryption logic. We will abstract the encryption mechanism to allow for future cryptographic algorithm upgrades and ensure data integrity through robust validation.
Plugin Structure and Initialization
A standard WordPress plugin structure is employed. The main plugin file will register the Gutenberg block and enqueue necessary scripts and styles. For this block, we’ll use a dedicated JavaScript file for the block’s editor interface and a PHP file for server-side encryption/decryption operations.
Plugin Main File (`secure-file-vault.php`)
This file serves as the entry point for our plugin. It hooks into WordPress actions to register the block type and enqueue assets.
<?php
/**
* Plugin Name: Secure File Vault Block
* Description: A Gutenberg block for secure file encryption and decryption.
* Version: 1.0.0
* Author: Antigravity
* License: GPL-2.0-or-later
* Text Domain: secure-file-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 secure_file_vault_register_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'secure_file_vault_register_block' );
/**
* Enqueue front-end assets.
*/
function secure_file_vault_enqueue_frontend_assets() {
// Only enqueue if the block is present on the page.
if ( has_block( 'secure-file-vault/vault' ) ) {
wp_enqueue_script(
'secure-file-vault-frontend',
plugin_dir_url( __FILE__ ) . 'build/frontend.js',
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'assets/js/frontend.js' ),
true
);
wp_enqueue_style(
'secure-file-vault-frontend-style',
plugin_dir_url( __FILE__ ) . 'build/frontend.css',
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'assets/css/frontend.css' )
);
}
}
add_action( 'wp_enqueue_scripts', 'secure_file_vault_enqueue_frontend_assets' );
/**
* Enqueue editor assets.
*/
function secure_file_vault_enqueue_editor_assets() {
wp_enqueue_script(
'secure-file-vault-editor-script',
plugin_dir_url( __FILE__ ) . 'build/index.js',
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'assets/js/index.js' )
);
wp_enqueue_style(
'secure-file-vault-editor-style',
plugin_dir_url( __FILE__ ) . 'build/index.css',
array( 'wp-edit-blocks' ),
filemtime( plugin_dir_path( __FILE__ ) . 'assets/css/editor.css' )
);
}
add_action( 'enqueue_block_editor_assets', 'secure_file_vault_enqueue_editor_assets' );
// Server-side encryption/decryption endpoint (example)
// This would typically be handled via AJAX or a REST API endpoint.
// For simplicity, we'll outline the concept here.
// A more robust implementation would use WP REST API.
// add_action( 'wp_ajax_encrypt_file', 'secure_file_vault_handle_encryption' );
// add_action( 'wp_ajax_decrypt_file', 'secure_file_vault_handle_decryption' );
// function secure_file_vault_handle_encryption() {
// // ... encryption logic ...
// }
// function secure_file_vault_handle_decryption() {
// // ... decryption logic ...
// }
Block Configuration (`block.json`)
The `block.json` file defines the block’s metadata, including its name, title, icon, category, and script/style dependencies. This is crucial for Gutenberg to recognize and load the block correctly.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "secure-file-vault/vault",
"version": "1.0.0",
"title": "Secure File Vault",
"category": "security",
"icon": "lock",
"description": "A secure block for encrypting and decrypting files.",
"keywords": [ "encryption", "security", "file", "vault" ],
"attributes": {
"fileContent": {
"type": "string",
"default": ""
},
"encryptionKey": {
"type": "string",
"default": ""
},
"isEncrypted": {
"type": "boolean",
"default": false
}
},
"supports": {
"html": false
},
"textdomain": "secure-file-vault",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/frontend.css"
}
Editor Interface Development (JavaScript)
The editor interface is built using React and the WordPress Block Editor components. We’ll use Tailwind CSS for styling, ensuring a consistent and modern look. The core logic will involve handling file uploads (or direct text input for simplicity in this example), key input, and triggering encryption/decryption actions.
Editor Script (`assets/js/index.js`)
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import {
PanelBody,
TextareaControl,
Button,
Spinner,
Placeholder,
__experimentalUseCustomUnits as useUnits,
} from '@wordpress/components';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './style.scss'; // Editor styles
const Edit = ( { attributes, setAttributes } ) => {
const { fileContent, encryptionKey, isEncrypted } = attributes;
const [ isLoading, setIsLoading ] = useState( false );
const [ error, setError ] = useState( null );
const [ decryptedContent, setDecryptedContent ] = useState( '' );
const blockProps = useBlockProps();
// Effect to initialize decryptedContent when block loads or isEncrypted changes
useEffect( () => {
if ( isEncrypted && fileContent ) {
// If it's encrypted, we assume fileContent holds the encrypted data.
// We don't automatically decrypt on load in the editor for security.
// The user must provide the key and initiate decryption.
setDecryptedContent( '' ); // Clear any previous decrypted content
} else {
setDecryptedContent( fileContent ); // If not encrypted, display raw content
}
}, [ fileContent, isEncrypted ] );
const handleFileChange = ( event ) => {
const file = event.target.files[ 0 ];
if ( ! file ) {
return;
}
const reader = new FileReader();
reader.onload = ( e ) => {
setAttributes( { fileContent: e.target.result, isEncrypted: false } );
setDecryptedContent( e.target.result ); // Update visible content
setError( null );
};
reader.onerror = () => {
setError( __( 'Error reading file.', 'secure-file-vault' ) );
setAttributes( { fileContent: '', isEncrypted: false } );
setDecryptedContent( '' );
};
reader.readAsText( file ); // Read as text for simplicity; binary would need different handling
};
const handleKeyChange = ( value ) => {
setAttributes( { encryptionKey: value } );
setError( null );
};
const handleContentChange = ( value ) => {
setAttributes( { fileContent: value, isEncrypted: false } );
setDecryptedContent( value ); // Update visible content
setError( null );
};
const handleEncrypt = async () => {
if ( ! encryptionKey ) {
setError( __( 'Encryption key is required.', 'secure-file-vault' ) );
return;
}
if ( ! fileContent ) {
setError( __( 'No content to encrypt.', 'secure-file-vault' ) );
return;
}
setIsLoading( true );
setError( null );
try {
// In a real-world scenario, this would be an AJAX call to a PHP endpoint.
// For this example, we'll simulate it.
// const response = await wp.apiFetch({
// path: '/secure-file-vault/v1/encrypt',
// method: 'POST',
// data: { content: fileContent, key: encryptionKey },
// });
// if (response.success) {
// setAttributes({ fileContent: response.data.encryptedContent, isEncrypted: true });
// setDecryptedContent(''); // Clear decrypted view
// } else {
// throw new Error(response.data.message || __('Encryption failed.', 'secure-file-vault'));
// }
// --- SIMULATED ENCRYPTION ---
// Replace with actual crypto library and PHP backend call
const simulatedEncryptedContent = btoa(
JSON.stringify({
data: fileContent,
iv: 'simulated_iv', // In real crypto, generate a random IV
keyHash: 'simulated_key_hash' // In real crypto, hash the key for comparison
})
);
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
setAttributes( { fileContent: simulatedEncryptedContent, isEncrypted: true } );
setDecryptedContent( '' ); // Clear decrypted view
// --- END SIMULATION ---
} catch ( e ) {
setError( e.message );
} finally {
setIsLoading( false );
}
};
const handleDecrypt = async () => {
if ( ! encryptionKey ) {
setError( __( 'Encryption key is required.', 'secure-file-vault' ) );
return;
}
if ( ! fileContent || ! isEncrypted ) {
setError( __( 'No encrypted content to decrypt.', 'secure-file-vault' ) );
return;
}
setIsLoading( true );
setError( null );
try {
// AJAX call to PHP endpoint
// const response = await wp.apiFetch({
// path: '/secure-file-vault/v1/decrypt',
// method: 'POST',
// data: { encryptedContent: fileContent, key: encryptionKey },
// });
// if (response.success) {
// setAttributes({ fileContent: response.data.decryptedContent, isEncrypted: false });
// setDecryptedContent(response.data.decryptedContent);
// } else {
// throw new Error(response.data.message || __('Decryption failed.', 'secure-file-vault'));
// }
// --- SIMULATED DECRYPTION ---
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
const decodedPayload = JSON.parse(atob(fileContent));
// In a real scenario, verify keyHash and decrypt using IV and key
if (decodedPayload.iv === 'simulated_iv') { // Basic check
setAttributes( { fileContent: decodedPayload.data, isEncrypted: false } );
setDecryptedContent( decodedPayload.data );
} else {
throw new Error( __( 'Decryption failed: Invalid data or key.', 'secure-file-vault' ) );
}
// --- END SIMULATION ---
} catch ( e ) {
setError( e.message );
} finally {
setIsLoading( false );
}
};
const renderEditorContent = () => {
if ( isLoading ) {
return (
{ __( 'Processing...', 'secure-file-vault' ) }
);
}
if ( error ) {
return (
{ error }
);
}
if ( isEncrypted ) {
return (
{ __( 'File is currently encrypted.', 'secure-file-vault' ) }
);
}
return (
{ __( 'Supports .txt files.', 'secure-file-vault' ) }
);
};
return (
{ /* Additional settings could go here */ }
{ renderEditorContent() }
);
};
registerBlockType( 'secure-file-vault/vault', {
edit: Edit,
save: ( { attributes } ) => {
const { fileContent, isEncrypted } = attributes;
// The 'save' function should only output static HTML.
// Dynamic behavior (like decryption on the front-end) will be handled by frontend.js.
// We store the encrypted content or raw content as is.
return (
<div className="wp-block-secure-file-vault-vault" data-is-encrypted={ isEncrypted ? 'true' : 'false' }>
{ /* The actual content is stored in a hidden attribute or data attribute for frontend.js to access */ }
<div className="sfv-content-wrapper" style="display: none;">{ fileContent }</div>
</div>
);
},
} );
Editor Styles (`assets/css/editor.css`)
These styles are specific to the Gutenberg editor. We’ll use Tailwind CSS classes here, assuming Tailwind is configured for the WordPress build process (e.g., via `@wordpress/scripts`).
/* Import Tailwind CSS */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles for the block editor */
.wp-block-secure-file-vault-vault {
@apply border border-gray-300 p-4 rounded-md shadow-sm;
}
.sfv-loading-container {
@apply flex flex-col items-center justify-center py-8;
}
.sfv-error-message {
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4;
}
.sfv-editor-content--encrypted .sfv-status-message {
@apply text-yellow-700 bg-yellow-100 border border-yellow-400 px-4 py-3 rounded mb-4;
}
.sfv-file-input-wrapper {
@apply mb-4;
}
.sfv-file-input-label {
@apply block text-sm font-medium text-gray-700 mb-1 cursor-pointer bg-gray-50 border border-gray-300 rounded-md px-3 py-2 shadow-sm focus-within:ring-indigo-500 focus-within:border-indigo-500;
transition: background-color 0.2s ease;
}
.sfv-file-input-label:hover {
@apply bg-gray-100;
}
.sfv-file-input {
@apply sr-only; /* Hide the actual input */
}
.sfv-file-input-hint {
@apply text-xs text-gray-500 mt-1 block;
}
/* Ensure TextareaControl and Button styles are overridden or compatible */
.components-textarea-control__container,
.components-button {
@apply mt-4;
}
.components-button.sfv-button-encrypt,
.components-button.sfv-button-decrypt {
@apply w-full justify-center;
}
Frontend Rendering and Interaction (JavaScript)
The frontend JavaScript handles the display of the block on the actual website. It needs to be able to read the stored encrypted content and provide an interface for users to input their key and decrypt the file. This interaction should be secure and avoid exposing sensitive data unnecessarily.
Frontend Script (`assets/js/frontend.js`)
/**
* Frontend script for the Secure File Vault block.
*/
document.addEventListener( 'DOMContentLoaded', () => {
const vaultBlocks = document.querySelectorAll( '.wp-block-secure-file-vault-vault' );
vaultBlocks.forEach( ( block ) => {
const contentWrapper = block.querySelector( '.sfv-content-wrapper' );
if ( ! contentWrapper ) {
return;
}
const encryptedData = contentWrapper.textContent.trim();
const isEncrypted = block.dataset.isEncrypted === 'true';
if ( ! encryptedData ) {
return; // No content to display
}
// Render initial state based on isEncrypted attribute
if ( isEncrypted ) {
renderEncryptedState( block, encryptedData );
} else {
renderDecryptedState( block, encryptedData );
}
} );
// Event delegation for dynamically added elements (if any)
document.body.addEventListener( 'click', ( event ) => {
if ( event.target.classList.contains( 'sfv-decrypt-button' ) ) {
const block = event.target.closest( '.wp-block-secure-file-vault-vault' );
const keyInput = block.querySelector( '.sfv-key-input' );
const encryptedData = block.querySelector( '.sfv-content-wrapper' ).textContent.trim();
const errorMessageContainer = block.querySelector( '.sfv-error-message' );
if ( ! keyInput || ! encryptedData ) return;
const key = keyInput.value;
if ( ! key ) {
displayError( errorMessageContainer, 'Encryption key is required.' );
return;
}
// Clear previous error
clearError( errorMessageContainer );
// Simulate decryption (replace with actual AJAX call)
try {
// --- SIMULATED DECRYPTION ---
const decodedPayload = JSON.parse( atob( encryptedData ) );
// In a real scenario, verify keyHash and decrypt using IV and key
if ( decodedPayload.iv === 'simulated_iv' ) { // Basic check
renderDecryptedState( block, decodedPayload.data );
} else {
throw new Error( 'Decryption failed: Invalid data or key.' );
}
// --- END SIMULATION ---
} catch ( e ) {
displayError( errorMessageContainer, e.message );
}
}
} );
} );
function renderEncryptedState( block, encryptedData ) {
block.innerHTML = `
`;
}
function renderDecryptedState( block, decryptedContent ) {
block.innerHTML = `
`;
}
function displayError( container, message ) {
container.textContent = message;
container.style.display = 'block';
}
function clearError( container ) {
container.textContent = '';
container.style.display = 'none';
}
// Helper for i18n on frontend (requires wp_localize_script)
const __ = ( text, domain ) => {
// In a real plugin, this would be replaced by WordPress's i18n functions
// after wp_localize_script is used to pass translations.
// For this example, we'll use a simple placeholder.
return text;
};
Frontend Styles (`assets/css/frontend.css`)
/* Import Tailwind CSS */
@tailwind base;
@tailwind components;
@tailwind utilities;
.wp-block-secure-file-vault-vault {
/* Base styles for the block container on the frontend */
@apply my-4;
}
.sfv-vault-container {
@apply border border-gray-300 p-6 rounded-lg shadow-lg bg-white;
}
.sfv-vault-container--encrypted .sfv-status-message {
@apply text-blue-700 bg-blue-100 border border-blue-400 px-4 py-3 rounded mb-4;
}
.sfv-input-group {
@apply mb-4;
}
.sfv-label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.sfv-key-input,
.sfv-decrypted-textarea {
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500;
font-family: inherit; /* Ensure consistent font */
}
.sfv-decrypted-textarea {
@apply bg-gray-50 cursor-default;
min-height: 200px; /* Ensure reasonable height */
resize: vertical; /* Allow vertical resizing */
}
.sfv-button {
@apply px-4 py-2 rounded-md font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
.sfv-button--primary {
@apply bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500;
}
.sfv-button--primary:hover {
@apply shadow-md;
}
.sfv-error-message {
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 hidden; /* Hidden by default */
}
/* Hide the content wrapper */
.sfv-content-wrapper {
display: none !important;
}
Backend Encryption/Decryption Logic (PHP)
The actual encryption and decryption should happen on the server-side for security. This prevents sensitive keys from being exposed in the client-side JavaScript. We’ll use PHP’s OpenSSL extension for robust encryption. A WordPress REST API endpoint is the recommended approach for handling these requests.
REST API Endpoint Example (`secure-file-vault.php` or separate file)
<?php
// Add this to your secure-file-vault.php or a dedicated API file
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define encryption constants
define( 'SFV_ENCRYPTION_METHOD', 'aes-256-cbc' );
define( 'SFV_KEY_ITERATIONS', 10000 ); // Number of iterations for PBKDF2
define( 'SFV_SALT_LENGTH', 16 ); // Salt length in bytes
define( 'SFV_IV_LENGTH', openssl_cipher_iv_length( SFV_ENCRYPTION_METHOD ) );
/**
* Generates a salt for key derivation.
*
* @return string The generated salt.
*/
function sfv_generate_salt() {
return random_bytes( SFV_SALT_LENGTH );
}
/**
* Derives a strong encryption key from a password and salt using PBKDF2.
*
* @param string $password The user's password.
* @param string $salt The salt to use.
* @return string The derived encryption key.
*/
function sfv_derive_key( $password, $salt ) {
// Use PBKDF2 for key derivation
// Note: PHP's hash_pbkdf2 requires PHP 5.5+
return hash_pbkdf2( 'sha256', $password, $salt, SFV_KEY_ITERATIONS, 32 ); // 32 bytes for AES-256
}
/**
* Encrypts data using AES-256-CBC.
*
* @param string $plaintext The data to encrypt.
* @param string $password The password to use for key derivation.
* @return string|false Encrypted data in base64 encoding, or false on failure.
*/
function sfv_encrypt_data( $plaintext, $password ) {
if ( empty( $plaintext ) || empty( $password ) ) {
return false;
}
$salt = sfv_generate_salt();
$key = sfv_derive_key( $password, $salt );
$iv = openssl_random_pseudo_bytes( SFV_IV_LENGTH );
$encrypted = openssl_encrypt( $plaintext, SFV_ENCRYPTION_METHOD, $key, OPENSSL_RAW_DATA, $iv );
if ( $encrypted === false ) {
error_log( 'OpenSSL encryption failed: ' . openssl_error_string() );
return false;
}
// Prepend salt and IV to the encrypted data for later decryption
$data_to_store = base64_encode( $salt . $iv . $encrypted );
return $data_to_store;
}
/**
* Decrypts data encrypted with sfv_encrypt_data.
*
* @param string $base64_data The base64 encoded data (salt + iv + encrypted).
* @param string $password The password used for encryption.
* @return string|false The decrypted plaintext, or false on failure.
*/
function sfv_decrypt_data( $base64_data, $password ) {
if ( empty( $base64_data ) || empty( $password ) ) {
return false;
}
$decoded_data = base64_decode( $base64_data );
if ( $decoded_data === false ) {
return false;
}
$salt = substr( $decoded_data, 0, SFV_SALT_LENGTH );
$iv = substr( $decoded_data, SFV_SALT_LENGTH, SFV_IV_LENGTH );
$encrypted = substr( $decoded_data, SFV_SALT_LENGTH + SFV_IV_LENGTH );
if ( strlen( $salt ) !== SFV_SALT_LENGTH || strlen( $iv ) !== SFV_IV_LENGTH ) {
return false; // Corrupted data
}
$key = sfv_derive_key( $password, $salt );
$decrypted = openssl_decrypt( $encrypted, SFV_ENCRYPTION_METHOD, $key, OPENSSL_RAW_DATA, $iv );
if ( $decrypted === false ) {
error_log( 'OpenSSL decryption failed: ' . openssl_error_string() );
return false;
}
return $decrypted;
}
/**
* Registers the REST API routes for the block.
*/
function sfv_register_rest_routes() {
register_rest_route( 'secure-file-vault/v1', '/process', array(
'methods' => WP_REST_Server::CREATABLE, // Handles POST requests
'callback' => 'sfv_handle_process_request',
'permission_callback' => '__return_true', // In a real app, implement proper permissions
) );
}
add_action( 'rest_api_init', 'sfv_register_rest_routes' );
/**
* Handles the encryption/decryption requests via REST API.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response Response object.
*/
function sfv_handle_process_request( WP_REST_Request $request ) {
$action = $request->get_param( 'action' ); // 'encrypt' or 'decrypt'
$content = $request->get_param( 'content' );
$encrypted_content = $request->get_param( 'encrypted_content' );
$password = $request->get_param( 'password' );
if ( ! in_array( $action, [ 'encrypt', 'decrypt' ] ) ) {
return new WP_Error( 'invalid_action', 'Invalid action specified.', array( 'status' => 400 ) );
}
if ( empty( $password ) ) {
return new WP