• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom secure file encryption vault block for Gutenberg using Vanilla JS Web Components

Step-by-Step Guide to building a custom secure file encryption vault block for Gutenberg using Vanilla JS Web Components

Project Setup: The Foundation for Our Encryption Vault

We’ll begin by establishing the necessary directory structure for our custom Gutenberg block. This involves creating a plugin folder within your WordPress installation’s wp-content/plugins/ directory. Inside this plugin folder, we’ll set up a build directory for our compiled JavaScript and CSS, and a src directory for our uncompiled source files. This separation is crucial for maintainability and for leveraging modern JavaScript build tools.

For this project, we’ll use @wordpress/scripts, a package that provides convenient scripts for building WordPress plugins and blocks, including Babel compilation, Webpack bundling, and CSS preprocessing. Initialize your project by navigating to your plugin directory in the terminal and running:

npm init -y
npm install @wordpress/scripts --save-dev

Next, we need to configure our package.json file to include build scripts. Add the following lines to your package.json:

{
  "name": "secure-file-encryption-vault",
  "version": "1.0.0",
  "description": "A custom Gutenberg block for secure file encryption.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": ["wordpress", "gutenberg", "block", "encryption", "web-components"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.10.0"
  }
}

The build script will compile our source files into the build directory, and the start script will watch for changes and recompile automatically, which is invaluable during development.

Plugin Header and Block Registration

Every WordPress plugin requires a header comment block to identify it. Create a main PHP file for your plugin (e.g., secure-file-encryption-vault.php) in the root of your plugin directory and add the following header:

<?php
/**
 * Plugin Name: Secure File Encryption Vault
 * Plugin URI: https://example.com/plugins/secure-file-encryption-vault/
 * Description: A custom Gutenberg block for secure file encryption using Web Components.
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://yourwebsite.com/
 * License: GPL-2.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: secure-file-encryption-vault
 * Domain Path: /languages
 */

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_encryption_vault_block_init() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'secure_file_encryption_vault_block_init' );
?>

This PHP file will also serve as the entry point for registering our Gutenberg block. We’ll use a block.json file to define our block’s metadata and dependencies. Create a block.json file in the root of your plugin directory with the following content:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "secure-vault/encryption-block",
  "version": "1.0.0",
  "title": "Secure File Encryption Vault",
  "category": "widgets",
  "icon": "lock",
  "description": "Encrypt and decrypt files securely within the WordPress editor.",
  "keywords": ["encryption", "security", "file", "vault", "web-components"],
  "attributes": {
    "fileContent": {
      "type": "string",
      "default": ""
    },
    "encryptionKey": {
      "type": "string",
      "default": ""
    }
  },
  "supports": {
    "html": false
  },
  "textdomain": "secure-file-encryption-vault",
  "editorScript": "file:./build/index.js",
  "editorStyle": "file:./build/index.css",
  "style": "file:./build/style-index.css"
}

The block.json file is essential. It tells WordPress about our block’s name, title, category, icon, and crucially, where to find its editor script and styles. We’ve also defined two attributes: fileContent to store the content of the file being encrypted/decrypted, and encryptionKey for the user-provided key. The editorScript points to our compiled JavaScript file, which will be generated in the build directory.

Frontend and Editor Logic with Vanilla JS Web Components

Now, let’s dive into the core of our block: the Web Component that will handle the encryption and decryption logic. We’ll create a JavaScript file, say src/encryption-component.js, to house our custom element.

// src/encryption-component.js

class EncryptionComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });

        this.fileContent = '';
        this.encryptionKey = '';
        this.encryptedContent = '';
        this.decryptedContent = '';

        this.render();
    }

    connectedCallback() {
        this.shadowRoot.getElementById('file-input').addEventListener('change', this.handleFileChange.bind(this));
        this.shadowRoot.getElementById('encryption-key').addEventListener('input', this.handleKeyChange.bind(this));
        this.shadowRoot.getElementById('encrypt-button').addEventListener('click', this.handleEncrypt.bind(this));
        this.shadowRoot.getElementById('decrypt-button').addEventListener('click', this.handleDecrypt.bind(this));
    }

    disconnectedCallback() {
        // Clean up event listeners if necessary
    }

    handleFileChange(event) {
        const file = event.target.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = (e) => {
            this.fileContent = e.target.result;
            this.updateDisplay();
        };
        reader.readAsText(file);
    }

    handleKeyChange(event) {
        this.encryptionKey = event.target.value;
        this.updateDisplay();
    }

    async handleEncrypt() {
        if (!this.fileContent || !this.encryptionKey) {
            alert('Please select a file and enter an encryption key.');
            return;
        }

        try {
            const encodedKey = new TextEncoder().encode(this.encryptionKey);
            const key = await crypto.subtle.importKey(
                'raw',
                encodedKey,
                { name: 'AES-GCM' },
                false,
                ['encrypt', 'decrypt']
            );

            const encodedData = new TextEncoder().encode(this.fileContent);
            const iv = crypto.getRandomValues(new Uint8Array(12)); // Initialization Vector

            const encrypted = await crypto.subtle.encrypt(
                {
                    name: 'AES-GCM',
                    iv: iv,
                },
                key,
                encodedData
            );

            const encryptedArray = new Uint8Array(encrypted);
            const ivArray = new Uint8Array(iv);

            // Combine IV and ciphertext for storage/transmission
            const combined = new Uint8Array(ivArray.length + encryptedArray.length);
            combined.set(ivArray);
            combined.set(encryptedArray, ivArray.length);

            this.encryptedContent = btoa(String.fromCharCode.apply(null, combined));
            this.updateDisplay();
        } catch (error) {
            console.error('Encryption failed:', error);
            alert('Encryption failed. Please check your key and file.');
        }
    }

    async handleDecrypt() {
        if (!this.encryptedContent || !this.encryptionKey) {
            alert('Please provide encrypted content and an encryption key.');
            return;
        }

        try {
            const decodedData = Uint8Array.from(atob(this.encryptedContent), c => c.charCodeAt(0));

            const ivLength = 12;
            const iv = decodedData.slice(0, ivLength);
            const encryptedData = decodedData.slice(ivLength);

            const encodedKey = new TextEncoder().encode(this.encryptionKey);
            const key = await crypto.subtle.importKey(
                'raw',
                encodedKey,
                { name: 'AES-GCM' },
                false,
                ['encrypt', 'decrypt']
            );

            const decrypted = await crypto.subtle.decrypt(
                {
                    name: 'AES-GCM',
                    iv: iv,
                },
                key,
                encryptedData
            );

            this.decryptedContent = new TextDecoder().decode(decrypted);
            this.updateDisplay();
        } catch (error) {
            console.error('Decryption failed:', error);
            alert('Decryption failed. Please check your key.');
        }
    }

    updateDisplay() {
        this.shadowRoot.getElementById('encrypted-output').textContent = this.encryptedContent;
        this.shadowRoot.getElementById('decrypted-output').textContent = this.decryptedContent;
    }

    render() {
        this.shadowRoot.innerHTML = `
            
            

Input

Output

`; } } customElements.define('encryption-component', EncryptionComponent);

This Web Component uses the Web Crypto API for robust encryption. It defines an <encryption-component> custom element. The constructor initializes state and renders the component’s HTML structure and basic styling within its shadow DOM. The connectedCallback attaches event listeners to the file input, key input, and buttons. The handleFileChange reads the selected file’s content. handleKeyChange updates the encryption key. The handleEncrypt and handleDecrypt methods implement AES-GCM encryption/decryption using the Web Crypto API, combining the Initialization Vector (IV) with the ciphertext for storage and extracting it during decryption. The encrypted content is then Base64 encoded for easier handling.

Integrating the Web Component into the Gutenberg Block

Now, we need to connect our Web Component to the Gutenberg editor. Create a JavaScript file for your block’s editor logic, typically named src/index.js. This file will register the block and render our custom component within the block’s edit and save functions.

// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './encryption-component'; // Import the Web Component definition

// Import styles
import './editor.scss';
import './style.scss';

