File upload with encryption using public key client side (App Connect extension)

I’ve been quite active today looking into encrypting uploaded files using public key encryption. You’ll see my other posts on this.

I’ve now used ChatGPT to create an extension which does the encrypting client-side so it’s even more secure. The unencrypted file never leaves the browser.

Could anyone who has more knowledge of Wappler extensions please look over this and give some feedback?

hybrid-encryption-package.zip (6.8 KB)

I would love this to be added to Wappler core but this might be the solution for now? It’s currently just PHP but I’m sure a Node version could easily be added.

Can you pack and publish this extension to npm so it can be easily installed by other Wappler users. See:

Hi George

This was created by ChatGPT and so I don’t know how good the quality is or if there are any issues with it. Are you able to give it a glance over and confirm it’s all good? I will then happily add it to npm so others can easily use it.

Cheers, Jon.

Not exactly sure how you anticipate this working.
How is it applied to the input?
On the client extension, the.json file makes no sense, however there should be an .HJSON file to define the inputs for the extension.
Similarly, the server connect extension has no HJSON definition

Perhaps you could work me through the anticipated workflow so i can work out what is needed. How and to what is the extension code applied on the client side?
Do you have any screen shots of what you have on a working model?

As an aside, currently Gemini does a much better job with extensions than ChatGPT.
Both code well but ChatGPT seems devoid of the ability to produce the HJSON part where Gemini seems to have a decent understanding.

1 Like

:face_with_peeking_eye:

Spat my bloody tea out Brian!

:rofl:

4 Likes

Fixed so i guess thats a happy ending. :rofl::rofl:

1 Like

Thanks Brian.

I was looking for a solution to encrypt files that are uploaded via staff using a public key so they can only be opened by those with the private key. I’ve never made a Wappler extension before so I thought I’d see what ChatGPT could do and this was the result.

Thanks for looking through it. I will use Gemini next time.

There aren’t any screenshots, I’ve not had it working, I literally just uploaded what AI produced here so those that know more than me can see what they think.

Cheers, Jon.

Question is, how to you see this encyption applied to the file within the .app structure.
Either the encryption needs to be applied as the file as selected (and stored in memory?) or the post action intercepted somehow to encrypt the content of th post collection.
The encryption side is pretty straight forward, applying the encryption to the file is another issue.
Have you any ideas in your head about that?

This may help from Gemini.

To implement client-side encryption in Wappler, you need to extend App Connect with a custom formatter. Because RSA cannot encrypt large files directly (it is limited by key size), the standard approach is Hybrid Encryption:

  • AES encrypts the file (fast, handles large data).
  • RSA (Public Key) encrypts the small AES key.
  • Both are bundled and sent to your PHP server.
  1. The App Connect Extension
    Create a file named dmx-encryption.js in your js folder and include it in your main layout page.
    // dmx-encryption.js
    dmx.Formatter('file', 'encryptForServer', async function(file, publicKeyPem) {
    if (!file) return null;

    try {
    // 1. Import the RSA Public Key from PEM string
    const importedPublicKey = await importRsaPublicKey(publicKeyPem);

     // 2. Generate a temporary AES-GCM key (256-bit)
     const aesKey = await window.crypto.subtle.generateKey(
         { name: "AES-GCM", length: 256 },
         true,
         ["encrypt"]
     );
    
     // 3. Encrypt the File
     const fileBuffer = await file.arrayBuffer();
     const iv = window.crypto.getRandomValues(new Uint8Array(12)); // Initialization Vector
     const encryptedContent = await window.crypto.subtle.encrypt(
         { name: "AES-GCM", iv: iv },
         aesKey,
         fileBuffer
     );
    
     // 4. Encrypt the AES key using the RSA Public Key
     const exportedAesKey = await window.crypto.subtle.exportKey("raw", aesKey);
     const encryptedAesKey = await window.crypto.subtle.encrypt(
         { name: "RSA-OAEP" },
         importedPublicKey,
         exportedAesKey
     );
    
     // 5. Package: [IV (12b)] + [Key Length (4b)] + [Encrypted Key] + [Encrypted Data]
     const payload = new Blob([
         iv, 
         new Uint32Array([encryptedAesKey.byteLength]), 
         encryptedAesKey, 
         encryptedContent
     ], { type: 'application/octet-stream' });
    
     // Return as a new file object for the Wappler upload
     return new File([payload], file.name + '.enc');
    

    } catch (e) {
    console.error("Encryption failed:", e);
    return file; // Fallback to original if error occurs
    }
    });

