Initial ChronoSeal release
This commit is contained in:
commit
a66debdece
48 files changed
+2362
No files matched your search
@@ -0,0 +1,21 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --workspace --release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --workspace
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
target/
|
||||||
|
pkg/
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Rust stable
|
||||||
|
- wasm-pack
|
||||||
|
- NodeJS (optional frontend tooling)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt
|
||||||
|
cargo clippy
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Keep security-sensitive logic inside Rust/WASM
|
||||||
|
- Avoid placing trust logic in JavaScript
|
||||||
|
- Preserve silent-failure behavior
|
||||||
|
- Maintain deterministic protocol serialization
|
||||||
Generated
+1302
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
members = [
|
||||||
|
"shared",
|
||||||
|
"server",
|
||||||
|
"wasm"
|
||||||
|
]
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
FROM rust:1.88-bookworm AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN cargo build -p server --release
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /opt/chronoseal
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/release/server /usr/local/bin/chronoseal
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
|
||||||
|
CMD ["chronoseal"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
This project is licensed under GPLv3.
|
||||||
|
https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# ChronoSeal
|
||||||
|
|
||||||
|
ChronoSeal is a high-security anti-automation and anti-AI-scraping framework built using Rust, WASM, cryptographic heartbeat ledgers, and behavioral attestation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Rust + Axum backend
|
||||||
|
- WASM runtime verification
|
||||||
|
- Ed25519 signatures
|
||||||
|
- Blake3 hash-chain continuity
|
||||||
|
- Behavioral entropy collection
|
||||||
|
- Silent anti-bot mitigation
|
||||||
|
- Adaptive trust scoring
|
||||||
|
- Stateless HTTP verification
|
||||||
|
- GPLv3 licensed
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
Browser
|
||||||
|
├── WASM VM
|
||||||
|
├── Heartbeat protocol
|
||||||
|
├── Cryptographic ledger
|
||||||
|
└── Behavioral attestation
|
||||||
|
|
||||||
|
Server
|
||||||
|
├── Session validation
|
||||||
|
├── Hash-chain verification
|
||||||
|
├── Trust scoring
|
||||||
|
└── Adaptive mitigation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p server --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wasm-pack build wasm --target web --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPLv3
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting Vulnerabilities
|
||||||
|
|
||||||
|
Please do not disclose security vulnerabilities publicly before responsible disclosure.
|
||||||
|
|
||||||
|
Contact maintainers privately with:
|
||||||
|
- reproduction steps
|
||||||
|
- affected versions
|
||||||
|
- impact assessment
|
||||||
|
- proof-of-concept if applicable
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
ChronoSeal intentionally operates as:
|
||||||
|
- anti-automation middleware
|
||||||
|
- behavioral attestation layer
|
||||||
|
- cryptographic continuity verifier
|
||||||
|
|
||||||
|
Security hardening evolves continuously.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ChronoSeal Anti-Bot Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
User=chronoseal
|
||||||
|
Group=chronoseal
|
||||||
|
|
||||||
|
WorkingDirectory=/opt/chronoseal
|
||||||
|
|
||||||
|
ExecStart=/usr/local/bin/chronoseal
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
|
||||||
|
LockPersonality=true
|
||||||
|
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
chronoseal:
|
||||||
|
build: .
|
||||||
|
container_name: chronoseal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
RUST_LOG: info
|
||||||
|
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# ChronoSeal Architecture
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
- Continuous browser attestation
|
||||||
|
- Cryptographic heartbeat chains
|
||||||
|
- WASM-isolated secrets
|
||||||
|
- Behavioral entropy verification
|
||||||
|
- Silent mitigation
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### WASM Runtime
|
||||||
|
|
||||||
|
Responsible for:
|
||||||
|
- heartbeat generation
|
||||||
|
- signature generation
|
||||||
|
- entropy collection
|
||||||
|
- VM execution
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
Responsible for:
|
||||||
|
- session verification
|
||||||
|
- trust scoring
|
||||||
|
- chain validation
|
||||||
|
- mitigation
|
||||||
|
|
||||||
|
## Threat Model
|
||||||
|
|
||||||
|
Designed to increase:
|
||||||
|
- scraping cost
|
||||||
|
- operational complexity
|
||||||
|
- synchronization burden
|
||||||
|
|
||||||
|
ChronoSeal does not attempt impossible perfect prevention.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Native
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p server --release
|
||||||
|
sudo cp target/release/server /usr/local/bin/chronoseal
|
||||||
|
```
|
||||||
|
|
||||||
|
## systemd
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp chronoseal.service /etc/systemd/system/
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable chronoseal
|
||||||
|
sudo systemctl start chronoseal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reverse Proxy
|
||||||
|
|
||||||
|
Recommended:
|
||||||
|
- nginx
|
||||||
|
- Nginx Proxy Manager
|
||||||
|
- HAProxy
|
||||||
|
|
||||||
|
Enable:
|
||||||
|
- HTTP/2
|
||||||
|
- TLS 1.3
|
||||||
|
- aggressive timeout policies
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const events = [];
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
events.push({ x: e.clientX, y: e.clientY, t: performance.now() });
|
||||||
|
if (events.length > 300) events.shift();
|
||||||
|
});
|
||||||
|
|
||||||
|
export function collectEntropy(since) {
|
||||||
|
return events.filter(e => e.t > since);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import init, { generate_keypair, sign_message, compute_next_hash, run_program } from './pkg/antibot_wasm.js';
|
||||||
|
import { collectEntropy } from './entropy.js';
|
||||||
|
import { sendRequest } from './transport.js';
|
||||||
|
|
||||||
|
let session, prevHash, currentSalt, opcodesB64, lastTime;
|
||||||
|
|
||||||
|
export async function initHeartbeat() {
|
||||||
|
await init();
|
||||||
|
const pubHex = generate_keypair();
|
||||||
|
const initResp = await sendRequest('/init', 'POST', { public_key: pubHex });
|
||||||
|
session = initResp.session_id;
|
||||||
|
prevHash = initResp.initial_hash;
|
||||||
|
currentSalt = initResp.salt;
|
||||||
|
opcodesB64 = initResp.opcodes_b64;
|
||||||
|
lastTime = performance.now();
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNext() {
|
||||||
|
const delay = 12000 + Math.random() * 13000;
|
||||||
|
setTimeout(sendHeartbeat, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendHeartbeat() {
|
||||||
|
try {
|
||||||
|
const now = performance.now();
|
||||||
|
const events = collectEntropy(lastTime);
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
const stackState = JSON.stringify(run_program(opcodesB64));
|
||||||
|
const fingerprint = {
|
||||||
|
aspectRatio: (screen.width / screen.height).toFixed(10),
|
||||||
|
devicePixelRatio: String(window.devicePixelRatio),
|
||||||
|
hardwareConcurrency: navigator.hardwareConcurrency || 1
|
||||||
|
};
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const entropyData = { events: events.map(e => ({ x: e.x, y: e.y, t: e.t })) };
|
||||||
|
const entropyJson = JSON.stringify(entropyData);
|
||||||
|
|
||||||
|
const signable = {
|
||||||
|
sessionId: session,
|
||||||
|
prevHash: prevHash,
|
||||||
|
timestamp: timestamp,
|
||||||
|
entropyData: entropyData,
|
||||||
|
stackState: JSON.parse(stackState),
|
||||||
|
fingerprint: fingerprint
|
||||||
|
};
|
||||||
|
const msg = JSON.stringify(signable, Object.keys(signable).sort());
|
||||||
|
const sig = sign_message(msg);
|
||||||
|
|
||||||
|
const resp = await sendRequest('/hb', 'POST', {
|
||||||
|
session_id: session,
|
||||||
|
prev_hash: prevHash,
|
||||||
|
timestamp,
|
||||||
|
entropy_data: entropyData,
|
||||||
|
stack_state: JSON.parse(stackState),
|
||||||
|
fingerprint,
|
||||||
|
signature: sig
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.next_salt) {
|
||||||
|
currentSalt = resp.next_salt;
|
||||||
|
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackState, currentSalt);
|
||||||
|
} else {
|
||||||
|
console.warn('Heartbeat rejected');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Anti-Scraper Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Protected Page</h1>
|
||||||
|
<p>Move your mouse to generate entropy.</p>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { initHeartbeat } from './heartbeat.js';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await initHeartbeat();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export async function sendRequest(url, method, body) {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
echo "Building WASM..."
|
||||||
|
cd ../wasm
|
||||||
|
wasm-pack build --target web
|
||||||
|
mv pkg ../frontend/pkg
|
||||||
|
echo "Building server..."
|
||||||
|
cd ../server
|
||||||
|
cargo build --release
|
||||||
|
echo "Done."
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Starting server with static frontend serving..."
|
||||||
|
cd ../server
|
||||||
|
cargo run --release
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
bash build.sh
|
||||||
|
echo "Release artifacts:"
|
||||||
|
echo " - server/target/release/antibot-server"
|
||||||
|
echo " - frontend/ (including pkg/)"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "antibot-server"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
shared = { path = "../shared" }
|
||||||
|
axum = "0.7"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
||||||
|
hex = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
|
rand = "0.8"
|
||||||
|
ed25519-dalek = "2"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use crate::session::AppState;
|
||||||
|
|
||||||
|
pub async fn cleanup_loop(state: Arc<AppState>) {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||||
|
let db = state.db.lock().await; // this is infallible
|
||||||
|
let now = crate::storage::current_time_ms();
|
||||||
|
let _ = db.execute(
|
||||||
|
"DELETE FROM sessions WHERE expires_at < ?1",
|
||||||
|
rusqlite::params![now],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use ed25519_dalek::{VerifyingKey, Signature};
|
||||||
|
use shared::protocol::HeartbeatRequest;
|
||||||
|
|
||||||
|
pub fn verify_signature(
|
||||||
|
pub_key_bytes: &[u8],
|
||||||
|
req: &HeartbeatRequest,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let pk = VerifyingKey::from_bytes(
|
||||||
|
&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?,
|
||||||
|
)?;
|
||||||
|
let sig_bytes = hex::decode(&req.signature)?;
|
||||||
|
let sig = Signature::from_slice(&sig_bytes)?;
|
||||||
|
|
||||||
|
// Build canonical JSON exactly as client signed (sorted keys, no extra spaces)
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"sessionId": req.session_id,
|
||||||
|
"prevHash": req.prev_hash,
|
||||||
|
"timestamp": req.timestamp,
|
||||||
|
"entropyData": req.entropy_data,
|
||||||
|
"stackState": req.stack_state,
|
||||||
|
"fingerprint": req.fingerprint,
|
||||||
|
});
|
||||||
|
let message = serde_json::to_string(&payload)?;
|
||||||
|
|
||||||
|
pk.verify_strict(message.as_bytes(), &sig)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use shared::protocol::Fingerprint;
|
||||||
|
|
||||||
|
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
|
||||||
|
if ar < 0.5 || ar > 3.0 { return Err("aspect ratio".into()); }
|
||||||
|
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
|
||||||
|
if dpr <= 0.0 || dpr > 5.0 { return Err("dpr".into()); }
|
||||||
|
if fp.hardware_concurrency == 0 { return Err("hw".into()); }
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
mod cleanup;
|
||||||
|
mod crypto;
|
||||||
|
mod fingerprint;
|
||||||
|
mod middleware;
|
||||||
|
mod ratelimit;
|
||||||
|
mod routes;
|
||||||
|
mod session;
|
||||||
|
mod storage;
|
||||||
|
mod trust;
|
||||||
|
mod vm;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use session::AppState;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let conn = storage::init_db().expect("DB init");
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
db: Mutex::new(conn),
|
||||||
|
rate_limiter: Mutex::new(ratelimit::RateLimiter::new(
|
||||||
|
shared::constants::RATE_LIMIT_COUNT,
|
||||||
|
shared::constants::RATE_LIMIT_WINDOW_SECS,
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Periodic cleanup
|
||||||
|
let bg_state = state.clone();
|
||||||
|
tokio::spawn(async move { cleanup::cleanup_loop(bg_state).await });
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/init", axum::routing::post(routes::init::handler))
|
||||||
|
.route("/hb", axum::routing::post(routes::heartbeat::handler))
|
||||||
|
.nest_service("/", tower_http::services::ServeDir::new("../frontend"))
|
||||||
|
.layer(tower_http::cors::CorsLayer::permissive())
|
||||||
|
.layer(axum::middleware::from_fn(middleware::log_request))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
|
info!("Server running on :3000");
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use axum::extract::Request;
|
||||||
|
use axum::middleware::Next;
|
||||||
|
use axum::response::Response;
|
||||||
|
|
||||||
|
pub async fn log_request(req: Request, next: Next) -> Response {
|
||||||
|
let method = req.method().clone();
|
||||||
|
let uri = req.uri().clone();
|
||||||
|
let response = next.run(req).await;
|
||||||
|
tracing::info!("{} {} -> {}", method, uri, response.status());
|
||||||
|
response
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub struct RateLimiter {
|
||||||
|
buckets: HashMap<String, (u32, Instant)>,
|
||||||
|
limit: u32,
|
||||||
|
window_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RateLimiter {
|
||||||
|
pub fn new(limit: u32, window_secs: u64) -> Self {
|
||||||
|
Self { buckets: HashMap::new(), limit, window_secs }
|
||||||
|
}
|
||||||
|
pub fn check(&mut self, key: &str) -> bool {
|
||||||
|
let now = Instant::now();
|
||||||
|
let entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
||||||
|
if now.duration_since(entry.1).as_secs() >= self.window_secs {
|
||||||
|
*entry = (1, now);
|
||||||
|
true
|
||||||
|
} else if entry.0 >= self.limit {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
entry.0 += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, Json};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use shared::protocol::{HeartbeatRequest, HeartbeatResponse};
|
||||||
|
use crate::session::AppState;
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<HeartbeatRequest>,
|
||||||
|
) -> (StatusCode, Json<HeartbeatResponse>) {
|
||||||
|
// Rate limiting
|
||||||
|
{
|
||||||
|
let mut rl = state.rate_limiter.lock().await;
|
||||||
|
if !rl.check(&payload.session_id) {
|
||||||
|
tracing::debug!("Rate limit hit: {}", payload.session_id);
|
||||||
|
return (StatusCode::OK, Json(HeartbeatResponse { status: "ok".into(), next_salt: None }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
match crate::session::verify_heartbeat(&db, &payload) {
|
||||||
|
Ok(next_salt) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(HeartbeatResponse { status: "ok".into(), next_salt: Some(next_salt) }),
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
|
||||||
|
(StatusCode::OK, Json(HeartbeatResponse { status: "ok".into(), next_salt: None }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, Json};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use shared::protocol::{InitRequest, InitResponse};
|
||||||
|
use crate::session::AppState;
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<InitRequest>,
|
||||||
|
) -> Result<Json<InitResponse>, (StatusCode, String)> {
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
crate::session::create_session(&db, &payload.public_key)
|
||||||
|
.map(Json)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Init error: {}", e);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal".into())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod init;
|
||||||
|
pub mod heartbeat;
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
pub struct AppState {
|
||||||
|
pub db: tokio::sync::Mutex<rusqlite::Connection>,
|
||||||
|
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
use rusqlite::params;
|
||||||
|
use shared::protocol::{HeartbeatRequest, InitResponse};
|
||||||
|
use crate::{crypto, trust, fingerprint, vm, storage};
|
||||||
|
|
||||||
|
pub fn create_session(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
pub_key_hex: &str,
|
||||||
|
) -> Result<InitResponse, Box<dyn std::error::Error>> {
|
||||||
|
let pub_key = hex::decode(pub_key_hex)?;
|
||||||
|
if pub_key.len() != shared::constants::SESSION_ID_LEN {
|
||||||
|
return Err("invalid pubkey len".into());
|
||||||
|
}
|
||||||
|
let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>());
|
||||||
|
let salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
|
||||||
|
let now = storage::current_time_ms();
|
||||||
|
let expires_at = now + (shared::constants::EXPIRATION_MINUTES as u64) * 60 * 1000;
|
||||||
|
|
||||||
|
let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt);
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO sessions (session_id, public_key, salt, last_hash, created_at, last_seen, expires_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
params![session_id, pub_key, salt.to_vec(), initial_hash, now, now, expires_at],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let opcodes = vm::generate_random_program(8..=16);
|
||||||
|
let opcodes_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &opcodes);
|
||||||
|
|
||||||
|
Ok(InitResponse {
|
||||||
|
session_id,
|
||||||
|
salt: hex::encode(salt),
|
||||||
|
opcodes_b64,
|
||||||
|
initial_hash: hex::encode(&initial_hash),
|
||||||
|
expires_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_heartbeat(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
req: &HeartbeatRequest,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT public_key, salt, last_hash, expires_at FROM sessions WHERE session_id = ?1",
|
||||||
|
)?;
|
||||||
|
let (pub_key, salt, stored_last_hash, expires_at): (Vec<u8>, Vec<u8>, Vec<u8>, u64) =
|
||||||
|
stmt.query_row(params![req.session_id], |row| {
|
||||||
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let now = storage::current_time_ms();
|
||||||
|
if now > expires_at {
|
||||||
|
return Err("expired".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verify signature
|
||||||
|
crypto::verify_signature(&pub_key, req)?;
|
||||||
|
|
||||||
|
// 2. Check chain continuity
|
||||||
|
if stored_last_hash != hex::decode(&req.prev_hash)? {
|
||||||
|
return Err("chain broken".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Time window
|
||||||
|
let diff = (now as i64) - (req.timestamp as i64);
|
||||||
|
if diff.abs() > shared::constants::MAX_TIMESTAMP_DRIFT_MS {
|
||||||
|
return Err("timestamp drift".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Trusted mouse & fingerprint
|
||||||
|
trust::validate_mouse(&req.entropy_data)?;
|
||||||
|
fingerprint::validate(&req.fingerprint)?;
|
||||||
|
|
||||||
|
// 5. Compute new hash
|
||||||
|
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
|
||||||
|
let new_hash = shared::hashing::next_chain_hash(
|
||||||
|
&prev_hash_bytes,
|
||||||
|
req.timestamp,
|
||||||
|
&req.entropy_data,
|
||||||
|
&req.stack_state,
|
||||||
|
&salt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. New salt for client
|
||||||
|
let next_salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
|
||||||
|
let next_salt_hex = hex::encode(next_salt);
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE sessions SET last_hash=?1, salt=?2, chain_length=chain_length+1, last_seen=?3 WHERE session_id=?4",
|
||||||
|
params![new_hash, next_salt.to_vec(), now, req.session_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(next_salt_hex)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use rusqlite::Connection;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub fn init_db() -> Result<Connection, rusqlite::Error> {
|
||||||
|
let conn = Connection::open_in_memory()?;
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
session_id TEXT PRIMARY KEY,
|
||||||
|
public_key BLOB NOT NULL,
|
||||||
|
salt BLOB NOT NULL,
|
||||||
|
last_hash BLOB NOT NULL,
|
||||||
|
chain_length INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
last_seen INTEGER NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_time_ms() -> u64 {
|
||||||
|
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
use shared::protocol::EntropyData;
|
||||||
|
|
||||||
|
pub fn validate_mouse(data: &EntropyData) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let events = &data.events;
|
||||||
|
if events.len() < 3 {
|
||||||
|
return Err("few events".into());
|
||||||
|
}
|
||||||
|
let mut total_dist = 0.0;
|
||||||
|
let mut pauses = 0u32;
|
||||||
|
for i in 1..events.len() {
|
||||||
|
let p = &events[i-1];
|
||||||
|
let c = &events[i];
|
||||||
|
let dx = c.x - p.x;
|
||||||
|
let dy = c.y - p.y;
|
||||||
|
let dt = (c.timestamp_ms - p.timestamp_ms).max(1.0);
|
||||||
|
let dist = (dx*dx + dy*dy).sqrt();
|
||||||
|
total_dist += dist;
|
||||||
|
if dist < 0.2 && dt > 50.0 { pauses += 1; }
|
||||||
|
}
|
||||||
|
if total_dist < shared::constants::MIN_MOUSE_TOTAL_DIST {
|
||||||
|
return Err("insufficient distance".into());
|
||||||
|
}
|
||||||
|
let avg_speed = total_dist / events.len() as f64;
|
||||||
|
if avg_speed > shared::constants::MAX_MOUSE_AVG_SPEED {
|
||||||
|
return Err("speed too high".into());
|
||||||
|
}
|
||||||
|
if pauses < shared::constants::MIN_PAUSE_COUNT {
|
||||||
|
return Err("no pause".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Vec<u8> {
|
||||||
|
// Same logic as earlier, using shared::hashing for HASH if needed
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let count = rng.gen_range(len_range);
|
||||||
|
let mut ops = Vec::new();
|
||||||
|
let mut depth: i32 = 0;
|
||||||
|
for _ in 0..count {
|
||||||
|
if depth < 2 {
|
||||||
|
ops.push(0x00); // PUSH
|
||||||
|
let val = rng.gen::<u32>();
|
||||||
|
ops.extend_from_slice(&val.to_le_bytes());
|
||||||
|
depth += 1;
|
||||||
|
} else {
|
||||||
|
let op = rng.gen_range(0..10);
|
||||||
|
match op {
|
||||||
|
0x00 => {
|
||||||
|
ops.push(0x00);
|
||||||
|
let val = rng.gen::<u32>();
|
||||||
|
ops.extend_from_slice(&val.to_le_bytes());
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
0x01..=0x08 => { ops.push(op as u8); depth -= 1; }
|
||||||
|
0x09 => { ops.push(0x09); depth = 1; }
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server does not need to execute the program; client does.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "shared"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1" # <- add this line
|
||||||
|
blake3 = "1"
|
||||||
|
hex = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
|
rand = "0.8"
|
||||||
|
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub const SESSION_ID_LEN: usize = 32;
|
||||||
|
pub const SALT_LEN: usize = 16;
|
||||||
|
pub const HEARTBEAT_MIN_INTERVAL_MS: u64 = 12_000;
|
||||||
|
pub const HEARTBEAT_MAX_INTERVAL_MS: u64 = 25_000;
|
||||||
|
pub const EXPIRATION_MINUTES: i64 = 30;
|
||||||
|
pub const RATE_LIMIT_COUNT: u32 = 5;
|
||||||
|
pub const RATE_LIMIT_WINDOW_SECS: u64 = 10;
|
||||||
|
pub const MAX_TIMESTAMP_DRIFT_MS: i64 = 30_000;
|
||||||
|
pub const MIN_MOUSE_TOTAL_DIST: f64 = 10.0;
|
||||||
|
pub const MAX_MOUSE_AVG_SPEED: f64 = 2.0; // px/ms
|
||||||
|
pub const MIN_PAUSE_COUNT: u32 = 1;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use blake3::Hasher;
|
||||||
|
use crate::protocol::{EntropyData, StackState};
|
||||||
|
|
||||||
|
/// Initial hash for a brand-new session: Blake3(session_id || pub_key || salt)
|
||||||
|
pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||||
|
let mut h = Hasher::new();
|
||||||
|
h.update(session_id.as_bytes());
|
||||||
|
h.update(pub_key);
|
||||||
|
h.update(salt);
|
||||||
|
h.finalize().as_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next hash in the chain: Blake3 with the salt mixed in (no keyed mode needed)
|
||||||
|
pub fn next_chain_hash(
|
||||||
|
prev_hash: &[u8],
|
||||||
|
timestamp: u64,
|
||||||
|
entropy: &EntropyData,
|
||||||
|
stack: &StackState,
|
||||||
|
salt: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let entropy_json = serde_json::to_string(entropy).unwrap();
|
||||||
|
let stack_json = serde_json::to_string(stack).unwrap();
|
||||||
|
|
||||||
|
let entropy_hash = blake3::hash(entropy_json.as_bytes());
|
||||||
|
let stack_hash = blake3::hash(stack_json.as_bytes());
|
||||||
|
|
||||||
|
let mut h = Hasher::new();
|
||||||
|
// Mix the salt into the hash state
|
||||||
|
h.update(salt);
|
||||||
|
h.update(prev_hash);
|
||||||
|
h.update(×tamp.to_le_bytes());
|
||||||
|
h.update(entropy_hash.as_bytes());
|
||||||
|
h.update(stack_hash.as_bytes());
|
||||||
|
h.finalize().as_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash of all stack items for VM HASH opcode
|
||||||
|
pub fn hash_stack(stack: &[u32]) -> u32 {
|
||||||
|
let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||||||
|
let hash = blake3::hash(&data);
|
||||||
|
u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod constants;
|
||||||
|
pub mod hashing;
|
||||||
|
pub mod protocol;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct InitRequest {
|
||||||
|
pub public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct InitResponse {
|
||||||
|
pub session_id: String,
|
||||||
|
pub salt: String,
|
||||||
|
pub opcodes_b64: String,
|
||||||
|
pub initial_hash: String,
|
||||||
|
pub expires_at: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct HeartbeatRequest {
|
||||||
|
pub session_id: String,
|
||||||
|
pub prev_hash: String,
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub entropy_data: EntropyData,
|
||||||
|
pub stack_state: StackState,
|
||||||
|
pub fingerprint: Fingerprint,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct HeartbeatResponse {
|
||||||
|
pub status: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_salt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct Fingerprint {
|
||||||
|
#[serde(rename = "aspectRatio")]
|
||||||
|
pub aspect_ratio: String,
|
||||||
|
#[serde(rename = "devicePixelRatio")]
|
||||||
|
pub device_pixel_ratio: String,
|
||||||
|
#[serde(rename = "hardwareConcurrency")]
|
||||||
|
pub hardware_concurrency: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct EntropyData {
|
||||||
|
pub events: Vec<MouseEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
pub struct MouseEvent {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
#[serde(rename = "t")]
|
||||||
|
pub timestamp_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StackState {
|
||||||
|
pub stack: Vec<u32>,
|
||||||
|
pub ip: u16,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "antibot-wasm"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
shared = { path = "../shared" }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||||
|
rand = "0.8" # <-- add this
|
||||||
|
blake3 = "1"
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
hex = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
|
serde-wasm-bindgen = "0.6"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Example: break debugger detection, console clearing, etc.
|
||||||
|
// Currently empty.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use ed25519_dalek::{SigningKey, Signer};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static KEYPAIR: RefCell<Option<SigningKey>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn generate_keypair() -> String {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let sk = SigningKey::generate(&mut rng);
|
||||||
|
let pk = sk.verifying_key();
|
||||||
|
let hex_pub = hex::encode(pk.as_bytes());
|
||||||
|
KEYPAIR.with(|kp| *kp.borrow_mut() = Some(sk));
|
||||||
|
hex_pub
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_public_key() -> String {
|
||||||
|
KEYPAIR.with(|kp| hex::encode(kp.borrow().as_ref().unwrap().verifying_key().as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn sign_message(message_json: &str) -> String {
|
||||||
|
KEYPAIR.with(|kp| {
|
||||||
|
let sk = kp.borrow();
|
||||||
|
let sig = sk.as_ref().unwrap().sign(message_json.as_bytes());
|
||||||
|
hex::encode(sig.to_bytes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn compute_next_hash(
|
||||||
|
prev_hash_hex: &str,
|
||||||
|
timestamp: u64,
|
||||||
|
entropy_data_json: &str,
|
||||||
|
stack_state_json: &str,
|
||||||
|
salt_hex: &str,
|
||||||
|
) -> String {
|
||||||
|
let prev = hex::decode(prev_hash_hex).unwrap();
|
||||||
|
let salt = hex::decode(salt_hex).unwrap();
|
||||||
|
let entropy = serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json).unwrap();
|
||||||
|
let stack = serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap();
|
||||||
|
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
|
||||||
|
hex::encode(&new)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// This module is handled on the JS side; WASM only receives the prepared entropy data.
|
||||||
|
// Could be used to add extra entropy sources (e.g., from JS via import).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// Fingerprint collection is done in JS, this module is a placeholder.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod anti_debug;
|
||||||
|
pub mod crypto;
|
||||||
|
pub mod entropy;
|
||||||
|
pub mod fingerprint;
|
||||||
|
pub mod transport;
|
||||||
|
pub mod vm;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// Could contain WebTransport related code if needed later.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use shared::protocol::StackState;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_program(program_b64: &str) -> JsValue {
|
||||||
|
use base64::Engine;
|
||||||
|
let bytes = base64::engine::general_purpose::STANDARD.decode(program_b64).unwrap();
|
||||||
|
let state = execute(&bytes);
|
||||||
|
serde_wasm_bindgen::to_value(&state).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(program: &[u8]) -> StackState {
|
||||||
|
let mut stack: Vec<u32> = Vec::new();
|
||||||
|
let mut ip: usize = 0;
|
||||||
|
while ip < program.len() {
|
||||||
|
let op = program[ip];
|
||||||
|
ip += 1;
|
||||||
|
match op {
|
||||||
|
0x00 => {
|
||||||
|
if ip + 4 > program.len() { break; }
|
||||||
|
let val = u32::from_le_bytes([program[ip], program[ip+1], program[ip+2], program[ip+3]]);
|
||||||
|
ip += 4;
|
||||||
|
stack.push(val);
|
||||||
|
}
|
||||||
|
0x01..=0x07 => {
|
||||||
|
if stack.len() < 2 { break; }
|
||||||
|
let b = stack.pop().unwrap();
|
||||||
|
let a = stack.pop().unwrap();
|
||||||
|
let r = match op {
|
||||||
|
0x01 => a.wrapping_add(b),
|
||||||
|
0x02 => a.wrapping_sub(b),
|
||||||
|
0x03 => a.wrapping_mul(b),
|
||||||
|
0x04 => a ^ b,
|
||||||
|
0x05 => a & b,
|
||||||
|
0x06 => a | b,
|
||||||
|
0x07 => a.rotate_left(b % 32),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
stack.push(r);
|
||||||
|
}
|
||||||
|
0x08 => {
|
||||||
|
if stack.is_empty() { break; }
|
||||||
|
let a = stack.pop().unwrap();
|
||||||
|
stack.push(!a);
|
||||||
|
}
|
||||||
|
0x09 => {
|
||||||
|
let r = shared::hashing::hash_stack(&stack);
|
||||||
|
stack.clear();
|
||||||
|
stack.push(r);
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StackState { stack, ip: ip as u16 }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user