commit a66debdece33f9dc2a7c3d163a5399caf34dcb90 Author: Sunil Thakares Date: Thu May 7 21:56:58 2026 +0530 Initial ChronoSeal release diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..0d32a25 --- /dev/null +++ b/.github/workflows/rust.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b00867a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ +pkg/ +node_modules/ +dist/ +*.log +.env +.idea/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a9b6b97 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..39e3e6f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1302 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "antibot-server" +version = "0.2.0" +dependencies = [ + "axum", + "base64", + "ed25519-dalek", + "hex", + "rand", + "rusqlite", + "serde", + "serde_json", + "shared", + "tokio", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "antibot-wasm" +version = "0.2.0" +dependencies = [ + "base64", + "blake3", + "ed25519-dalek", + "getrandom", + "hex", + "rand", + "serde", + "serde-wasm-bindgen", + "serde_json", + "shared", + "wasm-bindgen", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared" +version = "0.2.0" +dependencies = [ + "base64", + "blake3", + "ed25519-dalek", + "hex", + "rand", + "serde", + "serde_json", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..edbc756 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +resolver = "2" + +members = [ + "shared", + "server", + "wasm" +] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5d58123 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b0069d8 --- /dev/null +++ b/LICENSE.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f3f2a2 --- /dev/null +++ b/README.md @@ -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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6e3b672 --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/chronoseal.service b/chronoseal.service new file mode 100644 index 0000000..ad3f850 --- /dev/null +++ b/chronoseal.service @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8ce389a --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..cebfed0 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..2a7b365 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -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 diff --git a/frontend/entropy.js b/frontend/entropy.js new file mode 100644 index 0000000..5a97959 --- /dev/null +++ b/frontend/entropy.js @@ -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); +} \ No newline at end of file diff --git a/frontend/heartbeat.js b/frontend/heartbeat.js new file mode 100644 index 0000000..c591ee2 --- /dev/null +++ b/frontend/heartbeat.js @@ -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(); + } +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b3dc31e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + Anti-Scraper Demo + + +

Protected Page

+

Move your mouse to generate entropy.

+ + + \ No newline at end of file diff --git a/frontend/main.js b/frontend/main.js new file mode 100644 index 0000000..e44681f --- /dev/null +++ b/frontend/main.js @@ -0,0 +1,5 @@ +import { initHeartbeat } from './heartbeat.js'; + +(async () => { + await initHeartbeat(); +})(); \ No newline at end of file diff --git a/frontend/transport.js b/frontend/transport.js new file mode 100644 index 0000000..0423ed0 --- /dev/null +++ b/frontend/transport.js @@ -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(); +} \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..9883cbd --- /dev/null +++ b/scripts/build.sh @@ -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." \ No newline at end of file diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100644 index 0000000..9a6840d --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Starting server with static frontend serving..." +cd ../server +cargo run --release \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100644 index 0000000..f923c19 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,5 @@ +#!/bin/bash +bash build.sh +echo "Release artifacts:" +echo " - server/target/release/antibot-server" +echo " - frontend/ (including pkg/)" \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..0e09a0b --- /dev/null +++ b/server/Cargo.toml @@ -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" \ No newline at end of file diff --git a/server/src/cleanup.rs b/server/src/cleanup.rs new file mode 100644 index 0000000..da8943e --- /dev/null +++ b/server/src/cleanup.rs @@ -0,0 +1,14 @@ +use std::sync::Arc; +use crate::session::AppState; + +pub async fn cleanup_loop(state: Arc) { + 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], + ); + } +} \ No newline at end of file diff --git a/server/src/crypto.rs b/server/src/crypto.rs new file mode 100644 index 0000000..3c6e4f9 --- /dev/null +++ b/server/src/crypto.rs @@ -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> { + 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(()) +} \ No newline at end of file diff --git a/server/src/fingerprint.rs b/server/src/fingerprint.rs new file mode 100644 index 0000000..19f27b8 --- /dev/null +++ b/server/src/fingerprint.rs @@ -0,0 +1,10 @@ +use shared::protocol::Fingerprint; + +pub fn validate(fp: &Fingerprint) -> Result<(), Box> { + 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(()) +} \ No newline at end of file diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..c1d395f --- /dev/null +++ b/server/src/main.rs @@ -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(); +} \ No newline at end of file diff --git a/server/src/middleware.rs b/server/src/middleware.rs new file mode 100644 index 0000000..a39396b --- /dev/null +++ b/server/src/middleware.rs @@ -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 +} \ No newline at end of file diff --git a/server/src/ratelimit.rs b/server/src/ratelimit.rs new file mode 100644 index 0000000..852ac6c --- /dev/null +++ b/server/src/ratelimit.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; +use std::time::Instant; + +pub struct RateLimiter { + buckets: HashMap, + 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 + } + } +} \ No newline at end of file diff --git a/server/src/routes/heartbeat.rs b/server/src/routes/heartbeat.rs new file mode 100644 index 0000000..731a37e --- /dev/null +++ b/server/src/routes/heartbeat.rs @@ -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>, + Json(payload): Json, +) -> (StatusCode, Json) { + // 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 })) + } + } +} \ No newline at end of file diff --git a/server/src/routes/init.rs b/server/src/routes/init.rs new file mode 100644 index 0000000..f931968 --- /dev/null +++ b/server/src/routes/init.rs @@ -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>, + Json(payload): Json, +) -> Result, (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()) + }) +} \ No newline at end of file diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs new file mode 100644 index 0000000..f2317ce --- /dev/null +++ b/server/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod init; +pub mod heartbeat; diff --git a/server/src/session.rs b/server/src/session.rs new file mode 100644 index 0000000..de92101 --- /dev/null +++ b/server/src/session.rs @@ -0,0 +1,98 @@ +pub struct AppState { + pub db: tokio::sync::Mutex, + pub rate_limiter: tokio::sync::Mutex, +} + +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> { + 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> { + 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, Vec, Vec, 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) +} \ No newline at end of file diff --git a/server/src/storage.rs b/server/src/storage.rs new file mode 100644 index 0000000..fe7ffc7 --- /dev/null +++ b/server/src/storage.rs @@ -0,0 +1,23 @@ +use rusqlite::Connection; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn init_db() -> Result { + 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 +} \ No newline at end of file diff --git a/server/src/trust.rs b/server/src/trust.rs new file mode 100644 index 0000000..d85c665 --- /dev/null +++ b/server/src/trust.rs @@ -0,0 +1,31 @@ +use shared::protocol::EntropyData; + +pub fn validate_mouse(data: &EntropyData) -> Result<(), Box> { + 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(()) +} \ No newline at end of file diff --git a/server/src/vm.rs b/server/src/vm.rs new file mode 100644 index 0000000..451126d --- /dev/null +++ b/server/src/vm.rs @@ -0,0 +1,33 @@ +use rand::Rng; + +pub fn generate_random_program(len_range: std::ops::RangeInclusive) -> Vec { + // 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::(); + 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::(); + 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. \ No newline at end of file diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..390f21c --- /dev/null +++ b/shared/Cargo.toml @@ -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"] } \ No newline at end of file diff --git a/shared/src/constants.rs b/shared/src/constants.rs new file mode 100644 index 0000000..9a14d3a --- /dev/null +++ b/shared/src/constants.rs @@ -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; \ No newline at end of file diff --git a/shared/src/hashing.rs b/shared/src/hashing.rs new file mode 100644 index 0000000..7c4ae30 --- /dev/null +++ b/shared/src/hashing.rs @@ -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 { + 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 { + 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 = 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()) +} \ No newline at end of file diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 0000000..7e45bb6 --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,3 @@ +pub mod constants; +pub mod hashing; +pub mod protocol; \ No newline at end of file diff --git a/shared/src/protocol.rs b/shared/src/protocol.rs new file mode 100644 index 0000000..44a62fc --- /dev/null +++ b/shared/src/protocol.rs @@ -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, +} + +#[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, +} + +#[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, + pub ip: u16, +} \ No newline at end of file diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..c464c5b --- /dev/null +++ b/wasm/Cargo.toml @@ -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" \ No newline at end of file diff --git a/wasm/src/anti_debug.rs b/wasm/src/anti_debug.rs new file mode 100644 index 0000000..edc1522 --- /dev/null +++ b/wasm/src/anti_debug.rs @@ -0,0 +1,2 @@ +// Example: break debugger detection, console clearing, etc. +// Currently empty. \ No newline at end of file diff --git a/wasm/src/crypto.rs b/wasm/src/crypto.rs new file mode 100644 index 0000000..e759817 --- /dev/null +++ b/wasm/src/crypto.rs @@ -0,0 +1,47 @@ +use ed25519_dalek::{SigningKey, Signer}; +use std::cell::RefCell; +use wasm_bindgen::prelude::*; + +thread_local! { + static KEYPAIR: RefCell> = 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::(entropy_data_json).unwrap(); + let stack = serde_json::from_str::(stack_state_json).unwrap(); + let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt); + hex::encode(&new) +} \ No newline at end of file diff --git a/wasm/src/entropy.rs b/wasm/src/entropy.rs new file mode 100644 index 0000000..67915a1 --- /dev/null +++ b/wasm/src/entropy.rs @@ -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). \ No newline at end of file diff --git a/wasm/src/fingerprint.rs b/wasm/src/fingerprint.rs new file mode 100644 index 0000000..f3274de --- /dev/null +++ b/wasm/src/fingerprint.rs @@ -0,0 +1 @@ +// Fingerprint collection is done in JS, this module is a placeholder. \ No newline at end of file diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..f6b57e6 --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,6 @@ +pub mod anti_debug; +pub mod crypto; +pub mod entropy; +pub mod fingerprint; +pub mod transport; +pub mod vm; \ No newline at end of file diff --git a/wasm/src/transport.rs b/wasm/src/transport.rs new file mode 100644 index 0000000..6b16e53 --- /dev/null +++ b/wasm/src/transport.rs @@ -0,0 +1 @@ +// Could contain WebTransport related code if needed later. \ No newline at end of file diff --git a/wasm/src/vm.rs b/wasm/src/vm.rs new file mode 100644 index 0000000..f07b695 --- /dev/null +++ b/wasm/src/vm.rs @@ -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 = 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 } +} \ No newline at end of file