// Helper: Convert PEM to ArrayBuffer for WebCrypto
async function importRsaPublicKey(pem) {
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC KEY-----";
const pemContents = pem.split(pemHeader)[1].split(pemFooter)[0].replace(/\s+/g, '');
const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0));

return window.crypto.subtle.importKey(
    "spki",
    binaryDer,
    { name: "RSA-OAEP", hash: "SHA-256" },
    true,
    ["encrypt"]
);

}

  1. Implementation in Wappler UI
    Since App Connect usually maps file inputs directly to the $_FILES array, you need a way to swap the original file for the encrypted one.
  • Add a Variable: In your page, add a dmx-value variable named var_encrypted_file.
  • File Input Event: Select your File Input. Under Events, add an onchange event.
  • The Action: * Set the value of var_encrypted_file to:
    inp_file.file.encryptForServer("YOUR_PEM_PUBLIC_KEY_HERE")
  • The Form: Use a Server Connect Form. Instead of relying on the file input name, manually add a Post variable to the Server Connect component that sends var_encrypted_file.
  1. Server-Side Decryption (PHP)
    On your server, you will receive a binary blob. You must use your Private Key to reverse the process.
<?php // PHP Decryption Logic $data = file_get_contents($_FILES['encrypted_file']['tmp_name']); $private_key = openssl_get_privatekey(file_get_contents("path/to/private_key.pem")); // 1. Extract IV (First 12 bytes) $iv = substr($data, 0, 12); // 2. Extract Encrypted AES Key Length (Next 4 bytes) $keyLen = unpack("V", substr($data, 12, 4))[1]; // 3. Extract and Decrypt AES Key $encAesKey = substr($data, 16, $keyLen); openssl_private_decrypt($encAesKey, $aesKey, $private_key, OPENSSL_PKCS1_OAEP_PADDING); // 4. Decrypt File Content $ciphertext = substr($data, 16 + $keyLen); $decrypted = openssl_decrypt($ciphertext, 'aes-256-gcm', $aesKey, OPENSSL_RAW_DATA, $iv, $tag); // Note: In GCM mode, the tag is usually appended to the end of the ciphertext // by WebCrypto. You may need to slice the last 16 bytes as the $tag. ?>

Summary Table

Component Responsibility
Formatter Intercepts file, runs WebCrypto API, returns encrypted Blob.
RSA Public Key Stays on the Client. Only used to "lock" the AES key.
RSA Private Key Stays on the Server. Only used to "unlock" the AES key.
AES-GCM Performs the heavy lifting of encrypting the actual file data.
Would you like me to help you refine the PHP decryption script to handle the GCM authentication tag correctly?

Another view from Claude:

Hybrid Encryption Package Overview

This is a Wappler custom component package that implements hybrid encryption for file security using a combination of AES and RSA encryption algorithms. Here's what it does:

Purpose

Securely encrypt files on the client-side and decrypt them on the server-side using industry-standard hybrid encryption.

How It Works

Client-Side (JavaScript):

  1. Fetches the RSA public key from the server
  2. Generates a random 256-bit AES key and 96-bit initialization vector (IV)
  3. Encrypts the file using AES-256-GCM (Galois/Counter Mode) with the generated AES key
  4. Encrypts the AES key using RSA-OAEP (RSA with the public key)
  5. Returns an object containing:
  • The encrypted file (base64 encoded)
  • The encrypted AES key (base64 encoded)
  • The IV (base64 encoded)
  • The authentication tag (base64 encoded)

Server-Side (PHP):

  • public_key.php: Serves the RSA public key to clients
  • hybrid_decrypt.php: Decrypts files by:
    1. Receiving the encrypted data package (encrypted key, IV, tag, and encrypted file)
    2. Using the RSA private key to decrypt the AES key
    3. Using the decrypted AES key to decrypt the file content
    4. Saving the decrypted file on the server

Security Features

  • Hybrid encryption: Combines the speed of symmetric encryption (AES) with the security of asymmetric encryption (RSA)
  • AES-256-GCM: Authenticated encryption providing both confidentiality and integrity
  • RSA-OAEP: Optimal Asymmetric Encryption Padding for secure key exchange
  • Random key generation: Each file gets a unique AES key

Package Structure

  • Client component: encryptFile - A Wappler App Connect action under the "Security" category
  • Server components: PHP scripts for key distribution and decryption
  • Key files: RSA public/private key pair (PEM format)

This package enables secure file uploads where sensitive files are encrypted before transmission and can only be decrypted by the server holding the private key.

I can ask Claude to create a publishable Wappler Module, or you could do that yourself.

I went ahead and asked Claude to create the package, with this result:

