Initial ChronoSeal release

This commit is contained in:
thakares committed 2026-05-07 21:57:47 +05:30
commit a66debdece
48 files changed
+2362

No files matched your search

+21
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
target/
pkg/
node_modules/
dist/
*.log
.env
.idea/
+22
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
[workspace]
resolver = "2"
members = [
"shared",
"server",
"wasm"
]
+23
View File
@@ -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"]
+5
View File
@@ -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
+49
View File
@@ -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
View File
@@ -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.
+35
View File
@@ -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
+16
View File
@@ -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
+36
View File
@@ -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.
+36
View File
@@ -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
+10
View File
@@ -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);
}
+72
View File
@@ -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();
}
}
+12
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
import { initHeartbeat } from './heartbeat.js';
(async () => {
await initHeartbeat();
})();
+9
View File
@@ -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();
}
+10
View File
@@ -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."
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
echo "Starting server with static frontend serving..."
cd ../server
cargo run --release
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
bash build.sh
echo "Release artifacts:"
echo " - server/target/release/antibot-server"
echo " - frontend/ (including pkg/)"
+20
View File
@@ -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"
+14
View File
@@ -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],
);
}
}
+27
View File
@@ -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(())
}
+10
View File
@@ -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(())
}
+47
View File
@@ -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();
}
+11
View File
@@ -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
}
+27
View File
@@ -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
}
}
}
+30
View File
@@ -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 }))
}
}
}
+17
View File
@@ -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())
})
}
+2
View File
@@ -0,0 +1,2 @@
pub mod init;
pub mod heartbeat;
+98
View File
@@ -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)
}
+23
View File
@@ -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
}
+31
View File
@@ -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(())
}
+33
View File
@@ -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.
+13
View File
@@ -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"] }
+11
View File
@@ -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;
+42
View File
@@ -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(&timestamp.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())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod constants;
pub mod hashing;
pub mod protocol;
+62
View File
@@ -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,
}
+20
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
// Example: break debugger detection, console clearing, etc.
// Currently empty.
+47
View File
@@ -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)
}
+2
View File
@@ -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).
+1
View File
@@ -0,0 +1 @@
// Fingerprint collection is done in JS, this module is a placeholder.
+6
View File
@@ -0,0 +1,6 @@
pub mod anti_debug;
pub mod crypto;
pub mod entropy;
pub mod fingerprint;
pub mod transport;
pub mod vm;
+1
View File
@@ -0,0 +1 @@
// Could contain WebTransport related code if needed later.
+55
View File
@@ -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 }
}