Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e78daeeba |
No files matched your search
Generated
+303
-12
@@ -73,6 +73,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -226,9 +232,20 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.0",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chronoseal-server"
|
name = "chronoseal-server"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -236,12 +253,15 @@ dependencies = [
|
|||||||
"clap_complete",
|
"clap_complete",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
"rand",
|
"r2d2",
|
||||||
|
"r2d2_sqlite",
|
||||||
|
"rand 0.8.6",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"shared",
|
"shared",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
@@ -253,14 +273,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chronoseal-wasm"
|
name = "chronoseal-wasm"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"blake3",
|
"blake3",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
"hex",
|
"hex",
|
||||||
"rand",
|
"rand 0.8.6",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -453,7 +473,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -500,6 +520,12 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -571,6 +597,20 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -580,6 +620,15 @@ dependencies = [
|
|||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -693,6 +742,12 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@@ -701,6 +756,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -733,6 +790,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
@@ -912,6 +975,16 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -930,6 +1003,34 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r2d2"
|
||||||
|
version = "0.8.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"parking_lot",
|
||||||
|
"scheduled-thread-pool",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r2d2_sqlite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a982edf65c129796dba72f8775b292ef482b40d035e827a9825b3bc07ccc5f2"
|
||||||
|
dependencies = [
|
||||||
|
"r2d2",
|
||||||
|
"rusqlite",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -938,7 +1039,18 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -948,7 +1060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -957,9 +1069,15 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -1034,6 +1152,15 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scheduled-thread-pool"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
||||||
|
dependencies = [
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1167,13 +1294,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"blake3",
|
"blake3",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
"rand",
|
"rand 0.8.6",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -1200,7 +1327,7 @@ version = "2.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1592,6 +1719,12 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
@@ -1604,6 +1737,18 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"js-sys",
|
||||||
|
"rand 0.10.1",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1628,6 +1773,24 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.3+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.57.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.51.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.121"
|
version = "0.2.121"
|
||||||
@@ -1673,6 +1836,40 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1697,6 +1894,100 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.48"
|
version = "0.8.48"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import init, { generate_keypair, sign_message, compute_next_hash, run_program } from './pkg/antibot_wasm.js';
|
import init, { generate_keypair, sign_message, compute_next_hash, run_program } from './pkg/chronoseal_wasm.js';
|
||||||
import { collectEntropy } from './entropy.js';
|
import { collectEntropy } from './entropy.js';
|
||||||
import { sendRequest } from './transport.js';
|
import { sendRequest } from './transport.js';
|
||||||
|
|
||||||
let session, prevHash, currentSalt, opcodesB64, lastTime;
|
let session, prevHash, currentSalt, opcodesB64, lastTime;
|
||||||
|
let minInterval = 12000;
|
||||||
|
let maxInterval = 25000;
|
||||||
|
|
||||||
export async function initHeartbeat() {
|
export async function initHeartbeat() {
|
||||||
await init();
|
await init();
|
||||||
@@ -12,12 +14,14 @@ export async function initHeartbeat() {
|
|||||||
prevHash = initResp.initial_hash;
|
prevHash = initResp.initial_hash;
|
||||||
currentSalt = initResp.salt;
|
currentSalt = initResp.salt;
|
||||||
opcodesB64 = initResp.opcodes_b64;
|
opcodesB64 = initResp.opcodes_b64;
|
||||||
|
minInterval = initResp.heartbeat_min_interval_ms || 12000;
|
||||||
|
maxInterval = initResp.heartbeat_max_interval_ms || 25000;
|
||||||
lastTime = performance.now();
|
lastTime = performance.now();
|
||||||
scheduleNext();
|
scheduleNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNext() {
|
function scheduleNext() {
|
||||||
const delay = 12000 + Math.random() * 13000;
|
const delay = minInterval + Math.random() * (maxInterval - minInterval);
|
||||||
setTimeout(sendHeartbeat, delay);
|
setTimeout(sendHeartbeat, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chronoseal-server"
|
name = "chronoseal-server"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@@ -18,6 +18,9 @@ serde_json = "1"
|
|||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
r2d2 = "0.8"
|
||||||
|
r2d2_sqlite = "0.24"
|
||||||
|
thiserror = "2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
|
|||||||
+19
-9
@@ -1,5 +1,5 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use crate::session::AppState;
|
use crate::session::AppState;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn cleanup_loop(state: Arc<AppState>) {
|
pub async fn cleanup_loop(state: Arc<AppState>) {
|
||||||
loop {
|
loop {
|
||||||
@@ -7,18 +7,28 @@ pub async fn cleanup_loop(state: Arc<AppState>) {
|
|||||||
|
|
||||||
// Evict expired sessions from SQLite.
|
// Evict expired sessions from SQLite.
|
||||||
{
|
{
|
||||||
let db = state.db.lock().await;
|
if let Ok(conn) = state.db_pool.get() {
|
||||||
let now = crate::storage::current_time_ms();
|
let now = crate::storage::current_time_ms();
|
||||||
let _ = db.execute(
|
let _ = conn.execute(
|
||||||
"DELETE FROM sessions WHERE expires_at < ?1",
|
"DELETE FROM sessions WHERE expires_at < ?1",
|
||||||
rusqlite::params![now],
|
rusqlite::params![now],
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to get database connection from pool for cleanup");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict stale rate-limiter entries to prevent unbounded HashMap growth.
|
// Evict stale rate-limiter entries to prevent unbounded HashMap growth.
|
||||||
{
|
{
|
||||||
|
let window_secs = {
|
||||||
|
if let Ok(config) = state.config.read() {
|
||||||
|
config.rate_limit_window_secs
|
||||||
|
} else {
|
||||||
|
10 // fallback default
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut rl = state.rate_limiter.lock().await;
|
let mut rl = state.rate_limiter.lock().await;
|
||||||
rl.evict_stale();
|
rl.evict_stale(window_secs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-7
@@ -52,15 +52,21 @@ impl GlobalArgs {
|
|||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Run the ChronoSeal daemon.
|
/// Run the ChronoSeal daemon.
|
||||||
#[command(after_help = "Examples:\n chronoseal run\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal run\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run"
|
||||||
|
)]
|
||||||
Run(RunArgs),
|
Run(RunArgs),
|
||||||
|
|
||||||
/// Report whether the configured daemon is reachable and which PID file is present.
|
/// Report whether the configured daemon is reachable and which PID file is present.
|
||||||
#[command(after_help = "Examples:\n chronoseal status\n chronoseal status --format json\n chronoseal status --pid-file /run/chronoseal.pid")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal status\n chronoseal status --format json\n chronoseal status --pid-file /run/chronoseal.pid"
|
||||||
|
)]
|
||||||
Status(RuntimeArgs),
|
Status(RuntimeArgs),
|
||||||
|
|
||||||
/// Perform a daemon health probe.
|
/// Perform a daemon health probe.
|
||||||
#[command(after_help = "Examples:\n chronoseal health\n chronoseal health --format json\n chronoseal health --bind 127.0.0.1:3000")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal health\n chronoseal health --format json\n chronoseal health --bind 127.0.0.1:3000"
|
||||||
|
)]
|
||||||
Health(RuntimeArgs),
|
Health(RuntimeArgs),
|
||||||
|
|
||||||
/// Validate and print effective configuration.
|
/// Validate and print effective configuration.
|
||||||
@@ -76,7 +82,9 @@ pub enum Command {
|
|||||||
Version,
|
Version,
|
||||||
|
|
||||||
/// Print Prometheus metrics from the running daemon.
|
/// Print Prometheus metrics from the running daemon.
|
||||||
#[command(after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000"
|
||||||
|
)]
|
||||||
Metrics(RuntimeArgs),
|
Metrics(RuntimeArgs),
|
||||||
|
|
||||||
/// Print service statistics from the running daemon.
|
/// Print service statistics from the running daemon.
|
||||||
@@ -84,7 +92,9 @@ pub enum Command {
|
|||||||
Stats(RuntimeArgs),
|
Stats(RuntimeArgs),
|
||||||
|
|
||||||
/// Generate shell completions.
|
/// Generate shell completions.
|
||||||
#[command(after_help = "Examples:\n chronoseal completion bash\n chronoseal completion zsh > ~/.zfunc/_chronoseal")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal completion bash\n chronoseal completion zsh > ~/.zfunc/_chronoseal"
|
||||||
|
)]
|
||||||
Completion { shell: clap_complete::Shell },
|
Completion { shell: clap_complete::Shell },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,13 +130,17 @@ pub struct RuntimeArgs {
|
|||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum ConfigCommand {
|
pub enum ConfigCommand {
|
||||||
/// Validate configuration and print the effective values.
|
/// Validate configuration and print the effective values.
|
||||||
#[command(after_help = "Examples:\n chronoseal config check\n chronoseal config check --config /etc/chronoseal/config.toml\n chronoseal config check --output json")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal config check\n chronoseal config check --config /etc/chronoseal/config.toml\n chronoseal config check --output json"
|
||||||
|
)]
|
||||||
Check(RuntimeArgs),
|
Check(RuntimeArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum GenerateCommand {
|
pub enum GenerateCommand {
|
||||||
/// Generate an Ed25519 keypair as hex-encoded JSON/YAML/text.
|
/// Generate an Ed25519 keypair as hex-encoded JSON/YAML/text.
|
||||||
#[command(after_help = "Examples:\n chronoseal generate keypair\n chronoseal generate keypair --format json")]
|
#[command(
|
||||||
|
after_help = "Examples:\n chronoseal generate keypair\n chronoseal generate keypair --format json"
|
||||||
|
)]
|
||||||
Keypair,
|
Keypair,
|
||||||
}
|
}
|
||||||
+83
-2
@@ -14,6 +14,16 @@ pub struct Config {
|
|||||||
pub db_path: PathBuf,
|
pub db_path: PathBuf,
|
||||||
pub frontend_dir: PathBuf,
|
pub frontend_dir: PathBuf,
|
||||||
pub log_file: Option<PathBuf>,
|
pub log_file: Option<PathBuf>,
|
||||||
|
pub heartbeat_min_interval_ms: u64,
|
||||||
|
pub heartbeat_max_interval_ms: u64,
|
||||||
|
pub expiration_minutes: i64,
|
||||||
|
pub rate_limit_count: u32,
|
||||||
|
pub rate_limit_window_secs: u64,
|
||||||
|
pub max_timestamp_drift_ms: i64,
|
||||||
|
pub min_mouse_total_dist: f64,
|
||||||
|
pub max_mouse_avg_speed: f64,
|
||||||
|
pub min_pause_count: u32,
|
||||||
|
pub require_mouse_activity: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -24,6 +34,16 @@ impl Default for Config {
|
|||||||
db_path: default_state_dir().join("chronoseal.sqlite"),
|
db_path: default_state_dir().join("chronoseal.sqlite"),
|
||||||
frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"),
|
frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"),
|
||||||
log_file: None,
|
log_file: None,
|
||||||
|
heartbeat_min_interval_ms: 12_000,
|
||||||
|
heartbeat_max_interval_ms: 25_000,
|
||||||
|
expiration_minutes: 30,
|
||||||
|
rate_limit_count: 5,
|
||||||
|
rate_limit_window_secs: 10,
|
||||||
|
max_timestamp_drift_ms: 30_000,
|
||||||
|
min_mouse_total_dist: 10.0,
|
||||||
|
max_mouse_avg_speed: 2.0,
|
||||||
|
min_pause_count: 1,
|
||||||
|
require_mouse_activity: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +52,10 @@ impl Config {
|
|||||||
pub fn load(config_path: Option<&Path>) -> Result<Self, ConfigError> {
|
pub fn load(config_path: Option<&Path>) -> Result<Self, ConfigError> {
|
||||||
let mut config = Self::default();
|
let mut config = Self::default();
|
||||||
|
|
||||||
if let Some(path) = config_path.map(Path::to_path_buf).or_else(discover_config_path) {
|
if let Some(path) = config_path
|
||||||
|
.map(Path::to_path_buf)
|
||||||
|
.or_else(discover_config_path)
|
||||||
|
{
|
||||||
let raw = fs::read_to_string(&path).map_err(|source| ConfigError::Read {
|
let raw = fs::read_to_string(&path).map_err(|source| ConfigError::Read {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
source,
|
source,
|
||||||
@@ -96,6 +119,56 @@ impl Config {
|
|||||||
if let Ok(value) = env::var("CHRONOSEAL_LOG_FILE") {
|
if let Ok(value) = env::var("CHRONOSEAL_LOG_FILE") {
|
||||||
self.log_file = Some(PathBuf::from(value));
|
self.log_file = Some(PathBuf::from(value));
|
||||||
}
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_HEARTBEAT_MIN_INTERVAL_MS") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.heartbeat_min_interval_ms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_HEARTBEAT_MAX_INTERVAL_MS") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.heartbeat_max_interval_ms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_EXPIRATION_MINUTES") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.expiration_minutes = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_RATE_LIMIT_COUNT") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.rate_limit_count = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_RATE_LIMIT_WINDOW_SECS") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.rate_limit_window_secs = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_MAX_TIMESTAMP_DRIFT_MS") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.max_timestamp_drift_ms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_MIN_MOUSE_TOTAL_DIST") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.min_mouse_total_dist = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_MAX_MOUSE_AVG_SPEED") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.max_mouse_avg_speed = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_MIN_PAUSE_COUNT") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.min_pause_count = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_REQUIRE_MOUSE_ACTIVITY") {
|
||||||
|
if let Ok(val) = value.parse() {
|
||||||
|
self.require_mouse_activity = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +195,9 @@ impl std::fmt::Display for ConfigError {
|
|||||||
Self::Parse { path, source } => {
|
Self::Parse { path, source } => {
|
||||||
write!(f, "failed to parse {} as TOML: {source}", path.display())
|
write!(f, "failed to parse {} as TOML: {source}", path.display())
|
||||||
}
|
}
|
||||||
Self::InvalidBind { bind, source } => write!(f, "invalid bind address {bind}: {source}"),
|
Self::InvalidBind { bind, source } => {
|
||||||
|
write!(f, "invalid bind address {bind}: {source}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +205,12 @@ impl std::fmt::Display for ConfigError {
|
|||||||
impl std::error::Error for ConfigError {}
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
fn discover_config_path() -> Option<PathBuf> {
|
fn discover_config_path() -> Option<PathBuf> {
|
||||||
|
if let Ok(path) = env::var("CHRONOSEAL_CONFIG") {
|
||||||
|
let p = PathBuf::from(path);
|
||||||
|
if p.is_file() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
user_config_candidates()
|
user_config_candidates()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|candidate| candidate.is_file())
|
.find(|candidate| candidate.is_file())
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ pub fn verify_signature(
|
|||||||
pub_key_bytes: &[u8],
|
pub_key_bytes: &[u8],
|
||||||
req: &HeartbeatRequest,
|
req: &HeartbeatRequest,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let pk = VerifyingKey::from_bytes(
|
let pk = VerifyingKey::from_bytes(&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?)?;
|
||||||
&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?,
|
|
||||||
)?;
|
|
||||||
let sig_bytes = hex::decode(&req.signature)?;
|
let sig_bytes = hex::decode(&req.signature)?;
|
||||||
let sig = Signature::from_slice(&sig_bytes)?;
|
let sig = Signature::from_slice(&sig_bytes)?;
|
||||||
|
|
||||||
@@ -26,4 +24,4 @@ pub fn verify_signature(
|
|||||||
|
|
||||||
pk.verify_strict(message.as_bytes(), &sig)?;
|
pk.verify_strict(message.as_bytes(), &sig)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SessionError {
|
||||||
|
#[error("Hex decoding error: {0}")]
|
||||||
|
Hex(#[from] hex::FromHexError),
|
||||||
|
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] rusqlite::Error),
|
||||||
|
|
||||||
|
#[error("R2D2 pool error: {0}")]
|
||||||
|
Pool(#[from] r2d2::Error),
|
||||||
|
|
||||||
|
#[error("Invalid public key length")]
|
||||||
|
InvalidPublicKeyLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for SessionError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, error_message) = match self {
|
||||||
|
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||||
|
_ => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal server error".to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let body = Json(json!({
|
||||||
|
"error": error_message
|
||||||
|
}));
|
||||||
|
(status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum VerificationError {
|
||||||
|
#[error("Session not found")]
|
||||||
|
SessionNotFound,
|
||||||
|
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] rusqlite::Error),
|
||||||
|
|
||||||
|
#[error("Hex decoding error: {0}")]
|
||||||
|
Hex(#[from] hex::FromHexError),
|
||||||
|
|
||||||
|
#[error("Signature verification error: {0}")]
|
||||||
|
Signature(String),
|
||||||
|
|
||||||
|
#[error("Session has expired")]
|
||||||
|
Expired,
|
||||||
|
|
||||||
|
#[error("Chain is broken")]
|
||||||
|
ChainBroken,
|
||||||
|
|
||||||
|
#[error("Timestamp drift exceeded threshold")]
|
||||||
|
TimestampDrift,
|
||||||
|
|
||||||
|
#[error("Trust criteria failed: {0}")]
|
||||||
|
TrustFailed(String),
|
||||||
|
|
||||||
|
#[error("Fingerprint validation failed: {0}")]
|
||||||
|
FingerprintFailed(String),
|
||||||
|
}
|
||||||
@@ -2,9 +2,15 @@ use shared::protocol::Fingerprint;
|
|||||||
|
|
||||||
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
|
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
|
||||||
if ar < 0.5 || ar > 3.0 { return Err("aspect ratio".into()); }
|
if !(0.5..=3.0).contains(&ar) {
|
||||||
|
return Err("aspect ratio".into());
|
||||||
|
}
|
||||||
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
|
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
|
||||||
if dpr <= 0.0 || dpr > 5.0 { return Err("dpr".into()); }
|
if dpr <= 0.0 || dpr > 5.0 {
|
||||||
if fp.hardware_concurrency == 0 { return Err("hw".into()); }
|
return Err("dpr".into());
|
||||||
|
}
|
||||||
|
if fp.hardware_concurrency == 0 {
|
||||||
|
return Err("hw".into());
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ mod cleanup;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
|
mod errors;
|
||||||
mod fingerprint;
|
mod fingerprint;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
mod output;
|
mod output;
|
||||||
@@ -29,6 +30,9 @@ async fn main() {
|
|||||||
|
|
||||||
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
if let Some(config_path) = cli.globals.config.as_deref() {
|
||||||
|
std::env::set_var("CHRONOSEAL_CONFIG", config_path);
|
||||||
|
}
|
||||||
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
|
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
|
||||||
let log_file = log_file_for_command(&cli);
|
let log_file = log_file_for_command(&cli);
|
||||||
let _log_guard = init_logging(log_filter, log_file)?;
|
let _log_guard = init_logging(log_filter, log_file)?;
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ pub async fn log_request(req: Request, next: Next) -> Response {
|
|||||||
let response = next.run(req).await;
|
let response = next.run(req).await;
|
||||||
tracing::info!("{} {} -> {}", method, uri, response.status());
|
tracing::info!("{} {} -> {}", method, uri, response.status());
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
+45
-11
@@ -3,22 +3,22 @@ use std::time::Instant;
|
|||||||
|
|
||||||
pub struct RateLimiter {
|
pub struct RateLimiter {
|
||||||
buckets: HashMap<String, (u32, Instant)>,
|
buckets: HashMap<String, (u32, Instant)>,
|
||||||
limit: u32,
|
|
||||||
window_secs: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RateLimiter {
|
impl RateLimiter {
|
||||||
pub fn new(limit: u32, window_secs: u64) -> Self {
|
pub fn new() -> Self {
|
||||||
Self { buckets: HashMap::new(), limit, window_secs }
|
Self {
|
||||||
|
buckets: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(&mut self, key: &str) -> bool {
|
pub fn check(&mut self, key: &str, limit: u32, window_secs: u64) -> bool {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
let entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
||||||
if now.duration_since(entry.1).as_secs() >= self.window_secs {
|
if now.duration_since(entry.1).as_secs() >= window_secs {
|
||||||
*entry = (1, now);
|
*entry = (1, now);
|
||||||
true
|
true
|
||||||
} else if entry.0 >= self.limit {
|
} else if entry.0 >= limit {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
entry.0 += 1;
|
entry.0 += 1;
|
||||||
@@ -28,10 +28,44 @@ impl RateLimiter {
|
|||||||
|
|
||||||
/// Remove entries whose rate-limit window has fully elapsed.
|
/// Remove entries whose rate-limit window has fully elapsed.
|
||||||
/// Call this periodically (e.g. from the cleanup loop) to bound memory usage.
|
/// Call this periodically (e.g. from the cleanup loop) to bound memory usage.
|
||||||
pub fn evict_stale(&mut self) {
|
pub fn evict_stale(&mut self, window_secs: u64) {
|
||||||
let window = self.window_secs;
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
self.buckets
|
self.buckets
|
||||||
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window);
|
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rate_limiter() {
|
||||||
|
let mut rl = RateLimiter::new();
|
||||||
|
// Limit of 2 requests per 1 second window
|
||||||
|
assert!(rl.check("user1", 2, 1));
|
||||||
|
assert!(rl.check("user1", 2, 1));
|
||||||
|
assert!(!rl.check("user1", 2, 1)); // 3rd fails
|
||||||
|
|
||||||
|
assert!(rl.check("user2", 2, 1)); // different key succeeds
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(1100));
|
||||||
|
assert!(rl.check("user1", 2, 1)); // succeeds after time window
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rate_limiter_eviction() {
|
||||||
|
let mut rl = RateLimiter::new();
|
||||||
|
assert!(rl.check("user1", 1, 1));
|
||||||
|
assert_eq!(rl.buckets.len(), 1);
|
||||||
|
|
||||||
|
rl.evict_stale(1);
|
||||||
|
assert_eq!(rl.buckets.len(), 1); // not stale yet
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(1100));
|
||||||
|
rl.evict_stale(1);
|
||||||
|
assert_eq!(rl.buckets.len(), 0); // evicted
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::{extract::State, http::StatusCode, Json};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use shared::protocol::{HeartbeatRequest, HeartbeatResponse};
|
|
||||||
use crate::session::AppState;
|
use crate::session::AppState;
|
||||||
|
use axum::{extract::State, http::StatusCode, Json};
|
||||||
|
use shared::protocol::{HeartbeatRequest, HeartbeatResponse};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -9,22 +9,63 @@ pub async fn handler(
|
|||||||
) -> (StatusCode, Json<HeartbeatResponse>) {
|
) -> (StatusCode, Json<HeartbeatResponse>) {
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
{
|
{
|
||||||
|
let (limit, window_secs) = {
|
||||||
|
if let Ok(cfg) = state.config.read() {
|
||||||
|
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
|
||||||
|
} else {
|
||||||
|
(5, 10)
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut rl = state.rate_limiter.lock().await;
|
let mut rl = state.rate_limiter.lock().await;
|
||||||
if !rl.check(&payload.session_id) {
|
if !rl.check(&payload.session_id, limit, window_secs) {
|
||||||
tracing::debug!("Rate limit hit: {}", payload.session_id);
|
tracing::debug!("Rate limit hit: {}", payload.session_id);
|
||||||
return (StatusCode::OK, Json(HeartbeatResponse { status: "ok".into(), next_salt: None }));
|
return (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(HeartbeatResponse {
|
||||||
|
status: "ok".into(),
|
||||||
|
next_salt: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = state.db.lock().await;
|
let config = {
|
||||||
match crate::session::verify_heartbeat(&db, &payload) {
|
if let Ok(cfg) = state.config.read() {
|
||||||
|
cfg.clone()
|
||||||
|
} else {
|
||||||
|
crate::config::Config::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let conn = match state.db_pool.get() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Db pool error: {}", e);
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(HeartbeatResponse {
|
||||||
|
status: "error".into(),
|
||||||
|
next_salt: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match crate::session::verify_heartbeat(&conn, &config, &payload) {
|
||||||
Ok(next_salt) => (
|
Ok(next_salt) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(HeartbeatResponse { status: "ok".into(), next_salt: Some(next_salt) }),
|
Json(HeartbeatResponse {
|
||||||
|
status: "ok".into(),
|
||||||
|
next_salt: Some(next_salt),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
|
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
|
||||||
(StatusCode::OK, Json(HeartbeatResponse { status: "ok".into(), next_salt: None }))
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(HeartbeatResponse {
|
||||||
|
status: "ok".into(),
|
||||||
|
next_salt: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+16
-12
@@ -1,17 +1,21 @@
|
|||||||
use axum::{extract::State, http::StatusCode, Json};
|
use crate::errors::SessionError;
|
||||||
use std::sync::Arc;
|
|
||||||
use shared::protocol::{InitRequest, InitResponse};
|
|
||||||
use crate::session::AppState;
|
use crate::session::AppState;
|
||||||
|
use axum::{extract::State, Json};
|
||||||
|
use shared::protocol::{InitRequest, InitResponse};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(payload): Json<InitRequest>,
|
Json(payload): Json<InitRequest>,
|
||||||
) -> Result<Json<InitResponse>, (StatusCode, String)> {
|
) -> Result<Json<InitResponse>, SessionError> {
|
||||||
let db = state.db.lock().await;
|
let config = {
|
||||||
crate::session::create_session(&db, &payload.public_key)
|
if let Ok(cfg) = state.config.read() {
|
||||||
.map(Json)
|
cfg.clone()
|
||||||
.map_err(|e| {
|
} else {
|
||||||
tracing::error!("Init error: {}", e);
|
crate::config::Config::default()
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "Internal".into())
|
}
|
||||||
})
|
};
|
||||||
}
|
let conn = state.db_pool.get()?;
|
||||||
|
let resp = crate::session::create_session(&conn, &config, &payload.public_key)?;
|
||||||
|
Ok(Json(resp))
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
pub mod init;
|
|
||||||
pub mod heartbeat;
|
pub mod heartbeat;
|
||||||
|
pub mod init;
|
||||||
+40
-22
@@ -41,7 +41,9 @@ pub struct StatusReport {
|
|||||||
|
|
||||||
impl TextOutput for StatusReport {
|
impl TextOutput for StatusReport {
|
||||||
fn to_text(&self) -> String {
|
fn to_text(&self) -> String {
|
||||||
let pid = self.pid.map_or_else(|| "unknown".to_string(), |pid| pid.to_string());
|
let pid = self
|
||||||
|
.pid
|
||||||
|
.map_or_else(|| "unknown".to_string(), |pid| pid.to_string());
|
||||||
format!(
|
format!(
|
||||||
"running={}\nhealthy={}\nbind={}\npid_file={}\npid={}",
|
"running={}\nhealthy={}\nbind={}\npid_file={}\npid={}",
|
||||||
self.running, self.healthy, self.bind, self.pid_file, pid
|
self.running, self.healthy, self.bind, self.pid_file, pid
|
||||||
@@ -106,13 +108,11 @@ impl TextOutput for StoreStats {
|
|||||||
pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
install_pid_file(&config.pid_file)?;
|
install_pid_file(&config.pid_file)?;
|
||||||
|
|
||||||
let conn = storage::init_db(&config.db_path)?;
|
let db_pool = storage::init_pool(&config.db_path)?;
|
||||||
let state = Arc::new(session::AppState {
|
let state = Arc::new(session::AppState {
|
||||||
db: Mutex::new(conn),
|
db_pool,
|
||||||
rate_limiter: Mutex::new(RateLimiter::new(
|
rate_limiter: Mutex::new(RateLimiter::new()),
|
||||||
shared::constants::RATE_LIMIT_COUNT,
|
config: std::sync::RwLock::new(config.clone()),
|
||||||
shared::constants::RATE_LIMIT_WINDOW_SECS,
|
|
||||||
)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let bg_state = state.clone();
|
let bg_state = state.clone();
|
||||||
@@ -124,16 +124,19 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
|||||||
.route("/health", get(health_handler))
|
.route("/health", get(health_handler))
|
||||||
.route("/metrics", get(metrics_handler))
|
.route("/metrics", get(metrics_handler))
|
||||||
.route("/stats", get(stats_handler))
|
.route("/stats", get(stats_handler))
|
||||||
.nest_service("/", tower_http::services::ServeDir::new(&config.frontend_dir))
|
.nest_service(
|
||||||
|
"/",
|
||||||
|
tower_http::services::ServeDir::new(&config.frontend_dir),
|
||||||
|
)
|
||||||
.layer(tower_http::cors::CorsLayer::permissive())
|
.layer(tower_http::cors::CorsLayer::permissive())
|
||||||
.layer(axum::middleware::from_fn(crate::middleware::log_request))
|
.layer(axum::middleware::from_fn(crate::middleware::log_request))
|
||||||
.with_state(state);
|
.with_state(state.clone());
|
||||||
|
|
||||||
let addr: SocketAddr = config.bind.parse()?;
|
let addr: SocketAddr = config.bind.parse()?;
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
info!(bind = %config.bind, "chronoseal daemon started");
|
info!(bind = %config.bind, "chronoseal daemon started");
|
||||||
|
|
||||||
let shutdown = signal_task(config.clone());
|
let shutdown = signal_task(state.clone());
|
||||||
let result = axum::serve(listener, app)
|
let result = axum::serve(listener, app)
|
||||||
.with_graceful_shutdown(shutdown)
|
.with_graceful_shutdown(shutdown)
|
||||||
.await;
|
.await;
|
||||||
@@ -199,13 +202,19 @@ pub fn version() -> VersionReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn health_handler() -> impl IntoResponse {
|
async fn health_handler() -> impl IntoResponse {
|
||||||
(StatusCode::OK, Json(serde_json::json!({ "status": "healthy" })))
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(serde_json::json!({ "status": "healthy" })),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stats_handler(
|
async fn stats_handler(
|
||||||
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||||
) -> Result<Json<StoreStats>, (StatusCode, String)> {
|
) -> Result<Json<StoreStats>, (StatusCode, String)> {
|
||||||
let db = state.db.lock().await;
|
let db = state
|
||||||
|
.db_pool
|
||||||
|
.get()
|
||||||
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||||
storage::stats(&db)
|
storage::stats(&db)
|
||||||
.map(Json)
|
.map(Json)
|
||||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
||||||
@@ -214,7 +223,10 @@ async fn stats_handler(
|
|||||||
async fn metrics_handler(
|
async fn metrics_handler(
|
||||||
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||||
) -> Result<String, (StatusCode, String)> {
|
) -> Result<String, (StatusCode, String)> {
|
||||||
let db = state.db.lock().await;
|
let db = state
|
||||||
|
.db_pool
|
||||||
|
.get()
|
||||||
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||||
storage::stats(&db)
|
storage::stats(&db)
|
||||||
.map(|stats| {
|
.map(|stats| {
|
||||||
format!(
|
format!(
|
||||||
@@ -225,7 +237,7 @@ async fn metrics_handler(
|
|||||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn signal_task(config: Config) {
|
async fn signal_task(state: Arc<session::AppState>) {
|
||||||
let shutdown = Arc::new(Notify::new());
|
let shutdown = Arc::new(Notify::new());
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -248,19 +260,23 @@ async fn signal_task(config: Config) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let hup_config = config.clone();
|
let state_for_hup = state.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut sighup = signal(SignalKind::hangup()).expect("install SIGHUP handler");
|
let mut sighup = signal(SignalKind::hangup()).expect("install SIGHUP handler");
|
||||||
while sighup.recv().await.is_some() {
|
while sighup.recv().await.is_some() {
|
||||||
match Config::load(None) {
|
match Config::load(None) {
|
||||||
Ok(reloaded) => info!(
|
Ok(reloaded) => {
|
||||||
bind = %reloaded.bind,
|
info!(
|
||||||
db_path = %reloaded.db_path.display(),
|
bind = %reloaded.bind,
|
||||||
"received SIGHUP; configuration reloaded"
|
db_path = %reloaded.db_path.display(),
|
||||||
),
|
"received SIGHUP; configuration reloaded"
|
||||||
|
);
|
||||||
|
if let Ok(mut config_write) = state_for_hup.config.write() {
|
||||||
|
*config_write = reloaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => warn!(error = %err, "received SIGHUP; configuration reload failed"),
|
Err(err) => warn!(error = %err, "received SIGHUP; configuration reload failed"),
|
||||||
}
|
}
|
||||||
let _ = &hup_config;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -312,7 +328,9 @@ fn read_pid(path: &Path) -> Option<u32> {
|
|||||||
fn http_get(bind: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
fn http_get(bind: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let mut stream = TcpStream::connect_timeout(&bind.parse()?, Duration::from_secs(2))?;
|
let mut stream = TcpStream::connect_timeout(&bind.parse()?, Duration::from_secs(2))?;
|
||||||
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
|
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
|
||||||
stream.write_all(format!("GET {path} HTTP/1.1\r\nHost: chronoseal\r\nConnection: close\r\n\r\n").as_bytes())?;
|
stream.write_all(
|
||||||
|
format!("GET {path} HTTP/1.1\r\nHost: chronoseal\r\nConnection: close\r\n\r\n").as_bytes(),
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut response = String::new();
|
let mut response = String::new();
|
||||||
stream.read_to_string(&mut response)?;
|
stream.read_to_string(&mut response)?;
|
||||||
|
|||||||
+132
-16
@@ -1,24 +1,26 @@
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: tokio::sync::Mutex<rusqlite::Connection>,
|
pub db_pool: crate::storage::DbPool,
|
||||||
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>,
|
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>,
|
||||||
|
pub config: std::sync::RwLock<crate::config::Config>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::{crypto, fingerprint, storage, trust, vm};
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use shared::protocol::{HeartbeatRequest, InitResponse};
|
use shared::protocol::{HeartbeatRequest, InitResponse};
|
||||||
use crate::{crypto, trust, fingerprint, vm, storage};
|
|
||||||
|
|
||||||
pub fn create_session(
|
pub fn create_session(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
|
config: &crate::config::Config,
|
||||||
pub_key_hex: &str,
|
pub_key_hex: &str,
|
||||||
) -> Result<InitResponse, Box<dyn std::error::Error>> {
|
) -> Result<InitResponse, crate::errors::SessionError> {
|
||||||
let pub_key = hex::decode(pub_key_hex)?;
|
let pub_key = hex::decode(pub_key_hex)?;
|
||||||
if pub_key.len() != shared::constants::SESSION_ID_LEN {
|
if pub_key.len() != shared::constants::SESSION_ID_LEN {
|
||||||
return Err("invalid pubkey len".into());
|
return Err(crate::errors::SessionError::InvalidPublicKeyLength);
|
||||||
}
|
}
|
||||||
let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>());
|
let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>());
|
||||||
let salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
|
let salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
|
||||||
let now = storage::current_time_ms();
|
let now = storage::current_time_ms();
|
||||||
let expires_at = now + (shared::constants::EXPIRATION_MINUTES as u64) * 60 * 1000;
|
let expires_at = now + (config.expiration_minutes as u64) * 60 * 1000;
|
||||||
|
|
||||||
let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt);
|
let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt);
|
||||||
|
|
||||||
@@ -37,43 +39,56 @@ pub fn create_session(
|
|||||||
opcodes_b64,
|
opcodes_b64,
|
||||||
initial_hash: hex::encode(&initial_hash),
|
initial_hash: hex::encode(&initial_hash),
|
||||||
expires_at,
|
expires_at,
|
||||||
|
heartbeat_min_interval_ms: config.heartbeat_min_interval_ms,
|
||||||
|
heartbeat_max_interval_ms: config.heartbeat_max_interval_ms,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_heartbeat(
|
pub fn verify_heartbeat(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
|
config: &crate::config::Config,
|
||||||
req: &HeartbeatRequest,
|
req: &HeartbeatRequest,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, crate::errors::VerificationError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT public_key, salt, last_hash, expires_at FROM sessions WHERE session_id = ?1",
|
"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) =
|
let (pub_key, salt, stored_last_hash, expires_at): (Vec<u8>, Vec<u8>, Vec<u8>, u64) = stmt
|
||||||
stmt.query_row(params![req.session_id], |row| {
|
.query_row(params![req.session_id], |row| {
|
||||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
|
||||||
|
})
|
||||||
|
.map_err(|e| {
|
||||||
|
if matches!(e, rusqlite::Error::QueryReturnedNoRows) {
|
||||||
|
crate::errors::VerificationError::SessionNotFound
|
||||||
|
} else {
|
||||||
|
crate::errors::VerificationError::Database(e)
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let now = storage::current_time_ms();
|
let now = storage::current_time_ms();
|
||||||
if now > expires_at {
|
if now > expires_at {
|
||||||
return Err("expired".into());
|
return Err(crate::errors::VerificationError::Expired);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Verify signature
|
// 1. Verify signature
|
||||||
crypto::verify_signature(&pub_key, req)?;
|
crypto::verify_signature(&pub_key, req)
|
||||||
|
.map_err(|e| crate::errors::VerificationError::Signature(e.to_string()))?;
|
||||||
|
|
||||||
// 2. Check chain continuity
|
// 2. Check chain continuity
|
||||||
if stored_last_hash != hex::decode(&req.prev_hash)? {
|
if stored_last_hash != hex::decode(&req.prev_hash)? {
|
||||||
return Err("chain broken".into());
|
return Err(crate::errors::VerificationError::ChainBroken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Time window
|
// 3. Time window
|
||||||
let diff = (now as i64) - (req.timestamp as i64);
|
let diff = (now as i64) - (req.timestamp as i64);
|
||||||
if diff.abs() > shared::constants::MAX_TIMESTAMP_DRIFT_MS {
|
if diff.abs() > config.max_timestamp_drift_ms {
|
||||||
return Err("timestamp drift".into());
|
return Err(crate::errors::VerificationError::TimestampDrift);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Trusted mouse & fingerprint
|
// 4. Trusted mouse & fingerprint
|
||||||
trust::validate_mouse(&req.entropy_data)?;
|
trust::validate_mouse(&req.entropy_data, config)
|
||||||
fingerprint::validate(&req.fingerprint)?;
|
.map_err(|e| crate::errors::VerificationError::TrustFailed(e.to_string()))?;
|
||||||
|
fingerprint::validate(&req.fingerprint)
|
||||||
|
.map_err(|e| crate::errors::VerificationError::FingerprintFailed(e.to_string()))?;
|
||||||
|
|
||||||
// 5. Compute new hash
|
// 5. Compute new hash
|
||||||
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
|
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
|
||||||
@@ -95,4 +110,105 @@ pub fn verify_heartbeat(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(next_salt_hex)
|
Ok(next_salt_hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use ed25519_dalek::{Signer, SigningKey};
|
||||||
|
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, StackState};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
|
||||||
|
let mut payload: std::collections::BTreeMap<&str, serde_json::Value> =
|
||||||
|
std::collections::BTreeMap::new();
|
||||||
|
payload.insert(
|
||||||
|
"entropyData",
|
||||||
|
serde_json::to_value(&req.entropy_data).unwrap(),
|
||||||
|
);
|
||||||
|
payload.insert(
|
||||||
|
"fingerprint",
|
||||||
|
serde_json::to_value(&req.fingerprint).unwrap(),
|
||||||
|
);
|
||||||
|
payload.insert("prevHash", serde_json::json!(req.prev_hash));
|
||||||
|
payload.insert("sessionId", serde_json::json!(req.session_id));
|
||||||
|
payload.insert(
|
||||||
|
"stackState",
|
||||||
|
serde_json::to_value(&req.stack_state).unwrap(),
|
||||||
|
);
|
||||||
|
payload.insert("timestamp", serde_json::json!(req.timestamp));
|
||||||
|
let message = serde_json::to_string(&payload).unwrap();
|
||||||
|
let sig = sk.sign(message.as_bytes());
|
||||||
|
req.signature = hex::encode(sig.to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_session_lifecycle_and_verification() {
|
||||||
|
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
|
||||||
|
let config = crate::config::Config {
|
||||||
|
expiration_minutes: 30,
|
||||||
|
max_timestamp_drift_ms: 30000,
|
||||||
|
min_mouse_total_dist: 10.0,
|
||||||
|
max_mouse_avg_speed: 2.0,
|
||||||
|
min_pause_count: 1,
|
||||||
|
require_mouse_activity: false, // simpler for tests
|
||||||
|
..crate::config::Config::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate Ed25519 keypair
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let sk = SigningKey::generate(&mut rng);
|
||||||
|
let pk = sk.verifying_key();
|
||||||
|
let pub_key_hex = hex::encode(pk.to_bytes());
|
||||||
|
|
||||||
|
// 1. Create Session
|
||||||
|
let start_time = storage::current_time_ms();
|
||||||
|
let init_resp = create_session(&conn, &config, &pub_key_hex).unwrap();
|
||||||
|
assert!(init_resp.expires_at >= start_time + 30 * 60 * 1000);
|
||||||
|
assert!(init_resp.expires_at <= storage::current_time_ms() + 30 * 60 * 1000);
|
||||||
|
|
||||||
|
// Verify stats
|
||||||
|
let stats = storage::stats(&conn).unwrap();
|
||||||
|
assert_eq!(stats.sessions, 1);
|
||||||
|
assert_eq!(stats.expired_sessions, 0);
|
||||||
|
|
||||||
|
// 2. Heartbeat Verification
|
||||||
|
let now = storage::current_time_ms();
|
||||||
|
let entropy_data = EntropyData { events: vec![] };
|
||||||
|
let stack_state = StackState {
|
||||||
|
stack: vec![42],
|
||||||
|
ip: 5,
|
||||||
|
};
|
||||||
|
let fingerprint = Fingerprint {
|
||||||
|
aspect_ratio: "1.77".to_string(),
|
||||||
|
device_pixel_ratio: "2.0".to_string(),
|
||||||
|
hardware_concurrency: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut req = HeartbeatRequest {
|
||||||
|
session_id: init_resp.session_id.clone(),
|
||||||
|
prev_hash: init_resp.initial_hash.clone(),
|
||||||
|
timestamp: now,
|
||||||
|
entropy_data,
|
||||||
|
stack_state,
|
||||||
|
fingerprint,
|
||||||
|
signature: "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_request(&sk, &mut req);
|
||||||
|
|
||||||
|
// Verify successful heartbeat
|
||||||
|
let next_salt = verify_heartbeat(&conn, &config, &req).unwrap();
|
||||||
|
assert!(!next_salt.is_empty());
|
||||||
|
|
||||||
|
// Try duplicate/broken hash chain (prev_hash unchanged but expected next hash in DB)
|
||||||
|
let res = verify_heartbeat(&conn, &config, &req);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
res.unwrap_err(),
|
||||||
|
crate::errors::VerificationError::ChainBroken
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-11
@@ -10,17 +10,25 @@ pub struct StoreStats {
|
|||||||
pub max_chain_length: u64,
|
pub max_chain_length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_db(path: &Path) -> Result<Connection, rusqlite::Error> {
|
pub type DbPool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
|
||||||
if path == Path::new(":memory:") {
|
|
||||||
return init_schema(Connection::open_in_memory()?);
|
pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> {
|
||||||
}
|
let manager = if path == Path::new(":memory:") {
|
||||||
if let Some(parent) = path.parent() {
|
r2d2_sqlite::SqliteConnectionManager::memory()
|
||||||
let _ = std::fs::create_dir_all(parent);
|
} else {
|
||||||
}
|
if let Some(parent) = path.parent() {
|
||||||
init_schema(Connection::open(path)?)
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
r2d2_sqlite::SqliteConnectionManager::file(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool = r2d2::Pool::new(manager)?;
|
||||||
|
let conn = pool.get()?;
|
||||||
|
init_schema(&conn)?;
|
||||||
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_schema(conn: Connection) -> Result<Connection, rusqlite::Error> {
|
fn init_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"CREATE TABLE IF NOT EXISTS sessions (
|
"CREATE TABLE IF NOT EXISTS sessions (
|
||||||
session_id TEXT PRIMARY KEY,
|
session_id TEXT PRIMARY KEY,
|
||||||
@@ -33,7 +41,7 @@ fn init_schema(conn: Connection) -> Result<Connection, rusqlite::Error> {
|
|||||||
expires_at INTEGER NOT NULL
|
expires_at INTEGER NOT NULL
|
||||||
);",
|
);",
|
||||||
)?;
|
)?;
|
||||||
Ok(conn)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stats(conn: &Connection) -> Result<StoreStats, rusqlite::Error> {
|
pub fn stats(conn: &Connection) -> Result<StoreStats, rusqlite::Error> {
|
||||||
@@ -57,5 +65,8 @@ pub fn stats(conn: &Connection) -> Result<StoreStats, rusqlite::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_time_ms() -> u64 {
|
pub fn current_time_ms() -> u64 {
|
||||||
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64
|
||||||
}
|
}
|
||||||
+189
-7
@@ -1,7 +1,14 @@
|
|||||||
|
use crate::config::Config;
|
||||||
use shared::protocol::EntropyData;
|
use shared::protocol::EntropyData;
|
||||||
|
|
||||||
pub fn validate_mouse(data: &EntropyData) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn validate_mouse(
|
||||||
|
data: &EntropyData,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let events = &data.events;
|
let events = &data.events;
|
||||||
|
if !config.require_mouse_activity && events.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if events.len() < 3 {
|
if events.len() < 3 {
|
||||||
return Err("few events".into());
|
return Err("few events".into());
|
||||||
}
|
}
|
||||||
@@ -19,18 +26,193 @@ pub fn validate_mouse(data: &EntropyData) -> Result<(), Box<dyn std::error::Erro
|
|||||||
pauses += 1;
|
pauses += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if total_dist < shared::constants::MIN_MOUSE_TOTAL_DIST {
|
if total_dist < config.min_mouse_total_dist {
|
||||||
return Err("insufficient distance".into());
|
return Err("insufficient distance".into());
|
||||||
}
|
}
|
||||||
// Speed in px/ms: total distance over elapsed wall-clock time of the event window.
|
// Speed in px/ms: total distance over elapsed wall-clock time of the event window.
|
||||||
let total_time_ms =
|
let total_time_ms = (events.last().unwrap().timestamp_ms - events[0].timestamp_ms).max(1.0);
|
||||||
(events.last().unwrap().timestamp_ms - events[0].timestamp_ms).max(1.0);
|
|
||||||
let avg_speed = total_dist / total_time_ms;
|
let avg_speed = total_dist / total_time_ms;
|
||||||
if avg_speed > shared::constants::MAX_MOUSE_AVG_SPEED {
|
if avg_speed > config.max_mouse_avg_speed {
|
||||||
return Err("speed too high".into());
|
return Err("speed too high".into());
|
||||||
}
|
}
|
||||||
if pauses < shared::constants::MIN_PAUSE_COUNT {
|
if pauses < config.min_pause_count {
|
||||||
return Err("no pause".into());
|
return Err("no pause".into());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use shared::protocol::MouseEvent;
|
||||||
|
|
||||||
|
fn get_default_config() -> Config {
|
||||||
|
Config {
|
||||||
|
min_mouse_total_dist: 10.0,
|
||||||
|
max_mouse_avg_speed: 2.0,
|
||||||
|
min_pause_count: 1,
|
||||||
|
require_mouse_activity: true,
|
||||||
|
..Config::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_mouse_success() {
|
||||||
|
let config = get_default_config();
|
||||||
|
// Mouse moves from (0,0) to (5,0) then (15,0) with a pause
|
||||||
|
let events = vec![
|
||||||
|
MouseEvent {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 100.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 5.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 200.0,
|
||||||
|
},
|
||||||
|
// Pause here (dist = 0, time diff = 100ms > 50ms)
|
||||||
|
MouseEvent {
|
||||||
|
x: 5.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 300.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 15.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 400.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = EntropyData { events };
|
||||||
|
assert!(validate_mouse(&data, &config).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_mouse_insufficient_events() {
|
||||||
|
let config = get_default_config();
|
||||||
|
let events = vec![
|
||||||
|
MouseEvent {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 100.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 5.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 200.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = EntropyData { events };
|
||||||
|
let res = validate_mouse(&data, &config);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert_eq!(res.unwrap_err().to_string(), "few events");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_mouse_insufficient_distance() {
|
||||||
|
let config = get_default_config();
|
||||||
|
// Total distance is only 5.0 < 10.0
|
||||||
|
let events = vec![
|
||||||
|
MouseEvent {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 100.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 2.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 200.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 2.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 300.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 5.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 400.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = EntropyData { events };
|
||||||
|
let res = validate_mouse(&data, &config);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert_eq!(res.unwrap_err().to_string(), "insufficient distance");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_mouse_too_fast() {
|
||||||
|
let config = get_default_config();
|
||||||
|
// Distance is 200.0, time difference is 70ms -> speed 2.85 > 2.0
|
||||||
|
let events = vec![
|
||||||
|
MouseEvent {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 100.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 100.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 105.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 100.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 165.0,
|
||||||
|
}, // pause
|
||||||
|
MouseEvent {
|
||||||
|
x: 200.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 170.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = EntropyData { events };
|
||||||
|
let res = validate_mouse(&data, &config);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert_eq!(res.unwrap_err().to_string(), "speed too high");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_mouse_no_pauses() {
|
||||||
|
let config = get_default_config();
|
||||||
|
// Constant movement without any pause
|
||||||
|
let events = vec![
|
||||||
|
MouseEvent {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 100.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 5.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 200.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 10.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 300.0,
|
||||||
|
},
|
||||||
|
MouseEvent {
|
||||||
|
x: 15.0,
|
||||||
|
y: 0.0,
|
||||||
|
timestamp_ms: 400.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = EntropyData { events };
|
||||||
|
let res = validate_mouse(&data, &config);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert_eq!(res.unwrap_err().to_string(), "no pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_mouse_require_activity_toggle() {
|
||||||
|
let mut config = get_default_config();
|
||||||
|
config.require_mouse_activity = false;
|
||||||
|
|
||||||
|
let data = EntropyData { events: vec![] };
|
||||||
|
assert!(validate_mouse(&data, &config).is_ok());
|
||||||
|
|
||||||
|
config.require_mouse_activity = true;
|
||||||
|
assert!(validate_mouse(&data, &config).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -43,4 +43,4 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
|
|||||||
ops
|
ops
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server does not need to execute the program; client does.
|
// Server does not need to execute the program; client does.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,11 +1,2 @@
|
|||||||
pub const SESSION_ID_LEN: usize = 32;
|
pub const SESSION_ID_LEN: usize = 32;
|
||||||
pub const SALT_LEN: usize = 16;
|
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;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use blake3::Hasher;
|
|
||||||
use crate::protocol::{EntropyData, StackState};
|
use crate::protocol::{EntropyData, StackState};
|
||||||
|
use blake3::Hasher;
|
||||||
|
|
||||||
/// Initial hash for a brand-new session: Blake3(session_id || pub_key || salt)
|
/// 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> {
|
pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||||
@@ -39,4 +39,4 @@ pub fn hash_stack(stack: &[u32]) -> u32 {
|
|||||||
let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect();
|
let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||||||
let hash = blake3::hash(&data);
|
let hash = blake3::hash(&data);
|
||||||
u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap())
|
u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap())
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod hashing;
|
pub mod hashing;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
@@ -12,6 +12,8 @@ pub struct InitResponse {
|
|||||||
pub opcodes_b64: String,
|
pub opcodes_b64: String,
|
||||||
pub initial_hash: String,
|
pub initial_hash: String,
|
||||||
pub expires_at: u64,
|
pub expires_at: u64,
|
||||||
|
pub heartbeat_min_interval_ms: u64,
|
||||||
|
pub heartbeat_max_interval_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
@@ -59,4 +61,4 @@ pub struct MouseEvent {
|
|||||||
pub struct StackState {
|
pub struct StackState {
|
||||||
pub stack: Vec<u32>,
|
pub stack: Vec<u32>,
|
||||||
pub ip: u16,
|
pub ip: u16,
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chronoseal-wasm"
|
name = "chronoseal-wasm"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Example: break debugger detection, console clearing, etc.
|
// Example: break debugger detection, console clearing, etc.
|
||||||
// Currently empty.
|
// Currently empty.
|
||||||
+4
-6
@@ -3,7 +3,7 @@ use std::cell::RefCell;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static KEYPAIR: RefCell<Option<SigningKey>> = RefCell::new(None);
|
static KEYPAIR: RefCell<Option<SigningKey>> = const { RefCell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -51,10 +51,8 @@ pub fn compute_next_hash(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let prev = hex::decode(prev_hash_hex).unwrap_or_default();
|
let prev = hex::decode(prev_hash_hex).unwrap_or_default();
|
||||||
let salt = hex::decode(salt_hex).unwrap_or_default();
|
let salt = hex::decode(salt_hex).unwrap_or_default();
|
||||||
let entropy =
|
let entropy = serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json).unwrap();
|
||||||
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 stack =
|
|
||||||
serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap();
|
|
||||||
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
|
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
|
||||||
hex::encode(new)
|
hex::encode(new)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
// This module is handled on the JS side; WASM only receives the prepared entropy data.
|
// 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).
|
// Could be used to add extra entropy sources (e.g., from JS via import).
|
||||||
@@ -1 +1 @@
|
|||||||
// Fingerprint collection is done in JS, this module is a placeholder.
|
// Fingerprint collection is done in JS, this module is a placeholder.
|
||||||
+1
-1
@@ -3,4 +3,4 @@ pub mod crypto;
|
|||||||
pub mod entropy;
|
pub mod entropy;
|
||||||
pub mod fingerprint;
|
pub mod fingerprint;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
pub mod vm;
|
pub mod vm;
|
||||||
@@ -1 +1 @@
|
|||||||
// Could contain WebTransport related code if needed later.
|
// Could contain WebTransport related code if needed later.
|
||||||
+156
-8
@@ -1,10 +1,12 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
|
||||||
use shared::protocol::StackState;
|
use shared::protocol::StackState;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn run_program(program_b64: &str) -> JsValue {
|
pub fn run_program(program_b64: &str) -> JsValue {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let bytes = base64::engine::general_purpose::STANDARD.decode(program_b64).unwrap();
|
let bytes = base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(program_b64)
|
||||||
|
.unwrap();
|
||||||
let state = execute(&bytes);
|
let state = execute(&bytes);
|
||||||
serde_wasm_bindgen::to_value(&state).unwrap()
|
serde_wasm_bindgen::to_value(&state).unwrap()
|
||||||
}
|
}
|
||||||
@@ -17,13 +19,22 @@ fn execute(program: &[u8]) -> StackState {
|
|||||||
ip += 1;
|
ip += 1;
|
||||||
match op {
|
match op {
|
||||||
0x00 => {
|
0x00 => {
|
||||||
if ip + 4 > program.len() { break; }
|
if ip + 4 > program.len() {
|
||||||
let val = u32::from_le_bytes([program[ip], program[ip+1], program[ip+2], program[ip+3]]);
|
break;
|
||||||
|
}
|
||||||
|
let val = u32::from_le_bytes([
|
||||||
|
program[ip],
|
||||||
|
program[ip + 1],
|
||||||
|
program[ip + 2],
|
||||||
|
program[ip + 3],
|
||||||
|
]);
|
||||||
ip += 4;
|
ip += 4;
|
||||||
stack.push(val);
|
stack.push(val);
|
||||||
}
|
}
|
||||||
0x01..=0x07 => {
|
0x01..=0x07 => {
|
||||||
if stack.len() < 2 { break; }
|
if stack.len() < 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let b = stack.pop().unwrap();
|
let b = stack.pop().unwrap();
|
||||||
let a = stack.pop().unwrap();
|
let a = stack.pop().unwrap();
|
||||||
let r = match op {
|
let r = match op {
|
||||||
@@ -39,7 +50,9 @@ fn execute(program: &[u8]) -> StackState {
|
|||||||
stack.push(r);
|
stack.push(r);
|
||||||
}
|
}
|
||||||
0x08 => {
|
0x08 => {
|
||||||
if stack.is_empty() { break; }
|
if stack.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let a = stack.pop().unwrap();
|
let a = stack.pop().unwrap();
|
||||||
stack.push(!a);
|
stack.push(!a);
|
||||||
}
|
}
|
||||||
@@ -51,5 +64,140 @@ fn execute(program: &[u8]) -> StackState {
|
|||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StackState { stack, ip: ip as u16 }
|
StackState {
|
||||||
}
|
stack,
|
||||||
|
ip: ip as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_push() {
|
||||||
|
// PUSH 42, PUSH 100
|
||||||
|
let program = vec![0x00, 42, 0, 0, 0, 0x00, 100, 0, 0, 0];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![42, 100]);
|
||||||
|
assert_eq!(state.ip, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
// PUSH 5, PUSH 10, ADD
|
||||||
|
let program = vec![0x00, 5, 0, 0, 0, 0x00, 10, 0, 0, 0, 0x01];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![15]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_wrapping() {
|
||||||
|
// PUSH u32::MAX, PUSH 1, ADD
|
||||||
|
let program = vec![0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 1, 0, 0, 0, 0x01];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub() {
|
||||||
|
// PUSH 20, PUSH 7, SUB
|
||||||
|
let program = vec![0x00, 20, 0, 0, 0, 0x00, 7, 0, 0, 0, 0x02];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![13]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub_wrapping() {
|
||||||
|
// PUSH 0, PUSH 1, SUB
|
||||||
|
let program = vec![0x00, 0, 0, 0, 0, 0x00, 1, 0, 0, 0, 0x02];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![u32::MAX]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mul() {
|
||||||
|
// PUSH 6, PUSH 7, MUL
|
||||||
|
let program = vec![0x00, 6, 0, 0, 0, 0x00, 7, 0, 0, 0, 0x03];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![42]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xor() {
|
||||||
|
// PUSH 0b1010, PUSH 0b1100, XOR
|
||||||
|
let program = vec![0x00, 0b1010, 0, 0, 0, 0x00, 0b1100, 0, 0, 0, 0x04];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![0b0110]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and() {
|
||||||
|
// PUSH 0b1010, PUSH 0b1100, AND
|
||||||
|
let program = vec![0x00, 0b1010, 0, 0, 0, 0x00, 0b1100, 0, 0, 0, 0x05];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![0b1000]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_or() {
|
||||||
|
// PUSH 0b1010, PUSH 0b1100, OR
|
||||||
|
let program = vec![0x00, 0b1010, 0, 0, 0, 0x00, 0b1100, 0, 0, 0, 0x06];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![0b1110]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rot() {
|
||||||
|
// PUSH 1, PUSH 4, ROT
|
||||||
|
let program = vec![0x00, 1, 0, 0, 0, 0x00, 4, 0, 0, 0, 0x07];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![16]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_not() {
|
||||||
|
// PUSH 0, NOT
|
||||||
|
let program = vec![0x00, 0, 0, 0, 0, 0x08];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, vec![u32::MAX]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash() {
|
||||||
|
// PUSH 10, PUSH 20, HASH
|
||||||
|
let program = vec![0x00, 10, 0, 0, 0, 0x00, 20, 0, 0, 0, 0x09];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack.len(), 1);
|
||||||
|
let expected_hash = shared::hashing::hash_stack(&[10, 20]);
|
||||||
|
assert_eq!(state.stack[0], expected_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_underflow_binary() {
|
||||||
|
// PUSH 42, ADD (needs 2 values, only 1 on stack)
|
||||||
|
let program = vec![0x00, 42, 0, 0, 0, 0x01];
|
||||||
|
let state = execute(&program);
|
||||||
|
// ADD breaks when stack.len() < 2, stack has 42 left, ip is at the opcode ADD (6)
|
||||||
|
assert_eq!(state.stack, vec![42]);
|
||||||
|
assert_eq!(state.ip, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_underflow_unary() {
|
||||||
|
// NOT (needs 1 value, empty stack)
|
||||||
|
let program = vec![0x08];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, Vec::<u32>::new());
|
||||||
|
assert_eq!(state.ip, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_incomplete_push() {
|
||||||
|
// PUSH opcode, but only 2 bytes instead of 4
|
||||||
|
let program = vec![0x00, 42, 0];
|
||||||
|
let state = execute(&program);
|
||||||
|
assert_eq!(state.stack, Vec::<u32>::new());
|
||||||
|
assert_eq!(state.ip, 1); // execution stopped at op 0x00 because ip + 4 > program.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user