hybrid-encryption-package.zip (13.5 KB)

Open the PUBLISH.md file to guide you to publish the extension.

I'll soon be needing a similar package for NodeJS project

Hope this helps for those wishing to create NodeJS Extensions. Here is our extension.md file which aids LLMs in creating extensions.


How to Create a Wappler Server Connect Extension

A comprehensive guide for creating custom Server Connect extensions for Wappler, documenting best practices and correct setup procedures.

Table of Contents

  • Overview
  • Prerequisites
  • File Structure
  • Step-by-Step Guide
  • Critical Setup Requirements
  • Testing Your Extension
  • Common Pitfalls
  • Publishing Your Extension

Overview

Wappler Server Connect extensions allow you to create custom server actions that integrate seamlessly into the Wappler IDE. Extensions consist of:

  1. UI Definition File (.hjson) - Defines how your extension appears in Wappler
  2. Implementation File (.js) - Contains the actual server-side logic
  3. Dependencies (optional) - Any npm packages your extension needs

Prerequisites

  • Wappler 6.0.0 or higher
  • Node.js 14.0.0 or higher
  • Understanding of JavaScript and Node.js
  • Familiarity with Wappler Server Connect

File Structure

Distribution Structure (for GitHub/NPM)

When you distribute your extension, use a flat structure:

your-extension/
├── README.md
├── package.json
├── yourmodule.hjson        ← UI definition
├── yourmodule.js           ← Implementation code
└── .gitignore              ← Optional, only if using Git

Note: .gitignore is only needed if you're using Git for version control. .npmignore is not needed for manual distribution.

Installation Structure (in Wappler Projects)

When users install your extension, files go to specific locations:

[Your-Wappler-Project]/
├── extensions/
│   └── server_connect/
│       └── modules/
│           └── yourmodule.hjson        ← Copy UI definition here
└── lib/
    └── modules/
        └── yourmodule.js               ← Copy implementation here

CRITICAL:

  • File names MUST match (case-sensitive)
  • For distribution: keep files at root level
  • For installation: .hjson goes in extensions/server_connect/modules/
  • For installation: .js goes in lib/modules/
  • Both files should have the same base name

Step-by-Step Guide

Step 1: Create the Implementation File (.js)

Create lib/modules/yourmodule.js:

// Example: lib/modules/myextension.js

// Import any dependencies
const someLib = require('some-library');

// Export your action functions
exports.yourAction = function(options) {
    // Access parsed options using this.parseOptional()
    const param1 = this.parseOptional(options.param1, 'string', '');
    const param2 = this.parseOptional(options.param2, 'object', {});
    
    try {
        // Your logic here
        const result = doSomething(param1, param2);
        
        // Return your results
        return {
            success: true,
            data: result
        };
    } catch (error) {
        return {
            success: false,
            error: error.message
        };
    }
};

// You can export multiple actions
exports.anotherAction = function(options) {
    // Another action implementation
};

Key Points:

  • Use exports.functionName for each action
  • Function name MUST be lowercase (e.g., render, not Render)
  • Use this.parseOptional() to parse options
  • Return an object with your results
  • Handle errors gracefully

Important: Output Checkbox

  • For the actionOutput field, use initValue: true, defaultValue: false
  • initValue = initial checkbox state when action is added
  • defaultValue = value that causes the property to be omitted from JSON
  • This ensures the Output checkbox works correctly

Step 2: Create the UI Definition File (.hjson)

Create extensions/server_connect/modules/yourmodule.hjson:

{
  type: 'yourmodule_action'           # Unique identifier
  module: 'yourmodule'                # MUST match .js filename (lowercase)
  action: 'yourAction'                # MUST match exported function name
  groupTitle: 'Mr Cheese'             # Group name in Wappler UI
  groupIcon: 'fas fa-lg fa-cheese comp-exec'
  title: 'Your Action'                # Action display name
  icon: 'fas fa-lg fa-cog comp-exec'
  
  # Define output data structure
  dataScheme: [
    {name: 'success', type: 'boolean'}
    {name: 'data', type: 'text'}
    {name: 'error', type: 'text'}
  ]
  
  # Make the action pickable in data bindings
  dataPickObject: true
  
  # Define input properties
  properties: [
    {
      group: 'Settings'
      variables: [
        {
          name: 'actionName'
          optionName: 'name'
          title: 'Name'
          type: 'text'
          required: true
          defaultValue: ''
          baseName: 'myaction'
          help: 'The name for this action step'
        }
        {
          name: 'actionOutput'
          optionName: 'output'
          title: 'Output'
          type: 'boolean'
          initValue: true
          defaultValue: false
          help: 'Output the results'
        }
        {
          name: 'param1'
          optionName: 'param1'
          title: 'Parameter 1'
          type: 'text'
          serverDataBindings: true
          defaultValue: ''
          help: 'Description of parameter 1'
        }
        {
          name: 'param2'
          optionName: 'param2'
          title: 'Parameter 2'
          type: 'file'              # Use 'file' for file picker
          serverDataBindings: true
          defaultValue: ''
          help: 'Select a file'
        }
      ]
    }
  ]
  
  # Auto-install npm dependencies
  usedModules: {
    node: {
      "some-library": "^1.0.0"
    }
  }
}

Key HJSON Properties:

Property Required Description
type Yes Unique identifier for the action
module Yes MUST match .js filename (lowercase)
action Yes MUST match exported function name
groupTitle Yes Always use 'Mr Cheese' for all extensions
groupIcon Yes FontAwesome icon (use fa-cheese for consistency)
title Yes Display name for the action
icon Yes FontAwesome icon for the action
dataScheme Optional Output data structure
dataPickObject Optional Make action pickable in bindings
properties Yes Array of input property groups
usedModules Optional Auto-install dependencies

Step 3: Input Field Types

Common input field types for the type property:

Type Description Example
text Single line text input type: 'text'
textarea Multi-line text input type: 'textarea'
boolean Checkbox type: 'boolean'
number Number input (static only) type: 'number'
numberorstring Dynamic number/string type: 'numberorstring'
file File picker dialog type: 'file'
folder Folder picker dialog type: 'folder'
droplist Dropdown select See advanced example

Advanced: Dropdown List

{
  name: 'format'
  optionName: 'format'
  title: 'Output Format'
  type: 'droplist'
  values: [
    {title: 'JSON', value: 'json'}
    {title: 'XML', value: 'xml'}
    {title: 'CSV', value: 'csv'}
  ]
  defaultValue: 'json'
}

Step 4: Icon Color Modifiers

Use these color classes with FontAwesome icons:

Class Color Usage
comp-flows Blue Flow control
comp-errors Red Error handling
comp-data Teal Data operations
comp-loops Purple Loop actions
comp-general Gray General actions
comp-settings Orange Settings
comp-exec Green Execution
comp-files Yellow File operations

Example: fas fa-lg fa-database comp-data

Step 5: Install Dependencies

In your project root:

npm install your-dependencies

Or let Wappler auto-install by defining in usedModules:

usedModules: {
  node: {
    "ejs": "^3.1.9"
    "axios": "^1.6.0"
  }
}

Step 6: Restart Wappler

IMPORTANT: You MUST completely quit and restart Wappler after:

  • Adding new extensions
  • Modifying .hjson files
  • Installing dependencies

Simply reloading the project is NOT enough!

Critical Setup Requirements

:white_check_mark: DO:

  1. Match file names exactly - mymodule.js and mymodule.hjson
  2. Use lowercase for module and action - module: 'mymodule', action: 'render'
  3. Place files in correct directories:
    • .hjsonextensions/server_connect/modules/
    • .jslib/modules/
  4. Use this.parseOptional() in JavaScript functions
  5. Include actionName and actionOutput in all modules
  6. Fully restart Wappler after changes
  7. Use HJSON format (not JSON) for UI definition
  8. Export functions with exports.functionName

:cross_mark: DON'T:

  1. Don't use uppercase in module/action names (causes "Module doesn't exist" error)
  2. Don't put files in subfolders - they must be directly in modules/
  3. Don't use JSON format - must use HJSON (allows comments, no quotes needed)
  4. Don't use async unless truly needed (keep functions synchronous when possible)
  5. Don't forget to install dependencies with npm
  6. Don't just reload project - must fully restart Wappler
  7. Don't use old require('dmxAppConnect/lib/parser') - use this.parseOptional()

Testing Your Extension

1. Verify Installation

After restarting Wappler:

  1. Open a Server Connect API
  2. Click "+" to add a step
  3. Search for your extension name
  4. It should appear in the list

2. Test the Action

Create a test API:

1. Your Extension Action
   - Configure parameters
   - Set Output: true

2. Test in browser
   - Visit: /api/your-test.json
   - Check the JSON response

3. Debug Errors

Common errors and solutions:

"Module XXX doesn't exist"

  • Check: module name in .hjson matches .js filename (case-sensitive)
  • Check: .js file is in lib/modules/
  • Check: function is exported correctly

Extension doesn't appear in Wappler

  • Check: .hjson file is in extensions/server_connect/modules/
  • Check: HJSON syntax is valid (no JSON syntax errors)
  • Check: Wappler was fully restarted

