Skip to content

RND: WASM Twofish based on optimized C implementation#130

Closed
vitonsky wants to merge 2 commits into
masterfrom
twofish-wasm
Closed

RND: WASM Twofish based on optimized C implementation#130
vitonsky wants to merge 2 commits into
masterfrom
twofish-wasm

Conversation

@vitonsky

@vitonsky vitonsky commented Oct 9, 2025

Copy link
Copy Markdown
Member

No description provided.

@vitonsky

Copy link
Copy Markdown
Member Author

For research purpose I asked to implement wrappers for AI. Here are results.

Claude Sonnet 4

I'll help you create an Emscripten wrapper for this Twofish C implementation. Looking at the code, I can see it's a complete Twofish encryption library. Let me create the necessary files:

1. Main Wrapper File (twofish_wrapper.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten.h>

// Include the original Twofish headers
#include "platform.h"
#include "aes.h"

// We'll need to extract the relevant parts from TWOFISH2.C
// Since it's a large file, I'll include the essential structures and functions

// Twofish constants and structures (extracted from the original)
#define TWOFISH_BLOCK_SIZE 16
#define TWOFISH_KEY_SIZE 32

typedef struct {
    u4byte l_key[40];
    u4byte s_key[4];
    u4byte mk_tab[4 * 256];
    u4byte k_len;
} TWOFISH_CTX;

// Function prototypes (these should be extracted from TWOFISH2.C)
extern void twofish_set_key(TWOFISH_CTX *ctx, const u1byte key[], u4byte key_len);
extern void twofish_encrypt(const TWOFISH_CTX *ctx, const u1byte in_blk[], u1byte out_blk[]);
extern void twofish_decrypt(const TWOFISH_CTX *ctx, const u1byte in_blk[], u1byte out_blk[]);

// Global context for simplified JavaScript interface
static TWOFISH_CTX global_ctx;

// Wrapper functions for JavaScript
EMSCRIPTEN_KEEPALIVE
int twofish_init_key(const char* key_hex) {
    if (!key_hex) return -1;
    
    int key_len = strlen(key_hex) / 2;
    if (key_len > TWOFISH_KEY_SIZE) return -2;
    
    u1byte key[TWOFISH_KEY_SIZE];
    
    // Convert hex string to bytes
    for (int i = 0; i < key_len; i++) {
        char hex_byte[3] = {key_hex[i*2], key_hex[i*2+1], '\0'};
        key[i] = (u1byte)strtol(hex_byte, NULL, 16);
    }
    
    twofish_set_key(&global_ctx, key, key_len);
    return 0;
}

EMSCRIPTEN_KEEPALIVE
char* twofish_encrypt_block(const char* plaintext_hex) {
    if (!plaintext_hex) return NULL;
    
    if (strlen(plaintext_hex) != 32) return NULL; // Must be 16 bytes = 32 hex chars
    
    u1byte plaintext[TWOFISH_BLOCK_SIZE];
    u1byte ciphertext[TWOFISH_BLOCK_SIZE];
    
    // Convert hex string to bytes
    for (int i = 0; i < TWOFISH_BLOCK_SIZE; i++) {
        char hex_byte[3] = {plaintext_hex[i*2], plaintext_hex[i*2+1], '\0'};
        plaintext[i] = (u1byte)strtol(hex_byte, NULL, 16);
    }
    
    twofish_encrypt(&global_ctx, plaintext, ciphertext);
    
    // Convert result back to hex string
    char* result = (char*)malloc(33); // 32 hex chars + null terminator
    if (!result) return NULL;
    
    for (int i = 0; i < TWOFISH_BLOCK_SIZE; i++) {
        sprintf(result + i*2, "%02x", ciphertext[i]);
    }
    result[32] = '\0';
    
    return result;
}

EMSCRIPTEN_KEEPALIVE
char* twofish_decrypt_block(const char* ciphertext_hex) {
    if (!ciphertext_hex) return NULL;
    
    if (strlen(ciphertext_hex) != 32) return NULL; // Must be 16 bytes = 32 hex chars
    
    u1byte ciphertext[TWOFISH_BLOCK_SIZE];
    u1byte plaintext[TWOFISH_BLOCK_SIZE];
    
    // Convert hex string to bytes
    for (int i = 0; i < TWOFISH_BLOCK_SIZE; i++) {
        char hex_byte[3] = {ciphertext_hex[i*2], ciphertext_hex[i*2+1], '\0'};
        ciphertext[i] = (u1byte)strtol(hex_byte, NULL, 16);
    }
    
    twofish_decrypt(&global_ctx, ciphertext, plaintext);
    
    // Convert result back to hex string
    char* result = (char*)malloc(33); // 32 hex chars + null terminator
    if (!result) return NULL;
    
    for (int i = 0; i < TWOFISH_BLOCK_SIZE; i++) {
        sprintf(result + i*2, "%02x", plaintext[i]);
    }
    result[32] = '\0';
    
    return result;
}

EMSCRIPTEN_KEEPALIVE
void twofish_free_string(char* ptr) {
    if (ptr) free(ptr);
}

// ECB mode encryption for larger data
EMSCRIPTEN_KEEPALIVE
char* twofish_encrypt_ecb(const char* plaintext_hex) {
    if (!plaintext_hex) return NULL;
    
    int hex_len = strlen(plaintext_hex);
    if (hex_len % 32 != 0) return NULL; // Must be multiple of 16 bytes
    
    int num_blocks = hex_len / 32;
    char* result = (char*)malloc(hex_len + 1);
    if (!result) return NULL;
    
    for (int i = 0; i < num_blocks; i++) {
        char block_hex[33];
        strncpy(block_hex, plaintext_hex + i*32, 32);
        block_hex[32] = '\0';
        
        char* encrypted_block = twofish_encrypt_block(block_hex);
        if (!encrypted_block) {
            free(result);
            return NULL;
        }
        
        strcpy(result + i*32, encrypted_block);
        twofish_free_string(encrypted_block);
    }
    
    result[hex_len] = '\0';
    return result;
}

EMSCRIPTEN_KEEPALIVE
char* twofish_decrypt_ecb(const char* ciphertext_hex) {
    if (!ciphertext_hex) return NULL;
    
    int hex_len = strlen(ciphertext_hex);
    if (hex_len % 32 != 0) return NULL; // Must be multiple of 16 bytes
    
    int num_blocks = hex_len / 32;
    char* result = (char*)malloc(hex_len + 1);
    if (!result) return NULL;
    
    for (int i = 0; i < num_blocks; i++) {
        char block_hex[33];
        strncpy(block_hex, ciphertext_hex + i*32, 32);
        block_hex[32] = '\0';
        
        char* decrypted_block = twofish_decrypt_block(block_hex);
        if (!decrypted_block) {
            free(result);
            return NULL;
        }
        
        strcpy(result + i*32, decrypted_block);
        twofish_free_string(decrypted_block);
    }
    
    result[hex_len] = '\0';
    return result;
}

2. JavaScript Wrapper (twofish.js)

class TwofishCrypto {
    constructor() {
        this.module = null;
    }

    async init(wasmModule) {
        this.module = wasmModule;
    }

    // Helper function to convert string pointers to JavaScript strings
    _ptrToString(ptr) {
        if (!ptr) return null;
        const result = this.module.UTF8ToString(ptr);
        this.module._twofish_free_string(ptr);
        return result;
    }

    // Set the encryption key (hex string)
    setKey(keyHex) {
        if (typeof keyHex !== 'string') {
            throw new Error('Key must be a hex string');
        }
        
        if (!/^[0-9a-fA-F]*$/.test(keyHex)) {
            throw new Error('Key must contain only hex characters');
        }
        
        if (keyHex.length > 64) { // 32 bytes max
            throw new Error('Key too long (max 32 bytes)');
        }
        
        const keyPtr = this.module.stringToUTF8OnStack(keyHex);
        const result = this.module._twofish_init_key(keyPtr);
        
        if (result !== 0) {
            throw new Error(`Failed to set key (error code: ${result})`);
        }
    }

    // Encrypt a single 16-byte block (32 hex characters)
    encryptBlock(plaintextHex) {
        if (typeof plaintextHex !== 'string') {
            throw new Error('Plaintext must be a hex string');
        }
        
        if (plaintextHex.length !== 32) {
            throw new Error('Block must be exactly 16 bytes (32 hex characters)');
        }
        
        if (!/^[0-9a-fA-F]*$/.test(plaintextHex)) {
            throw new Error('Plaintext must contain only hex characters');
        }
        
        const plaintextPtr = this.module.stringToUTF8OnStack(plaintextHex);
        const resultPtr = this.module._twofish_encrypt_block(plaintextPtr);
        
        return this._ptrToString(resultPtr);
    }

    // Decrypt a single 16-byte block (32 hex characters)
    decryptBlock(ciphertextHex) {
        if (typeof ciphertextHex !== 'string') {
            throw new Error('Ciphertext must be a hex string');
        }
        
        if (ciphertextHex.length !== 32) {
            throw new Error('Block must be exactly 16 bytes (32 hex characters)');
        }
        
        if (!/^[0-9a-fA-F]*$/.test(ciphertextHex)) {
            throw new Error('Ciphertext must contain only hex characters');
        }
        
        const ciphertextPtr = this.module.stringToUTF8OnStack(ciphertextHex);
        const resultPtr = this.module._twofish_decrypt_block(ciphertextPtr);
        
        return this._ptrToString(resultPtr);
    }

    // Encrypt data in ECB mode (multiple blocks)
    encryptECB(plaintextHex) {
        if (typeof plaintextHex !== 'string') {
            throw new Error('Plaintext must be a hex string');
        }
        
        if (plaintextHex.length % 32 !== 0) {
            throw new Error('Data length must be multiple of 16 bytes (32 hex characters)');
        }
        
        if (!/^[0-9a-fA-F]*$/.test(plaintextHex)) {
            throw new Error('Plaintext must contain only hex characters');
        }
        
        const plaintextPtr = this.module.stringToUTF8OnStack(plaintextHex);
        const resultPtr = this.module._twofish_encrypt_ecb(plaintextPtr);
        
        return this._ptrToString(resultPtr);
    }

    // Decrypt data in ECB mode (multiple blocks)
    decryptECB(ciphertextHex) {
        if (typeof ciphertextHex !== 'string') {
            throw new Error('Ciphertext must be a hex string');
        }
        
        if (ciphertextHex.length % 32 !== 0) {
            throw new Error('Data length must be multiple of 16 bytes (32 hex characters)');
        }
        
        if (!/^[0-9a-fA-F]*$/.test(ciphertextHex)) {
            throw new Error('Ciphertext must contain only hex characters');
        }
        
        const ciphertextPtr = this.module.stringToUTF8OnStack(ciphertextHex);
        const resultPtr = this.module._twofish_decrypt_ecb(ciphertextPtr);
        
        return this._ptrToString(resultPtr);
    }

    // Utility functions for string/hex conversion
    static stringToHex(str) {
        return Array.from(str)
            .map(c => c.charCodeAt(0).toString(16).padStart(2, '0'))
            .join('');
    }

    static hexToString(hex) {
        if (hex.length % 2 !== 0) {
            throw new Error('Hex string must have even length');
        }
        
        let result = '';
        for (let i = 0; i < hex.length; i += 2) {
            result += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
        }
        return result;
    }

    // Convenience methods for string encryption/decryption
    encryptString(plaintext, key) {
        this.setKey(key);
        
        // Convert string to hex and pad to block size
        let hex = TwofishCrypto.stringToHex(plaintext);
        
        // Pad to multiple of 32 hex chars (16 bytes)
        while (hex.length % 32 !== 0) {
            hex += '00';
        }
        
        return this.encryptECB(hex);
    }

    decryptString(ciphertextHex, key) {
        this.setKey(key);
        
        const decryptedHex = this.decryptECB(ciphertextHex);
        
        // Convert back to string and remove padding
        let result = TwofishCrypto.hexToString(decryptedHex);
        
        // Remove null byte padding
        return result.replace(/\0+$/, '');
    }
}

// Export for different module systems
if (typeof module !== 'undefined' && module.exports) {
    module.exports = TwofishCrypto;
} else if (typeof window !== 'undefined') {
    window.TwofishCrypto = TwofishCrypto;
}

3. Build Script (build.sh)

#!/bin/bash

# Build script for Twofish Emscripten wrapper
set -e

echo "Building Twofish Emscripten wrapper..."

# Create build directory
mkdir -p build

# Extract and prepare source files
echo "Preparing source files..."

# You'll need to extract the actual implementation from TWOFISH2.C
# and create separate .c files for compilation

# Compile with Emscripten
emcc -O3 \
    -s WASM=1 \
    -s EXPORTED_RUNTIME_METHODS='["stringToUTF8OnStack","UTF8ToString"]' \
    -s EXPORTED_FUNCTIONS='["_twofish_init_key","_twofish_encrypt_block","_twofish_decrypt_block","_twofish_encrypt_ecb","_twofish_decrypt_ecb","_twofish_free_string"]' \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s MODULARIZE=1 \
    -s EXPORT_NAME='TwofishModule' \
    -I. \
    twofish_wrapper.c twofish2_extracted.c \
    -o build/twofish.js

echo "Build complete! Files generated:"
echo "  build/twofish.js"
echo "  build/twofish.wasm"

4. HTML Example (example.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Twofish Encryption Example</title>
</head>
<body>
    <h1>Twofish Encryption Demo</h1>
    
    <div>
        <h3>Encrypt Text</h3>
        <input type="text" id="plaintext" placeholder="Enter text to encrypt" />
        <input type="text" id="key" placeholder="Enter key (hex)" />
        <button onclick="encrypt()">Encrypt</button>
        <div>Encrypted: <span id="encrypted"></span></div>
    </div>
    
    <div>
        <h3>Decrypt Text</h3>
        <input type="text" id="ciphertext" placeholder="Enter encrypted text (hex)" />
        <input type="text" id="decryptKey" placeholder="Enter key (hex)" />
        <button onclick="decrypt()">Decrypt</button>
        <div>Decrypted: <span id="decrypted"></span></div>
    </div>

    <script src="build/twofish.js"></script>
    <script src="twofish.js"></script>
    <script>
        let twofish = null;

        // Initialize the Twofish module
        TwofishModule().then(module => {
            twofish = new TwofishCrypto();
            twofish.init(module);
            console.log('Twofish module loaded successfully');
        }).catch(err => {
            console.error('Failed to load Twofish module:', err);
        });

        function encrypt() {
            if (!twofish) {
                alert('Twofish module not loaded yet');
                return;
            }

            try {
                const plaintext = document.getElementById('plaintext').value;
                const key = document.getElementById('key').value;
                
                if (!plaintext || !key) {
                    alert('Please enter both plaintext and key');
                    return;
                }

                const encrypted = twofish.encryptString(plaintext, key);
                document.getElementById('encrypted').textContent = encrypted;
            } catch (err) {
                alert('Encryption error: ' + err.message);
            }
        }

        function decrypt() {
            if (!twofish) {
                alert('Twofish module not loaded yet');
                return;
            }

            try {
                const ciphertext = document.getElementById('ciphertext').value;
                const key = document.getElementById('decryptKey').value;
                
                if (!ciphertext || !key) {
                    alert('Please enter both ciphertext and key');
                    return;
                }

                const decrypted = twofish.decryptString(ciphertext, key);
                document.getElementById('decrypted').textContent = decrypted;
            } catch (err) {
                alert('Decryption error: ' + err.message);
            }
        }

        // Example usage
        window.addEventListener('load', () => {
            TwofishModule().then(module => {
                const crypto = new TwofishCrypto();
                crypto.init(module);
                
                // Test with a known vector
                const key = "0123456789abcdef0123456789abcdef";
                const plaintext = "hello world!!!!"; // 16 bytes
                
                console.log('Testing Twofish encryption...');
                console.log('Key:', key);
                console.log('Plaintext:', plaintext);
                
                const encrypted = crypto.encryptString(plaintext, key);
                console.log('Encrypted:', encrypted);
                
                const decrypted = crypto.decryptString(encrypted, key);
                console.log('Decrypted:', decrypted);
                
                console.log('Test passed:', plaintext === decrypted);
            });
        });
    </script>
</body>
</html>

5. Package.json for npm package

{
  "name": "twofish-wasm",
  "version": "1.0.0",
  "description": "Twofish encryption algorithm compiled to WebAssembly",
  "main": "twofish.js",
  "files": [
    "build/",
    "twofish.js",
    "README.md"
  ],
  "scripts": {
    "build": "./build.sh",
    "test": "node test.js"
  },
  "keywords": [
    "twofish",
    "encryption",
    "cryptography",
    "webassembly",
    "wasm"
  ],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "emscripten": "^3.1.0"
  }
}

Important Notes:

  1. Extract the Implementation: You'll need to extract the actual Twofish implementation from TWOFISH2.C and create a clean, compilable version. The original file is quite large and contains test code.

  2. Platform Dependencies: Make sure to handle the platform-specific types defined in PLATFORM.H.

  3. Table Dependencies: Include the lookup tables from TABLE.H in your build.

  4. Memory Management: The wrapper handles memory allocation/deallocation properly for the JavaScript interface.

  5. Error Handling: Comprehensive error checking is included for invalid inputs.

  6. Multiple Modes: The wrapper supports both single-block and ECB multi-block encryption.

To complete this implementation, you'll need to:

  1. Extract the core Twofish functions from the original TWOFISH2.C
  2. Create clean header files with the necessary definitions
  3. Compile with Emscripten using the provided build script
  4. Test with known test vectors

This wrapper provides a clean, modern JavaScript interface for the Twofish algorithm while maintaining the performance benefits of the optimized C implementation.

@vitonsky

Copy link
Copy Markdown
Member Author

Gemini 2.5 Pro (cpp approach)

An Emscripten wrapper allows you to compile C/C++ code into WebAssembly (Wasm) and JavaScript, making it possible to run high-performance native code directly in a web browser.

Here is a complete Emscripten wrapper for the provided C implementation of the Twofish block cipher. This solution uses Emscripten's embind tool to create a clean, modern JavaScript class that is easy to use.

Project Structure

Here are the files that constitute the complete project:

.
├── build.sh            # Build script to compile the project
├── index.html          # Example HTML page to demonstrate usage
├── README.md           # Instructions and explanation
├── wrapper.cpp         # The C++ wrapper code using embind
└── src/                # Original C source files from the zip
    ├── twofish2.c
    ├── twofish2.h
    ├── platform.h
    └── table.h

1. Original C Source Files

First, extract the necessary files from the provided twofish-optimized-c(1).zip archive and place them in a src/ directory. The required files are:

  • twofish2.c: The core Twofish implementation.
  • twofish2.h: Header file for the Twofish implementation.
  • platform.h: Defines platform-specific types. The original file works with Emscripten without modification.
  • table.h: Contains pre-computed lookup tables for the algorithm.

2. The C++ Embind Wrapper (wrapper.cpp)

This file acts as the bridge between the C implementation and JavaScript. It defines a C++ class that will be "bound" to a corresponding JavaScript class. It handles the memory and data type conversions, exposing a simple API to JavaScript. We use emscripten::typed_memory_view for efficient handling of binary data (Uint8Array) between JS and Wasm.

// wrapper.cpp
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <vector>

// This makes the C functions from the Twofish source available to the C++ wrapper
extern "C" {
    #include "src/twofish2.h"
}

/**
 * @class TwofishWrapper
 * A C++ class that wraps the C Twofish API for use with Emscripten's embind.
 * This class is not intended for direct C++ use, but to be exposed to JavaScript.
 */
class TwofishWrapper {
private:
    keyInstance encKey;     // Structure to hold the encryption key schedule
    keyInstance decKey;     // Structure to hold the decryption key schedule
    cipherInstance cipher;  // Structure for cipher configuration (mode, IV)
    bool initialized;       // Flag to check if initialization was successful

public:
    /**
     * Constructor that takes a key from JavaScript.
     * @param key_mv A memory view of a JS Uint8Array (16, 24, or 32 bytes).
     */
    TwofishWrapper(emscripten::typed_memory_view<uint8_t> key_mv) : initialized(false) {
        int keyLenBytes = key_mv.size();
        if (keyLenBytes != 16 && keyLenBytes != 24 && keyLenBytes != 32) {
            // Initialization fails if key is not 128, 192, or 256 bits
            return;
        }
        int keyLenBits = keyLenBytes * 8;

        // The C API wants a char*, so we get a pointer to the JS data and cast it.
        // We use const_cast because makeKey is not const-correct, but we know it only reads the key.
        const BYTE* keyData = key_mv.begin();
        char* keyMaterial = const_cast<char*>(reinterpret_cast<const char*>(keyData));

        // Create the key schedules for both encryption and decryption
        if (makeKey(&encKey, DIR_ENCRYPT, keyLenBits, keyMaterial) != 1) return;
        if (makeKey(&decKey, DIR_DECRYPT, keyLenBits, keyMaterial) != 1) return;
        
        // Initialize the cipher for ECB mode. ECB is simple and doesn't require an IV.
        if (cipherInit(&cipher, MODE_ECB, NULL) != 1) return;

        initialized = true;
    }

    /**
     * Exposes the initialization status to JavaScript.
     */
    bool isInitialized() const {
        return initialized;
    }

    /**
     * Encrypts a block of data.
     * @param plaintext_mv A memory view of a JS Uint8Array (must be multiple of 16 bytes).
     * @return A JS Uint8Array with the ciphertext, or null on failure.
     */
    emscripten::val encrypt(emscripten::typed_memory_view<uint8_t> plaintext_mv) {
        if (!initialized || plaintext_mv.size() == 0 || plaintext_mv.size() % 16 != 0) {
            return emscripten::val::null();
        }

        size_t data_size = plaintext_mv.size();
        const BYTE* plaintext_ptr = plaintext_mv.begin();
        std::vector<BYTE> ciphertext(data_size); // C++ buffer for the result

        // Encrypt block by block (16 bytes at a time)
        for (size_t i = 0; i < data_size; i += 16) {
            // C API is not const-correct, so we must cast away constness of the input buffer.
            BYTE* current_input_block = const_cast<BYTE*>(&plaintext_ptr[i]);
            BYTE* current_output_block = &ciphertext[i];
            blockEncrypt(&cipher, &encKey, current_input_block, 128, current_output_block);
        }

        // Return the result as a new JS Uint8Array
        return emscripten::val(emscripten::typed_memory_view(ciphertext.size(), ciphertext.data()));
    }

    /**
     * Decrypts a block of data.
     * @param ciphertext_mv A memory view of a JS Uint8Array (must be multiple of 16 bytes).
     * @return A JS Uint8Array with the decrypted plaintext, or null on failure.
     */
    emscripten::val decrypt(emscripten::typed_memory_view<uint8_t> ciphertext_mv) {
        if (!initialized || ciphertext_mv.size() == 0 || ciphertext_mv.size() % 16 != 0) {
            return emscripten::val::null();
        }

        size_t data_size = ciphertext_mv.size();
        const BYTE* ciphertext_ptr = ciphertext_mv.begin();
        std::vector<BYTE> decryptedtext(data_size); // C++ buffer for the result

        // Decrypt block by block (16 bytes at a time)
        for (size_t i = 0; i < data_size; i += 16) {
            BYTE* current_input_block = const_cast<BYTE*>(&ciphertext_ptr[i]);
            BYTE* current_output_block = &decryptedtext[i];
            blockDecrypt(&cipher, &decKey, current_input_block, 128, current_output_block);
        }

        // Return the result as a new JS Uint8Array
        return emscripten::val(emscripten::typed_memory_view(decryptedtext.size(), decryptedtext.data()));
    }
};

// This is the binding code that exposes the C++ class and its methods to JavaScript.
EMSCRIPTEN_BINDINGS(twofish_module) {
    emscripten::class_<TwofishWrapper>("Twofish")
        .constructor<emscripten::typed_memory_view<uint8_t>>()
        .function("isInitialized", &TwofishWrapper::isInitialized)
        .function("encrypt", &TwofishWrapper::encrypt)
        .function("decrypt", &TwofishWrapper::decrypt)
        // The .delete() method is automatically added by embind to call the C++ destructor
        ;
}

3. Build Script (build.sh)

This script invokes the Emscripten compiler (emcc) with the necessary flags to create twofish.js and twofish.wasm. Make sure this script is executable (chmod +x build.sh).

#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
set -e

# Define the source files
CPP_WRAPPER="wrapper.cpp"
C_SOURCE="src/twofish2.c"
OUTPUT_FILE="twofish.js"
INCLUDES="-I ." # Look for headers in the current directory (finds src/*)

# Emscripten compiler command
emcc -O3 --bind \
    -s MODULARIZE=1 \
    -s EXPORT_NAME="createTwofishModule" \
    -s ENVIRONMENT='web,worker' \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s WASM=1 \
    ${INCLUDES} \
    ${CPP_WRAPPER} \
    ${C_SOURCE} \
    -o ${OUTPUT_FILE}

echo "Build complete: ${OUTPUT_FILE} and twofish.wasm created."

4. Example Usage (index.html)

This file demonstrates how to load the WebAssembly module and use the resulting Twofish class to encrypt and decrypt data in the browser.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Emscripten Twofish Wrapper Demo</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; padding: 1em; max-width: 800px; margin: auto; }
        label { display: block; margin-top: 1em; font-weight: bold; }
        textarea, input { box-sizing: border-box; width: 100%; font-family: monospace; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
        textarea { height: 100px; }
        button { padding: 10px 15px; margin-top: 1em; margin-right: 0.5em; border: none; border-radius: 4px; background-color: #007bff; color: white; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        #status { font-style: italic; color: #555; }
    </style>
</head>
<body>
    <h1>Emscripten Twofish Wrapper Demo</h1>
    <p id="status">Loading WebAssembly module...</p>
    <p>This demo uses Twofish in <strong>ECB mode</strong>. The plaintext will be automatically padded with zeros to a multiple of 16 bytes before encryption.</p>
    
    <label for="key">Key (16, 24, or 32 characters for a 128, 192, or 256-bit key):</label>
    <input type="text" id="key" value="0123456789ABCDEF">

    <label for="plaintext">Plaintext:</label>
    <textarea id="plaintext">This is a secret message.</textarea>

    <button id="encryptBtn" disabled>Encrypt</button>
    <button id="decryptBtn" disabled>Decrypt</button>

    <label for="ciphertext">Ciphertext (Hex):</label>
    <textarea id="ciphertext" readonly></textarea>

    <label for="result">Decryption Result:</label>
    <textarea id="result" readonly></textarea>

    <!-- Load the Emscripten-generated JS file -->
    <script src="twofish.js"></script>
    <script>
        // --- Helper Functions ---
        const encoder = new TextEncoder();
        const decoder = new TextDecoder();

        function bytesToHex(bytes) {
            return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
        }

        // --- Main Application Logic ---
        const keyInput = document.getElementById('key');
        const plaintextInput = document.getElementById('plaintext');
        const ciphertextInput = document.getElementById('ciphertext');
        const resultInput = document.getElementById('result');
        const encryptBtn = document.getElementById('encryptBtn');
        const decryptBtn = document.getElementById('decryptBtn');
        const statusEl = document.getElementById('status');

        let Module, twofishInstance = null, currentCiphertextBytes = null;
        
        // Emscripten modules are loaded asynchronously
        createTwofishModule().then(loadedModule => {
            Module = loadedModule;
            statusEl.textContent = "WebAssembly module loaded successfully.";
            encryptBtn.disabled = false;
        });

        function getTwofishInstance() {
            // Clean up previous instance if it exists
            if (twofishInstance) {
                twofishInstance.delete();
                twofishInstance = null;
            }
            
            const keyBytes = encoder.encode(keyInput.value);
            if (keyBytes.length !== 16 && keyBytes.length !== 24 && keyBytes.length !== 32) {
                alert("Invalid key length. Must be 16, 24, or 32 bytes.");
                return null;
            }

            try {
                // Pass the Uint8Array key directly to the constructor
                const instance = new Module.Twofish(keyBytes);
                if (!instance.isInitialized()) {
                    instance.delete(); // Important to free memory even on failure
                    throw new Error("Initialization failed. Check key length.");
                }
                return instance;
            } catch (e) {
                alert(`Error initializing Twofish: ${e.message}`);
                return null;
            }
        }

        encryptBtn.addEventListener('click', () => {
            twofishInstance = getTwofishInstance();
            if (!twofishInstance) return;
            
            const plaintextBytes = encoder.encode(plaintextInput.value);
            
            // Pad with zeros to a multiple of 16 (the block size)
            const remainder = plaintextBytes.length % 16;
            let paddedPlaintext = plaintextBytes;
            if (remainder !== 0) {
                paddedPlaintext = new Uint8Array(plaintextBytes.length + (16 - remainder));
                paddedPlaintext.set(plaintextBytes);
            }

            const encryptedBytes = twofishInstance.encrypt(paddedPlaintext);
            if (!encryptedBytes) {
                alert("Encryption failed.");
                return;
            }

            currentCiphertextBytes = encryptedBytes;
            ciphertextInput.value = bytesToHex(encryptedBytes);
            resultInput.value = '';
            decryptBtn.disabled = false;
        });
      
        decryptBtn.addEventListener('click', () => {
            if (!currentCiphertextBytes) {
                alert("Please encrypt a message first!");
                return;
            }
            
            // Re-initialize with the same key
            twofishInstance = getTwofishInstance();
            if (!twofishInstance) return;

            const decryptedBytes = twofishInstance.decrypt(currentCiphertextBytes);
            if (!decryptedBytes) {
                alert("Decryption failed.");
                return;
            }
            
            // Display the result. The TextDecoder will automatically handle
            // trimming trailing null bytes from our simple padding scheme.
            resultInput.value = decoder.decode(decryptedBytes);
        });
    </script>
</body>
</html>

5. README.md

# Emscripten Twofish Wrapper

This project provides a WebAssembly wrapper around a C implementation of the Twofish block cipher, allowing it to be used in modern web browsers.

It uses Emscripten's `embind` feature to expose a clean, object-oriented JavaScript API.

## Prerequisites

-   You must have the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) installed and activated in your shell environment.

## File Structure

-   `src/`: Contains the original C source code for the Twofish algorithm.
-   `wrapper.cpp`: The C++ wrapper that uses `embind` to bridge C and JavaScript.
-   `build.sh`: A shell script to compile the C/C++ code into `twofish.js` and `twofish.wasm`.
-   `index.html`: A demonstration page showing how to load and use the module.
-   `README.md`: This file.

## How to Build

1.  Make sure the Emscripten environment is active (e.g., by running `source ./emsdk_env.sh` from your Emscripten SDK directory).
2.  Make the build script executable:
    ```bash
    chmod +x build.sh
    ```
3.  Run the build script:
    ```bash
    ./build.sh
    ```
    This will generate `twofish.js` and `twofish.wasm` in the root directory.

## How to Run the Demo

After building the project, you need to serve the files from a local web server (you cannot simply open `index.html` from the filesystem due to security restrictions on loading Wasm).

A simple way to do this is with Python:

```bash
# For Python 3
python -m http.server

# For Python 2
python -m SimpleHTTPServer

Then, open your web browser and navigate to http://localhost:8000.

JavaScript API

The generated module exposes a Twofish class.

new Twofish(key)

  • key: A Uint8Array containing the encryption key. Must be 16, 24, or 32 bytes long (for 128, 192, or 256-bit keys, respectively).
  • Returns: A new Twofish instance.

instance.encrypt(plaintext)

  • plaintext: A Uint8Array of data to encrypt. The length must be a multiple of 16 bytes.
  • Returns: A Uint8Array containing the ciphertext, or null if an error occurred.

instance.decrypt(ciphertext)

  • ciphertext: A Uint8Array of data to decrypt. The length must be a multiple of 16 bytes.
  • Returns: A Uint8Array containing the original plaintext, or null if an error occurred.

instance.delete()

  • Frees the memory associated with the C++ object. You should call this when you are finished with an instance to prevent memory leaks.

@vitonsky

vitonsky commented Apr 9, 2026

Copy link
Copy Markdown
Member Author

Implemented in #271

@vitonsky vitonsky closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant