Skip to content

libcaptcha/silent-challenge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

silent-challenge

Passive captcha combining motion attestation, navigator attestation, and SHA-256 balloon proof-of-work. Attestation scripts are compiled to encrypted bytecode and executed inside a polymorphic QuickJS WASM sandbox that automatically encrypts and signs responses.

How It Works

The attestation code does not run directly in the browser. It is compiled to QuickJS bytecode, encrypted with ChaCha20, and delivered as a .vmbc bundle. The WASM binary is regenerated every 10 minutes with fresh keys, dead code, and renamed symbols β€” making static analysis and replay attacks impractical.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Server (every 10 min)                                   β”‚
β”‚                                                         β”‚
β”‚  1. node scripts/build.js    β†’ vm.wasm + manifest.json  β”‚
β”‚  2. node scripts/compile.js  β†’ attestation.vmbc         β”‚
β”‚     (source: attestation expression β†’ encrypted bundle) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
          POST /challenge β†’ { challengeId, nonce, pow, vmbc }
                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browser (parallel)                                      β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€ WASM VM ──────────────────────────────────┐         β”‚
β”‚  β”‚  vm_init()                                 β”‚         β”‚
β”‚  β”‚  vm_exec_bytecode(attestation.vmbc)        β”‚         β”‚
β”‚  β”‚    ChaCha20 decrypt β†’ QuickJS eval         β”‚         β”‚
β”‚  β”‚    β†’ collects motion + navigator signals   β”‚         β”‚
β”‚  β”‚    ChaCha20 encrypt + HMAC-SHA256 sign     β”‚         β”‚
β”‚  β”‚    β†’ encrypted + signed response           β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€ Web Workers ──────────────────────────────┐         β”‚
β”‚  β”‚  sha256-balloon PoW mining                 β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
          POST /challenge/:id/verify β†’ { vmResponse, nonce }
                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Server                                                  β”‚
β”‚                                                         β”‚
β”‚  1. Verify HMAC signature (KEY_SIGN from manifest)      β”‚
β”‚  2. Decrypt attestation result (KEY_ENCRYPT)            β”‚
β”‚  3. Verify PoW (balloon hash β‰₯ difficulty)              β”‚
β”‚  4. Analyze motion data (behavioral biometrics)         β”‚
β”‚  5. Validate navigator signals (24 categories)          β”‚
β”‚  6. Compute weighted score β†’ issue signed token         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Bytecode Format

The attestation source is a JavaScript expression compiled with node scripts/compile.js. The VM evaluates it and returns the result as the response payload:

JSON.stringify({
    m: __motion_data__,
    s: __navigator_signals__,
    ts: __vm_ts(),
    integrity: __vm_integrity(),
});

This follows the same pattern as any QuickJS bytecode source:

'Hello from compiled bytecode!';

The compile step converts JS β†’ QuickJS bytecode β†’ encrypted .vmbc:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4B magic β”‚ 4B bc_len LE β”‚ 12B nonceβ”‚ ciphertext β”‚
β”‚ "VMBC"   β”‚              β”‚          β”‚            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The VM wraps the execution result in an encrypted signed response:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4B magic β”‚ 4B total_len β”‚ 12B nonce β”‚ ciphertext β”‚ 32B HMAC β”‚
β”‚ "VMRP"   β”‚ LE           β”‚           β”‚            β”‚          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The server holds manifest.json with the matching keys to verify the HMAC and decrypt the ciphertext.

WASM Regeneration

Every 10 minutes the server rebuilds the WASM binary. Each build:

  • Generates fresh 32-byte ChaCha20/HMAC keys
  • Injects 20-40 dead code functions with realistic bodies
  • Renames all exported and internal symbols
  • Shuffles key material across randomized decoy arrays
  • Applies control flow flattening and opaque predicates
  • Recompiles the attestation source against the new keys

This means every client receives a structurally unique binary. Reverse engineering one build does not help with the next.

Vendors

Package Role Usage
motion-attestation Behavioral biometrics collection dependency
navigator-attestation Browser environment fingerprinting dependency
sha256-balloon Memory-hard proof-of-work dependency
quickjs-wasm Polymorphic WASM sandbox reference

Install

npm install silent-challenge

The three attestation/PoW packages are runtime dependencies. The quickjs-wasm package is a build-time reference for compiling and serving the WASM sandbox.

Server

import express from 'express';
import { silentMiddleware } from 'silent-challenge/server';

const app = express();
const silent = silentMiddleware({
    secret: process.env.SILENT_SECRET,
    pow: { difficulty: 10, spaceCost: 512 },
    thresholds: { combined: 0.5, motion: 0.3, navigator: 0.3 },
    debug: false,
});

app.use(express.json({ limit: '64kb' }));
silent.mountRoutes(app);

app.get('/protected', silent.requireToken, (req, res) => {
    res.json({
        message: 'Access granted',
        score: req.silentPayload.score,
    });
});

app.listen(3000);

Routes

Method Path Description
POST /challenge Issue challenge + PoW
POST /challenge/:challengeId/verify Submit solution

Options

silentMiddleware({
    secret: string, // HMAC secret (auto-generated)
    debug: boolean, // include full analysis detail
    weights: {
        motion: 0.4, // behavioral biometrics weight
        navigator: 0.35, // environment attestation weight
        pow: 0.25, // proof-of-work weight
    },
    pow: {
        difficulty: 10, // leading zero bits required
        spaceCost: 512, // balloon memory blocks (Γ— 32B)
        timeCost: 1, // balloon mixing rounds
    },
    thresholds: {
        combined: 0.5, // minimum weighted score
        motion: 0.3, // minimum motion score
        navigator: 0.3, // minimum navigator score
    },
});

Browser Client

import { createClient } from 'silent-challenge/client';

const client = createClient({
    baseUrl: '',
    workerUrl: './worker.js',
    onProgress: ({ hashes, hashRate }) => {
        console.log(`${hashes} hashes @ ${hashRate} H/s`);
    },
});

// Start passive collection
const collector = client.attach(document, window);
collector.bind(document.getElementById('submit'), 'submit');

// When ready (e.g. form submit)
const result = await client.verify();
// result.cleared, result.token, result.score

The client runs three tasks in parallel:

  1. Motion collection β€” mouse, clicks, keystrokes, scroll, touch, sensors, event order (via motion-attestation)
  2. Navigator signals β€” 24 categories: automation markers, headless detection, VM indicators, canvas/WebGL fingerprints, prototype tampering (via navigator-attestation)
  3. PoW mining β€” multi-worker balloon hashing (via sha256-balloon)

Scoring

The combined score is a weighted sum:

score = motion Γ— 0.40 + navigator Γ— 0.35 + pow Γ— 0.25

PoW score is binary (1.0 if valid, 0.0 if not). Motion and navigator scores range from 0.0 to 1.0 based on anomaly penalties. A token is issued only when all three individual thresholds and the combined threshold are met.

Example

npm start
# β†’ http://localhost:3000

Interactive demo with step indicators, live stats, and score visualization.

Test

npm test

Project Structure

src/
  index.js           Barrel exports
  index.d.ts         TypeScript definitions
  challenge.js       Challenge issuance + combined verification
  server.js          Express middleware (mountRoutes, requireToken)
  client.js          Browser orchestrator (collect + solve + submit)

License

MIT

About

Combines motion and navigator attestation into a single passive challenge that runs concurrently inside the QuickJS WebAssembly sandbox while the SHA-256 balloon proof-of-work is being computed.

Topics

Resources

License

Stars

Watchers

Forks

Contributors