"Cannot find module"

  • Check: Dependencies installed with npm install
  • Check: usedModules defined in .hjson

Common Pitfalls

1. Case Sensitivity Issues

# ❌ WRONG
module: 'SSR'
action: 'Render'

# ✅ CORRECT
module: 'ssr'
action: 'render'

2. File Location Errors

# ❌ WRONG
extensions/server_connect/modules/ssr/ssr.hjson
extensions/server_connect/modules/ssr/ssr.js

# ✅ CORRECT
extensions/server_connect/modules/ssr.hjson
lib/modules/ssr.js

3. JSON vs HJSON

// ❌ WRONG - JSON format
{
  "type": "myaction",
  "module": "mymodule"
}

// ✅ CORRECT - HJSON format
{
  type: 'myaction'
  module: 'mymodule'
}

4. Parser Usage

// ❌ WRONG - Old method
const parse = require('dmxAppConnect/lib/parser');
const value = parse.parseOptional(this, options.param, 'string', '');

// ✅ CORRECT - New method
const value = this.parseOptional(options.param, 'string', '');

Publishing Your Extension

Option 1: GitHub Repository

  1. Create a repository with flat structure:
your-extension/
├── README.md
├── package.json
├── yourmodule.hjson        ← UI definition at root
├── yourmodule.js           ← Implementation at root
├── .gitignore              ← Optional: node_modules/, *.log, .DS_Store
└── examples/               ← Optional: usage examples
  1. Add installation instructions to README:

    # Copy UI definition
    cp yourmodule.hjson [PROJECT]/extensions/server_connect/modules/
    
    # Copy implementation
    cp yourmodule.js [PROJECT]/lib/modules/
    
  2. Users clone repository and copy files to their Wappler project directories

Option 2: NPM Package

Note: For NPM packages that Wappler can auto-install, you may need nested directories. However, for manual distribution, use the flat structure from Option 1.

  1. Create package structure (if supporting auto-install):
your-wappler-extension/
├── package.json
├── server_connect/
│   └── modules/
│       └── yourmodule.hjson
└── lib/
    └── modules/
        └── yourmodule.js

OR use flat structure for manual installation:

your-wappler-extension/
├── package.json
├── yourmodule.hjson
└── yourmodule.js
  1. Update package.json:

For flat structure (recommended):

{
  "name": "wappler-your-extension",
  "version": "1.0.0",
  "description": "Your extension description",
  "main": "yourmodule.js",
  "keywords": ["wappler", "server-connect", "extension"],
  "author": "MrCheese",
  "license": "MIT",
  "wappler": {
    "module": "yourmodule",
    "version": "1.0.0"
  }
}

For nested structure:

{
  "name": "wappler-your-extension",
  "version": "1.0.0",
  "description": "Your extension description",
  "main": "lib/modules/yourmodule.js",
  "keywords": ["wappler", "server-connect", "extension"],
  "author": "MrCheese",
  "license": "MIT",
  "wappler": {
    "module": "yourmodule",
    "version": "1.0.0"
  }
}

Important Notes:

  • The "main" field should match your actual file structure
  • Always use "author": "MrCheese" for all extensions
  • Do NOT include dependencies or engines fields - extensions run within Wappler projects
  • Use usedModules in the .hjson file instead to auto-install dependencies in user projects
  1. Publish to NPM:
npm login
npm publish --access=public
  1. Users install via Wappler:
    • Project Settings → Extensions
    • Click "+" and enter: wappler-your-extension

Additional Resources

Tips for Success

  1. Start Simple - Create a basic "Hello World" action first, then add complexity
  2. Test Incrementally - Test after each change to isolate issues quickly
  3. Study Existing Extensions - Look at published extensions on GitHub for patterns
  4. Use Version Control - Track changes to both .hjson and .js files
  5. Document Your Extension - Write clear README with examples and installation steps
  6. Handle Errors Gracefully - Always return error information to help users debug

Remember: The key to success is matching names exactly (case-sensitive), placing files in the correct directories, and fully restarting Wappler after any changes!


If you use this as direction please be sure to find and replace all instances of 'MrCheese' .

1 Like

@Cheese: Thank you for this most treasured Christmas present. I will make eager use of the extension.md file.

And a Merry Christmas to you and yours. :christmas_tree:

1 Like

You are very welcome indeed @ben and likewise to you dear Sir, have a great Christmas and New Year! We have quite a few documents like these we use for prompting. After all this weeks chaos is over and done with I'll go through them and any I believe may be useful I'll add to the Community.

2 Likes