registerBlockType('secure-vault/encryption-block', {
    apiVersion: 3,
    title: 'Secure File Encryption Vault',
    icon: 'lock',
    category: 'widgets',
    description: 'Encrypt and decrypt files securely within the WordPress editor.',
    keywords: ['encryption', 'security', 'file', 'vault', 'web-components'],
    attributes: {
        fileContent: {
            type: 'string',
            default: '',
        },
        encryptionKey: {
            type: 'string',
            default: '',
        },
        encryptedData: { // Attribute to store the encrypted data for saving
            type: 'string',
            default: '',
        }
    },

    edit: ({ attributes, setAttributes }) => {
        const { fileContent, encryptionKey, encryptedData } = attributes;

        // Function to update attributes from the Web Component
        const updateAttributes = (newFileContent, newEncryptionKey, newEncryptedData) => {
            setAttributes({ fileContent: newFileContent });
            setAttributes({ encryptionKey: newEncryptionKey });
            setAttributes({ encryptedData: newEncryptedData });
        };

        // We'll use a placeholder for the edit view and rely on the Web Component's
        // internal state management. The Web Component will handle its own rendering.
        // We need to pass down the initial attributes and a way to update them.
        // For simplicity in this example, we'll render the component directly.
        // In a more complex scenario, you might use a wrapper component.

        // A more robust approach would involve passing callbacks to the web component
        // to sync its internal state with Gutenberg attributes.
        // For this example, we'll assume the web component can be directly embedded
        // and its output (encryptedData) will be saved.

        // To make this work, the web component needs to expose a way to set its initial state
        // and emit events when its internal state (like encryptedData) changes.
        // For simplicity here, we'll just render the component and rely on its output.

        // Let's refine this: we need to ensure the web component's state is reflected
        // in the block's attributes for saving.

        // A common pattern is to have the web component emit custom events.
        // We'll need to modify the web component slightly to support this.
        // For now, let's assume the web component's `encryptedContent` property
        // will be accessible and we can use it to update the `encryptedData` attribute.

        // This is a simplified representation. In a real-world scenario, you'd
        // likely use a wrapper component or a more sophisticated communication
        // mechanism between the Web Component and React/WordPress block internals.

        // For demonstration, we'll render the component and assume it manages its state.
        // The `save` function will be responsible for outputting the `encryptedData`.
        // The `edit` function primarily needs to ensure the component is rendered.

        // Let's add a mechanism for the web component to communicate back.
        // We'll add a custom event listener in `connectedCallback` of the web component.
        // For now, we'll just render it.

        // The most straightforward way to integrate is to have the web component
        // render its UI and manage its internal state. The block's `save` function
        // will then capture the *output* of the web component (the encrypted data).
        // In the `edit` function, we just need to ensure the component is present.

        // To make this truly interactive and save state, the web component needs to
        // communicate its `encryptedContent` back to the block's attributes.
        // Let's assume the web component will emit a 'data-updated' event.

        // We'll need to add this event listener in the `edit` function.
        // However, directly manipulating the DOM like this within `edit` can be tricky.
        // A better approach is to use a wrapper component or a ref.

        // For this example, let's simplify: the `edit` function will render the component.
        // The `save` function will output the `encryptedData` attribute.
        // The user will interact with the component in the editor.
        // The `encryptedData` attribute will be updated by the web component
        // via a custom event (which we'll need to add to the web component).

        // Let's add a placeholder for the web component and assume it will be
        // rendered and managed.

        // A more direct integration:
        // We can render the web component and listen for its custom events.
        // Let's assume the web component emits a 'data-changed' event with detail:
        // { encryptedContent: '...', decryptedContent: '...' }

        // We need to ensure the web component is rendered and its events are captured.
        // This requires a bit more advanced React/JS interaction.

        // For a simpler approach, let's render the web component and rely on
        // its internal state. The `save` function will be key.

        // Let's refine the `edit` function to render the component and attempt
        // to capture its state. This is a common challenge with integrating
        // Web Components into React-based frameworks like Gutenberg.

        // A common pattern is to use a wrapper component that bridges the gap.
        // For this example, we'll render the component directly and rely on
        // the `save` function to capture the `encryptedData` attribute.

        // Let's add a mechanism to update attributes from the web component.
        // We'll need to add a custom event to the web component.

        // For now, let's render the component and focus on the `save` function.
        // The `edit` function's primary role here is to display the component.

        // Let's add a placeholder element and then attach the web component to it.
        // This is a more controlled way to integrate.

        // We'll render a div and then use `useEffect` to mount the web component.
        // This requires using React hooks within the `edit` function.

        const wrapperRef = React.useRef(null);

        React.useEffect(() => {
            const component = document.createElement('encryption-component');

            // Set initial state from attributes
            component.fileContent = fileContent; // Assuming the WC can accept this
            component.encryptionKey = encryptionKey; // Assuming the WC can accept this
            component.encryptedContent = encryptedData; // Load saved encrypted data

            // Listen for custom events from the web component
            const handleDataUpdate = (event) => {
                const { encryptedContent, decryptedContent } = event.detail;
                setAttributes({ encryptedData: encryptedContent });
                // We might also want to update fileContent and encryptionKey if the WC manages them
                // For simplicity, we'll focus on saving encryptedData.
            };

            component.addEventListener('data-updated', handleDataUpdate);

            // Append the web component to the wrapper
            if (wrapperRef.current) {
                wrapperRef.current.innerHTML = ''; // Clear previous content
                wrapperRef.current.appendChild(component);
            }

            // Cleanup function
            return () => {
                component.removeEventListener('data-updated', handleDataUpdate);
                if (wrapperRef.current && wrapperRef.current.contains(component)) {
                    wrapperRef.current.removeChild(component);
                }
            };
        }, [fileContent, encryptionKey, encryptedData, setAttributes]); // Re-run effect if attributes change

        // Render a container for the web component
        return React.createElement(
            'div',
            { ref: wrapperRef, className: 'secure-vault-editor-wrapper' }
            // The web component will be appended here by useEffect
        );
    },

    save: ({ attributes }) => {
        const { encryptedData } = attributes;

        // The save function should output static HTML.
        // We will store the encrypted data as a data attribute or within a hidden element.
        // The Web Component itself won't be rendered on the frontend by default
        // unless we explicitly tell it to.
        // For security, we don't want to expose the decryption logic on the frontend
        // without user interaction.
        // So, we'll save the encrypted data and potentially a placeholder or a message.

        // Option 1: Save as a data attribute on a wrapper element.
        // This is good for passing data to a frontend script that might decrypt it.
        // return React.createElement('div', { 'data-encrypted-content': encryptedData });

        // Option 2: Save within a hidden element.
        // This is also viable.

        // For this example, let's save the encrypted data in a way that it can be
        // retrieved by a separate frontend script or shortcode if needed.
        // We'll render a placeholder and store the encrypted data.

        // IMPORTANT: The Web Component's encryption logic should ideally NOT run
        // on the frontend by default for security reasons. The `save` function
        // should output the *result* of the encryption (the encrypted data).
        // If you need decryption on the frontend, you'd need a separate mechanism
        // and potentially a different block or shortcode.

        // For this block, we'll assume the primary use case is to store encrypted data.
        // The decryption would happen server-side or via a separate, secure frontend flow.

        // Let's output a simple message and store the encrypted data in a data attribute.
        // The user interacting with the block in the editor will see the component.
        // The frontend will see this saved output.

        if (!encryptedData) {
            return React.createElement('p', null, 'Secure Vault: No data encrypted yet.');
        }

        // We'll render a placeholder and attach the encrypted data.
        // The actual decryption logic should be handled elsewhere for security.
        // This block's `save` function is primarily for persisting the encrypted state.
        return React.createElement(
            'div',
            { className: 'secure-vault-saved-output', 'data-encrypted-content': encryptedData },
            React.createElement('p', null, 'Encrypted content stored securely. Decryption requires appropriate credentials and context.')
        );
    },
});

