diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..2e07606 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/Cargo.lock b/Cargo.lock index bf4a660..28d7dd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -41,7 +50,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", + "axum-macros", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -54,6 +65,9 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", "tower", "tower-layer", @@ -79,6 +93,17 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -94,6 +119,18 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.9.4" @@ -112,6 +149,16 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cc" +version = "1.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -124,9 +171,12 @@ version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ + "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", + "windows-link", ] [[package]] @@ -139,6 +189,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "deranged" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -150,6 +231,12 @@ dependencies = [ "syn", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fnv" version = "1.0.7" @@ -226,12 +313,45 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.3.1" @@ -266,6 +386,30 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -400,6 +544,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "libc" version = "0.2.175" @@ -458,8 +617,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", - "windows-sys", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", ] [[package]] @@ -486,6 +670,16 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -533,6 +727,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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.101" @@ -551,6 +760,85 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -623,6 +911,17 @@ dependencies = [ "serde_core", ] +[[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" @@ -635,6 +934,24 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.16", + "time", +] + [[package]] name = "slab" version = "0.4.11" @@ -681,13 +998,33 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -701,6 +1038,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -739,6 +1107,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -757,6 +1141,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -775,13 +1165,55 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "rand 0.9.2", + "serde", + "uuid-rng-internal", + "wasm-bindgen", +] + +[[package]] +name = "uuid-rng-internal" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23426b4394875bbc29a3074f94e1b52cd0eed2c8410c21a6edbfb033daef0aa1" +dependencies = [ + "rand 0.9.2", +] + [[package]] name = "warden-worker" version = "0.1.0" dependencies = [ "axum", + "base64 0.21.7", + "chrono", "console_error_panic_hook", + "console_log", + "constant_time_eq", + "getrandom 0.3.3", + "hex", + "js-sys", + "jsonwebtoken", + "log", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 1.0.69", + "tower-http", "tower-service", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", "worker", "worker-macros", ] @@ -792,6 +1224,24 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.103" @@ -887,6 +1337,74 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -960,6 +1478,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "worker" version = "0.6.5" @@ -1002,7 +1526,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", - "thiserror", + "thiserror 2.0.16", "wasm-bindgen", "wasm-bindgen-futures", ] @@ -1065,6 +1589,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 5958439..41f476b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,36 @@ release = false crate-type = ["cdylib"] [dependencies] -worker = { version = "0.6", features = ['http', 'axum'] } +# Worker & Web APIs +worker = { version = "0.6", features = ["axum", "http", "d1"] } worker-macros = { version = "0.6", features = ['http'] } -axum = { version = "0.8", default-features = false } -tower-service = "0.3.3" +wasm-bindgen = "0.2" +js-sys = "0.3" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["Crypto", "CryptoKey", "SubtleCrypto", "WorkerGlobalScope", "Pbkdf2Params"] } console_error_panic_hook = "0.1.7" + +# Axum and Routing +axum = { version = "0.8", default-features = false, features=["json", "macros", "form"] } +tower-service = "0.3" +tower-http = { version = "0.5", features = ["cors"] } + +# Data & Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Crypto & Encoding +jsonwebtoken = "9.2" +base64 = "0.21" +hex = "0.4" +rand = { version = "0.8" } +getrandom = { version = "0.3", features = ["wasm_js"] } +constant_time_eq = "0.3" + +# Utilities +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.6", features = ["v4", "serde", "rng-rand"] } +log = "0.4" +thiserror = "1.0" +once_cell = "1.19" +console_log = "1.0.0" diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..2e086e1 --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,48 @@ +-- Drop tables if they exist to ensure a clean slate +DROP TABLE IF EXISTS folders; +DROP TABLE IF EXISTS ciphers; +DROP TABLE IF EXISTS users; + +-- Users table to store user accounts and their master keys/hashes +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT, + email TEXT NOT NULL UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT 0, + master_password_hash TEXT NOT NULL, + master_password_hint TEXT, + key TEXT NOT NULL, -- The encrypted symmetric key + private_key TEXT NOT NULL, -- encrypted asymmetric private_key + public_key TEXT NOT NULL, -- asymmetric public_key + kdf_type INTEGER NOT NULL DEFAULT 0, -- 0 for PBKDF2 + kdf_iterations INTEGER NOT NULL DEFAULT 600000, + security_stamp TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- Ciphers table for storing encrypted vault items +CREATE TABLE IF NOT EXISTS ciphers ( + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT, + organization_id TEXT, + type INTEGER NOT NULL, + data TEXT NOT NULL, -- JSON blob of all encrypted fields (name, notes, login, etc.) + favorite BOOLEAN NOT NULL DEFAULT 0, + folder_id TEXT, + deleted_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL +); + +-- Folders table for organizing ciphers +CREATE TABLE IF NOT EXISTS folders ( + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + name TEXT NOT NULL, -- Encrypted folder name + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..cc09657 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,57 @@ +use axum::{ + extract::{FromRequestParts, State}, + http::{header, request::Parts, StatusCode}, +}; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use worker::Env; + +use crate::error::AppError; + +// Secret key for signing JWTs. In a real application, this should be a strong, +// securely stored secret from the Worker's environment variables. +const JWT_SECRET: &str = "a-very-secure-secret-key-that-should-be-in-env"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, // User ID + pub exp: usize, // Expiration time + pub nbf: usize, // Not before time + + pub premium: bool, + pub name: String, + pub email: String, + pub email_verified: bool, + pub amr: Vec, +} + +impl FromRequestParts for Claims +where + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + // Extract the token from the authorization header + let token = parts + .headers + .get(header::AUTHORIZATION) + .and_then(|auth_header| auth_header.to_str().ok()) + .and_then(|auth_value| { + if auth_value.starts_with("Bearer ") { + Some(auth_value[7..].to_owned()) + } else { + None + } + }) + .ok_or_else(|| AppError::Unauthorized("Missing or invalid token".to_string()))?; + + // Decode and validate the token + let decoding_key = DecodingKey::from_secret(JWT_SECRET.as_ref()); + let token_data = decode::(&token, &decoding_key, &Validation::default()) + .map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?; + + Ok(token_data.claims) + } +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..369efc8 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,81 @@ +use js_sys::Uint8Array; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::JsFuture; +use web_sys::{CryptoKey, SubtleCrypto, WorkerGlobalScope}; +use worker::js_sys; + +use crate::error::AppError; + +pub fn worker_global() -> Option { + use wasm_bindgen::JsCast; + + js_sys::global().dyn_into::().ok() +} + +/// Gets the SubtleCrypto interface from the global scope. +fn subtle_crypto() -> Result { + Ok(worker_global() + .ok_or_else(|| AppError::Crypto("Could not get worker global scope".to_string()))? + .crypto() + .map_err(|e| AppError::Crypto(format!("Failed to get crypto: {:?}", e)))? + .subtle()) +} + +/// Derives a key using PBKDF2-SHA256. +pub async fn pbkdf2_sha256( + password: &[u8], + salt: &[u8], + iterations: u32, + key_length_bits: u32, +) -> Result, AppError> { + let subtle = subtle_crypto()?; + + // Import the password as a raw key material + let password_array = Uint8Array::new_from_slice(password); + let password_obj = password_array.as_ref(); + let key_material = JsFuture::from( + subtle + .import_key_with_str( + "raw", + password_obj, + "PBKDF2", + false, + &js_sys::Array::of1(&JsValue::from_str("deriveBits")), + ) + .map_err(|e| AppError::Crypto(format!("PBKDF2 import_key failed: {:?}", e)))?, + ) + .await + .map_err(|e| AppError::Crypto(format!("PBKDF2 import_key await failed: {:?}", e)))?; + + let salt_array = Uint8Array::new_from_slice(salt); + // Define PBKDF2 parameters + let mut params = web_sys::Pbkdf2Params::new( + "PBKDF2", + JsValue::from_str("SHA-256").as_ref(), + iterations, + salt_array.as_ref(), + ); + + // Derive the bits + let derived_bits = JsFuture::from( + subtle + .derive_bits_with_object( + params.as_ref(), + &CryptoKey::from(key_material), + key_length_bits, + ) + .map_err(|e| AppError::Crypto(format!("PBKDF2 derive_bits failed: {:?}", e)))?, + ) + .await + .map_err(|e| AppError::Crypto(format!("PBKDF2 derive_bits await failed: {:?}", e)))?; + + Ok(js_sys::Uint8Array::new(&derived_bits).to_vec()) +} + +/// Generates a hash of the master key for password verification. +pub async fn hash_master_key( + master_key: &[u8], + master_password: &[u8], +) -> Result, AppError> { + pbkdf2_sha256(master_key, master_password, 1, 256).await +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..7416df1 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,7 @@ +use crate::error::AppError; +use std::sync::Arc; +use worker::{D1Database, Env}; + +pub fn get_db(env: &Arc) -> Result { + env.d1("vault1").map_err(AppError::Worker) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..56a819e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,62 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde_json::json; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Worker error: {0}")] + Worker(#[from] worker::Error), + + #[error("Database query failed")] + Database, + + #[error("Not found: {0}")] + NotFound(String), + + #[error("Invalid request: {0}")] + BadRequest(String), + + #[error("Unauthorized: {0}")] + Unauthorized(String), + + #[error("Cryptography error: {0}")] + Crypto(String), + + #[error(transparent)] + JsonWebToken(#[from] jsonwebtoken::errors::Error), + + #[error("Internal server error")] + Internal, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, error_message) = match self { + AppError::Worker(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Worker error: {}", e), + ), + AppError::Database => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database error".to_string(), + ), + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg), + AppError::Crypto(msg) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Crypto error: {}", msg), + ), + AppError::JsonWebToken(_) => (StatusCode::UNAUTHORIZED, "Invalid token".to_string()), + AppError::Internal => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + ), + }; + + let body = Json(json!({ "error": error_message })); + (status, body).into_response() + } +} diff --git a/src/handlers/accounts.rs b/src/handlers/accounts.rs new file mode 100644 index 0000000..444250a --- /dev/null +++ b/src/handlers/accounts.rs @@ -0,0 +1,92 @@ +use axum::{extract::State, Json}; +use chrono::Utc; +use std::sync::Arc; +use uuid::Uuid; +use worker::{query, D1Database, Env}; + +use crate::{ + db, + error::AppError, + models::user::{PreloginResponse, RegisterRequest, User}, +}; + +#[worker::send] +pub async fn prelogin( + State(env): State>, + Json(payload): Json, +) -> Result, AppError> { + let email = payload["email"] + .as_str() + .ok_or_else(|| AppError::BadRequest("Missing email".to_string()))?; + let db = db::get_db(&env)?; + + log::info!("Getting kdf for user {email:?}"); + let stmt = db.prepare("SELECT kdf_iterations FROM users WHERE email = ?1"); + let query = stmt.bind(&[email.into()])?; + let kdf_iterations: Option = query + .first(Some("kdf_iterations")) + .await + .map_err(|_| AppError::Database)?; + + log::info!("Returning {kdf_iterations:?}"); + Ok(Json(PreloginResponse { + kdf: 0, // PBKDF2 + kdf_iterations: kdf_iterations.unwrap_or(600_000), + })) +} + +#[worker::send] +pub async fn register( + State(env): State>, + Json(payload): Json, +) -> Result, AppError> { + log::info!("Get db"); + let db = db::get_db(&env)?; + log::info!("db got"); + let now = Utc::now().to_rfc3339(); + let user = User { + id: Uuid::new_v4().to_string(), + name: payload.name, + email: payload.email.to_lowercase(), + email_verified: false, + master_password_hash: payload.master_password_hash, + master_password_hint: payload.master_password_hint, + key: payload.user_symmetric_key, + private_key: payload.user_asymmetric_keys.encrypted_private_key, + public_key: payload.user_asymmetric_keys.public_key, + kdf_type: payload.kdf, + kdf_iterations: payload.kdf_iterations, + security_stamp: Uuid::new_v4().to_string(), + created_at: now.clone(), + updated_at: now, + }; + + log::info!("User {user:?}"); + let query = query!( + &db, + "INSERT INTO users (id, name, email, master_password_hash, key, private_key, public_key, kdf_iterations, security_stamp, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + user.id, + user.name, + user.email, + user.master_password_hash, + user.key, + user.private_key, + user.public_key, + user.kdf_iterations, + user.security_stamp, + user.created_at, + user.updated_at + ).map_err(|error|{ + log::error!("failed {error:?}"); + AppError::Database + })? + .run() + .await + .map_err(|error|{ + log::error!("failed {error:?}"); + AppError::Database + })?; + + Ok(Json(())) +} diff --git a/src/handlers/ciphers.rs b/src/handlers/ciphers.rs new file mode 100644 index 0000000..e0e5677 --- /dev/null +++ b/src/handlers/ciphers.rs @@ -0,0 +1,41 @@ +use axum::{extract::State, Json}; +use chrono::Utc; +use std::sync::Arc; +use uuid::Uuid; +use worker::Env; + +use crate::{auth::Claims, db, error::AppError, models::cipher::Cipher}; + +#[worker::send] +pub async fn create_cipher( + claims: Claims, + State(env): State>, + Json(mut payload): Json, +) -> Result, AppError> { + let db = db::get_db(&env)?; + let now = Utc::now().to_rfc3339(); + + payload.id = Uuid::new_v4().to_string(); + payload.user_id = Some(claims.sub); + payload.created_at = now.clone(); + payload.updated_at = now; + + db.prepare( + "INSERT INTO ciphers (id, user_id, type, data, favorite, folder_id, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + ) + .bind(&[ + payload.id.clone().into(), + payload.user_id.clone().into(), + payload.r#type.into(), + serde_json::to_string(&payload.data).unwrap().into(), + payload.favorite.into(), + payload.folder_id.clone().into(), + payload.created_at.clone().into(), + payload.updated_at.clone().into(), + ])? + .run() + .await?; + + Ok(Json(payload)) +} diff --git a/src/handlers/config.rs b/src/handlers/config.rs new file mode 100644 index 0000000..bf16671 --- /dev/null +++ b/src/handlers/config.rs @@ -0,0 +1,55 @@ +use axum::Json; +use serde_json::{json, Value}; + +#[worker::send] +pub async fn config() -> Json { + // let domain = crate::CONFIG.domain(); + // Official available feature flags can be found here: + // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103 + // Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12 + // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22 + // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 + // let mut feature_states = + // parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags()); + // feature_states.insert("duo-redirect".to_string(), true); + // feature_states.insert("email-verification".to_string(), true); + // feature_states.insert("unauth-ui-refresh".to_string(), true); + // feature_states.insert("enable-pm-flight-recorder".to_string(), true); + // feature_states.insert("mobile-error-reporting".to_string(), true); + + let domain = "https://warden-worker.deepgauravraj.workers.dev"; + Json(json!({ + // Note: The clients use this version to handle backwards compatibility concerns + // This means they expect a version that closely matches the Bitwarden server version + // We should make sure that we keep this updated when we support the new server features + // Version history: + // - Individual cipher key encryption: 2024.2.0 + "version": "2024.7.0", + "gitHash": "25cf6119-dirty", + "server": { + "name": "Vaultwarden", + "url": "https://github.com/dani-garcia/vaultwarden" + }, + "settings": { + "disableUserRegistration": true, + }, + "environment": { + "vault": domain, + "api": format!("{domain}/api"), + "identity": format!("{domain}/identity"), + "notifications": format!("{domain}/notifications"), + "sso": format!("{domain}/sso"), + "cloudRegion": null, + }, + // Bitwarden uses this for the self-hosted servers to indicate the default push technology + "push": { + "pushTechnology": 0, + "vapidPublicKey": null + }, + "featureStates": { + // "duo-redirect": true, + // "flexible-collections-v-1": false + }, + "object": "config", + })) +} diff --git a/src/handlers/identity.rs b/src/handlers/identity.rs new file mode 100644 index 0000000..7ac30da --- /dev/null +++ b/src/handlers/identity.rs @@ -0,0 +1,127 @@ +use axum::{extract::State, Form, Json}; +use chrono::{Duration, Utc}; +use constant_time_eq::constant_time_eq; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::sync::Arc; +use worker::{D1Database, Env}; + +use crate::{auth::Claims, db, error::AppError, models::user::User}; + +#[derive(Debug, Deserialize)] +pub struct TokenRequest { + grant_type: String, + username: String, + password: Option, // This is the masterPasswordHash + // Other fields like scope, client_id, device info are ignored for this basic implementation +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct TokenResponse { + #[serde(rename = "access_token")] + access_token: String, + #[serde(rename = "expires_in")] + expires_in: i64, + #[serde(rename = "token_type")] + token_type: String, + #[serde(rename = "refresh_token")] + refresh_token: String, + #[serde(rename = "Key")] + key: String, + #[serde(rename = "PrivateKey")] + private_key: String, + #[serde(rename = "Kdf")] + kdf: i32, + #[serde(rename = "ResetMasterPassword")] + reset_master_password: bool, + #[serde(rename = "ForcePasswordReset")] + force_password_reset: bool, + #[serde(rename = "UserDecryptionOptions")] + user_decryption_options: UserDecryptionOptions, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UserDecryptionOptions { + pub has_master_password: bool, + pub object: String, +} + +#[worker::send] +pub async fn token( + State(env): State>, + Form(payload): Form, +) -> Result, AppError> { + log::info!("Token payload {payload:?}"); + if payload.grant_type != "password" { + return Err(AppError::BadRequest("Unsupported grant_type".to_string())); + } + let password_hash = payload + .password + .ok_or_else(|| AppError::BadRequest("Missing password".to_string()))?; + + let db = db::get_db(&env)?; + + log::info!("Lookup user"); + let user: Value = db + .prepare("SELECT * FROM users WHERE email = ?1") + .bind(&[payload.username.to_lowercase().into()])? + .first(None) + .await + .map_err(|_| AppError::Unauthorized("Invalid credentials".to_string()))? + .ok_or_else(|| AppError::Unauthorized("Invalid credentials".to_string()))?; + log::info!("User is {user:?}"); + let user: User = serde_json::from_value(user).map_err(|_| AppError::Internal)?; + // Securely compare the provided hash with the stored hash + if !constant_time_eq( + user.master_password_hash.as_bytes(), + password_hash.as_bytes(), + ) { + log::info!("Password is incorrect"); + return Err(AppError::Unauthorized("Invalid credentials".to_string())); + } + log::info!("Password matched"); + + // Create JWT claims + let now = Utc::now(); + let expires_in = Duration::hours(1); + let exp = (now + expires_in).timestamp() as usize; + + let claims = Claims { + sub: user.id.clone(), + exp, + nbf: now.timestamp() as usize, + premium: true, + name: user.name.unwrap_or("User".to_string()), + email: user.email, + email_verified: true, + amr: vec!["Application".into()], + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret("a-very-secure-secret-key-that-should-be-in-env".as_ref()), + )?; + + // In a real implementation, the refresh token would be securely generated and stored + let refresh_token = "static-refresh-token-for-this-example".to_string(); + + Ok(Json(TokenResponse { + access_token: token, + expires_in: expires_in.num_seconds(), + token_type: "Bearer".to_string(), + refresh_token, + key: user.key, + private_key: user.private_key, + kdf: user.kdf_type, + force_password_reset: false, + reset_master_password: false, + user_decryption_options: UserDecryptionOptions { + has_master_password: true, + object: "userDecryptionOptions".to_string(), + }, + })) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..35e7ec4 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,5 @@ +pub mod accounts; +pub mod ciphers; +pub mod config; +pub mod identity; +pub mod sync; diff --git a/src/handlers/sync.rs b/src/handlers/sync.rs new file mode 100644 index 0000000..3d1861e --- /dev/null +++ b/src/handlers/sync.rs @@ -0,0 +1,79 @@ +use axum::{extract::State, Json}; +use std::sync::Arc; +use worker::Env; + +use crate::{ + auth::Claims, + db, + error::AppError, + models::{ + cipher::Cipher, + folder::Folder, + sync::{Profile, SyncResponse}, + user::User, + }, +}; + +#[worker::send] +pub async fn get_sync_data( + claims: Claims, + State(env): State>, +) -> Result, AppError> { + let user_id = claims.sub; + let db = db::get_db(&env)?; + + // Fetch profile + let user: User = db + .prepare("SELECT * FROM users WHERE id = ?1") + .bind(&[user_id.clone().into()])? + .first(None) + .await? + .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; + + // Fetch folders + let folders: Vec = db + .prepare("SELECT * FROM folders WHERE user_id = ?1") + .bind(&[user_id.clone().into()])? + .all() + .await? + .results()?; + + // Fetch ciphers + let ciphers: Vec = db + .prepare("SELECT * FROM ciphers WHERE user_id = ?1") + .bind(&[user_id.clone().into()])? + .all() + .await? + .results()?; + + let time = chrono::DateTime::parse_from_rfc3339(&user.created_at) + .map_err(|_| AppError::Internal)? + .to_rfc3339_opts(chrono::SecondsFormat::Micros, true); + let profile = Profile { + id: user.id, + name: user.name, + email: user.email, + master_password_hint: user.master_password_hint, + security_stamp: user.security_stamp, + object: "profile".to_string(), + premium: true, + premium_from_organization: false, + email_verified: true, + force_password_reset: false, + two_factor_enabled: false, + uses_key_connector: false, + creation_date: time, + key: user.key, + private_key: user.private_key, + }; + + let response = SyncResponse { + profile, + folders, + ciphers, + domains: serde_json::Value::Null, // Ignored for basic implementation + object: "sync".to_string(), + }; + + Ok(Json(response)) +} diff --git a/src/lib.rs b/src/lib.rs index 08a2fc2..f0ff213 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,33 @@ -use axum::{routing::get, Router}; +use axum::Router; +use tower_http::cors::{Any, CorsLayer}; use tower_service::Service; use worker::*; -fn router() -> Router { - Router::new().route("/", get(root)) -} +mod auth; +mod crypto; +mod db; +mod error; +mod handlers; +mod models; +mod router; #[event(fetch)] -async fn fetch( +pub async fn main( req: HttpRequest, - _env: Env, + env: Env, _ctx: Context, ) -> Result> { + // Set up logging console_error_panic_hook::set_once(); - Ok(router().call(req).await?) -} + console_log::init_with_level(log::Level::Debug); -pub async fn root() -> &'static str { - "Hello Axum!" + // Allow all origins for CORS, which is typical for a public API like Bitwarden's. + let cors = CorsLayer::new() + .allow_methods(Any) + .allow_headers(Any) + .allow_origin(Any); + + let mut app = router::api_router(env).layer(cors); + + Ok(app.call(req).await?) } diff --git a/src/models/cipher.rs b/src/models/cipher.rs new file mode 100644 index 0000000..a766697 --- /dev/null +++ b/src/models/cipher.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Cipher { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub organization_id: Option, + #[serde(rename = "type")] + pub r#type: i32, + // Store all other fields as a raw JSON value. + // The server doesn't need to understand the encrypted contents. + pub data: Value, + pub favorite: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub folder_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub created_at: String, + pub updated_at: String, +} diff --git a/src/models/folder.rs b/src/models/folder.rs new file mode 100644 index 0000000..eaf64ba --- /dev/null +++ b/src/models/folder.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Folder { + pub id: String, + pub user_id: String, + // The name is encrypted client-side + pub name: String, + pub created_at: String, + pub updated_at: String, +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..a5a3a48 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,4 @@ +pub mod cipher; +pub mod folder; +pub mod sync; +pub mod user; diff --git a/src/models/sync.rs b/src/models/sync.rs new file mode 100644 index 0000000..9f4ac94 --- /dev/null +++ b/src/models/sync.rs @@ -0,0 +1,46 @@ +use super::{cipher::Cipher, folder::Folder, user::User}; +use serde::Serialize; +use serde_json::Value; + +#[derive(Debug, Serialize)] +pub struct Profile { + pub name: Option, + pub email: String, + pub id: String, + #[serde(rename = "masterPasswordHint")] + pub master_password_hint: Option, + #[serde(rename = "securityStamp")] + pub security_stamp: String, + #[serde(rename = "Object")] + pub object: String, + #[serde(rename = "premiumFromOrganization")] + pub premium_from_organization: bool, + #[serde(rename = "forcePasswordReset")] + pub force_password_reset: bool, + #[serde(rename = "emailVerified")] + pub email_verified: bool, + #[serde(rename = "twoFactorEnabled")] + pub two_factor_enabled: bool, + pub premium: bool, + #[serde(rename = "usesKeyConnector")] + pub uses_key_connector: bool, + #[serde(rename = "creationDate")] + pub creation_date: String, + #[serde(rename = "privateKey")] + pub private_key: String, + pub key: String, +} + +#[derive(Debug, Serialize)] +pub struct SyncResponse { + #[serde(rename = "Profile")] + pub profile: Profile, + #[serde(rename = "Folders")] + pub folders: Vec, + #[serde(rename = "Ciphers")] + pub ciphers: Vec, + #[serde(rename = "Domains")] + pub domains: Value, + #[serde(rename = "Object")] + pub object: String, +} diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..35ce6fc --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,76 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: String, + pub name: Option, + pub email: String, + #[serde(with = "bool_from_int")] + pub email_verified: bool, + pub master_password_hash: String, + pub master_password_hint: Option, + pub key: String, + pub private_key: String, + pub public_key: String, + pub kdf_type: i32, + pub kdf_iterations: i32, + pub security_stamp: String, + pub created_at: String, + pub updated_at: String, +} + +mod bool_from_int { + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = i64::deserialize(deserializer)?; + match value { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(serde::de::Error::custom("expected integer 0 or 1")), + } + } + + pub fn serialize(value: &bool, serializer: S) -> Result + where + S: Serializer, + { + if *value { + serializer.serialize_i64(1) + } else { + serializer.serialize_i64(0) + } + } +} + +// For /accounts/prelogin response +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PreloginResponse { + pub kdf: i32, + pub kdf_iterations: i32, +} + +// For /accounts/register request +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisterRequest { + pub name: Option, + pub email: String, + pub master_password_hash: String, + pub master_password_hint: Option, + pub user_symmetric_key: String, + pub user_asymmetric_keys: KeyData, + pub kdf: i32, + pub kdf_iterations: i32, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KeyData { + pub public_key: String, + pub encrypted_private_key: String, +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..4549314 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,24 @@ +use axum::{ + routing::{get, post}, + Router, +}; +use std::sync::Arc; +use worker::Env; + +use crate::handlers::{accounts, ciphers, config, identity, sync}; + +pub fn api_router(env: Env) -> Router { + let app_state = Arc::new(env); + + Router::new() + // Identity/Auth routes + .route("/identity/accounts/prelogin", post(accounts::prelogin)) + .route("/identity/accounts/register", post(accounts::register)) + .route("/identity/connect/token", post(identity::token)) + // Main data sync route + .route("/api/sync", get(sync::get_sync_data)) + // Ciphers CRUD + .route("/api/ciphers/create", post(ciphers::create_cipher)) + .route("/api/config", get(config::config)) + .with_state(app_state) +} diff --git a/wrangler.toml b/wrangler.toml index 45dac11..21f8172 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -3,4 +3,9 @@ main = "build/index.js" compatibility_date = "2025-09-19" [build] -command = "cargo install -q worker-build && worker-build --release" \ No newline at end of file +command = "cargo install -q worker-build && worker-build --release" + +[[d1_databases]] +binding = "vault1" +database_name = "vault1" +database_id = "${D1_DATABASE_ID}"