// --- Modifications to encryption-component.js for event emission ---
// Add this to the end of the EncryptionComponent class in src/encryption-component.js

// Inside the EncryptionComponent class:
// Add this method:
/*
    emitUpdate() {
        this.dispatchEvent(new CustomEvent('data-updated', {
            detail: {
                encryptedContent: this.encryptedContent,
                decryptedContent: this.decryptedContent
            }
        }));
    }

    // Call this.emitUpdate() after this.updateDisplay() in:
    // handleEncrypt()
    // handleDecrypt()
*/

// Example modification in handleEncrypt():
/*
    async handleEncrypt() {
        // ... existing encryption logic ...
        this.encryptedContent = btoa(String.fromCharCode.apply(null, combined));
        this.updateDisplay();
        this.emitUpdate(); // Emit the event
    }
*/

// Example modification in handleDecrypt():
/*
    async handleDecrypt() {
        // ... existing decryption logic ...
        this.decryptedContent = new TextDecoder().decode(decrypted);
        this.updateDisplay();
        this.emitUpdate(); // Emit the event
    }
*/

In src/index.js, we import the necessary functions from @wordpress/blocks and our custom Web Component. The registerBlockType function is used to define our block. The edit function is where the magic happens for the editor view. We use React hooks (useRef and useEffect) to create an instance of our <encryption-component>, set its initial state from block attributes, and crucially, listen for a custom data-updated event emitted by the Web Component. When this event fires, we update the block’s attributes using setAttributes, ensuring that the encrypted data is saved.

The save function is responsible for rendering the static HTML that will be saved to the database. For security, we don’t want to embed the decryption logic directly in the frontend output. Instead, we save the encryptedData attribute, typically as a data-encrypted-content attribute on a wrapper element, along with a message indicating that the content is encrypted. This allows for server-side decryption or a separate, secure frontend decryption process.

Important Note on Web Component Integration: Integrating Web Components directly into React-based environments like Gutenberg can be complex. The provided edit function uses a common pattern involving useEffect and a wrapper ref to mount the Web Component and handle communication via custom events. Ensure your encryption-component.js emits the data-updated event as described in the comments within src/index.js.

Styling the Block

We need to add some basic styling for both the editor and the frontend. Create two SCSS files in your src directory:

/* src/editor.scss */
.secure-vault-editor-wrapper {
    border: 1px dashed #ccc;
    padding: 10px;
}
/* src/style.scss */
.secure-vault-saved-output {
    background-color: #f0f0f0;
    padding: 15px;
    border-radius: 5px;
    border: 1px solid #e0e0e0;
    p {
        margin: 0;
        font-style: italic;
        color: #555;
    }
}

These SCSS files will be automatically compiled into CSS files in the build directory by @wordpress/scripts, as specified in our block.json.

Building and Activating the Plugin

With all the files in place, navigate to your plugin’s root directory in the terminal and run the build command:

npm run build

This command will compile your JavaScript and SCSS files into the build directory. After the build process completes, go to your WordPress admin area, navigate to “Plugins,” and activate the “Secure File Encryption Vault” plugin.

You can now add the “Secure File Encryption Vault” block to any post or page. Interact with the component in the editor to encrypt your file content. The encrypted data will be saved, and you’ll see the placeholder output on the frontend.

Security Considerations and Further Enhancements

This implementation provides a foundational secure file encryption vault. However, several security considerations and enhancements are crucial for production environments:

  • Key Management: Storing encryption keys directly in the browser or passing them insecurely is a major vulnerability. For production, consider server-side key management, secure storage (e.g., WordPress options API with encryption, or external secret management services), and potentially using asymmetric encryption (public/private keys) for more robust security.
  • Frontend Decryption: The current save function prevents frontend decryption by default. If frontend decryption is a requirement, ensure it’s implemented with extreme care, possibly requiring user authentication and a secure way to deliver the decryption key (e.g., via AJAX calls to a secure API endpoint).
  • File Upload Security: If you extend this to handle actual file uploads, implement strict file type validation, size limits, and sanitize filenames to prevent malicious uploads.
  • Error Handling: Enhance error handling for encryption/decryption failures, providing more user-friendly feedback without revealing sensitive information.
  • Attribute Synchronization: For more complex interactions, refine the communication between the Web Component and Gutenberg attributes. Libraries like @wordpress/element and React’s lifecycle methods can offer more robust solutions.
  • Server-Side Processing: For maximum security, consider performing encryption and decryption server-side. This block could then act as an interface to trigger these server-side operations via AJAX.

By following these steps, you’ve built a custom Gutenberg block that leverages Vanilla JS Web Components and the Web Crypto API to provide a secure file encryption capability within WordPress. Remember to prioritize security best practices, especially concerning key management, for any production deployment.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Implementing automated compliance reporting for custom customer support tickets ledgers using native PHP ZipArchive streams
  • Implementing automated compliance reporting for custom online course lessons ledgers using mpdf engine
  • WordPress Development Recipe: Efficient binary storage and retrieval in custom tables using Anonymous Classes
  • Step-by-Step Guide to building a custom interactive mapping module block for Gutenberg using Vanilla JS Web Components
  • WordPress Development Recipe: Secure token-based API authentication for Stripe Payment webhook in custom plugins

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (650)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (858)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (629)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (299)
  • WordPress Theme Development (357)

Recent Posts

  • Implementing automated compliance reporting for custom customer support tickets ledgers using native PHP ZipArchive streams
  • Implementing automated compliance reporting for custom online course lessons ledgers using mpdf engine
  • WordPress Development Recipe: Efficient binary storage and retrieval in custom tables using Anonymous Classes

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (858)
  • Debugging & Troubleshooting (650)
  • Security & Compliance (629)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala