diff --git a/Cargo.lock b/Cargo.lock index e6323fd..f95ace8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -19,9 +30,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "async-trait" @@ -131,6 +142,26 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -139,9 +170,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cfg-if" @@ -151,15 +182,25 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", "rand_core", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + [[package]] name = "cmov" version = "0.5.4" @@ -181,6 +222,12 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -481,23 +528,21 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", "rand_core", - "wasip2", - "wasip3", ] [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -624,10 +669,11 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ + "bytemuck", "typenum", ] @@ -768,12 +814,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -803,8 +843,15 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", ] [[package]] @@ -822,12 +869,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -867,9 +908,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "matchit" @@ -920,12 +961,17 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" name = "nex-account" version = "0.1.3" dependencies = [ + "base64", + "bytemuck", "hmac", "log", "md-5", "prost", + "rand", "simplelog", + "spbr-common", "sqlx", + "thiserror", "tokio", "tonic", "tonic-prost", @@ -1149,9 +1195,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1164,9 +1210,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom", @@ -1236,12 +1282,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -1354,6 +1394,22 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spbr-common" +version = "0.1.0" +source = "sparse+https://crates.spbr.net/api/v1/crates/" +checksum = "04f4e4710d23bb730a95d00b853e304a4c1c9e86276792cd32a92057e9833452" +dependencies = [ + "aes", + "base64", + "bytemuck", + "hmac", + "hybrid-array", + "md-5", + "rand", + "thiserror", +] + [[package]] name = "spin" version = "0.9.8" @@ -1553,9 +1609,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -1623,9 +1679,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.49" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "libc", @@ -1645,9 +1701,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -1904,12 +1960,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "url" version = "2.5.8" @@ -1955,58 +2005,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -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]] -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]] name = "whoami" version = "2.1.2" @@ -2037,100 +2035,6 @@ dependencies = [ "windows-link", ] -[[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]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 232b02e..d5d107e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,13 @@ log = "0.4.32" simplelog = "0.12.2" hmac = "0.13.0" md-5 = "0.11.0" +spbr-common = { version = "0.1.0", registry = "spbr", features = ["crypto"] } +bytemuck = { version = "1.25.0", features = ["derive"] } +base64 = "0.22.1" +thiserror = "2.0.18" + +[dev-dependencies] +rand = "0.10.2" [build-dependencies] tonic-prost-build = "0.14.6" diff --git a/src/lib.rs b/src/lib.rs index 64f9fc2..df88594 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,19 @@ +use std::{ + env, result, + sync::LazyLock, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use base64::{DecodeSliceError, Engine, engine::general_purpose::STANDARD}; +use bytemuck::{Pod, Zeroable}; use hmac::{Hmac, KeyInit, Mac}; use md5::Md5; +use spbr_common::crypto::encsign::{ + self, EncSignKeypair, decrypt_and_check, encrypt_and_gen_signature_blocks, +}; +use thiserror::Error; + +use crate::Error::{FutureToken, KeypairUnset, PaddingNonzero, TokenExpired}; pub mod grpc; @@ -9,10 +23,12 @@ pub const GUEST_PID: PID = 100; pub const RDV_AUTHENTICATION_PID: PID = 1; pub const RDV_SECURE_PID: PID = 2; -pub type NexKey = [u8; 16]; +pub type NexKey = [u8; 0x10]; type HmacMd5 = Hmac; +const TOKEN_EXPIERY_TIME: Duration = Duration::from_hours(24); + pub fn derive_pid_hmac(pid: PID, key: &NexKey) -> [u8; 4] { let mut hmac = HmacMd5::new_from_slice(&key[..]).expect("infallible"); hmac.update(&pid.to_be_bytes()); @@ -20,3 +36,125 @@ pub fn derive_pid_hmac(pid: PID, key: &NexKey) -> [u8; 4] { .try_into() .expect("infallible") } + +pub static KEYPAIR: LazyLock> = + LazyLock::new(|| env::var("NEX_ACCOUNT_KEYPAIR").ok()?.parse().ok()); + +#[repr(C)] +#[derive(Pod, Clone, Copy, Zeroable)] +struct NexActToken { + pid: PID, + padding: u32, + timestamp: u64, + nex_key: NexKey, +} +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Base64(#[from] DecodeSliceError), + #[error("NEX_ACCOUNT_KEYPAIR has not been set")] + KeypairUnset, + #[error("signature decryption validation error occurred: {0}")] + EncSign(#[from] encsign::Error), + #[error("signature padding is not zero")] + PaddingNonzero, + #[error("token is apparently from the future")] + FutureToken, + #[error("token expired")] + TokenExpired, +} + +type Result = result::Result; + +/// Generate a nex-account token from a pid, and a key +/// +/// This function generates a nex-account token from the users pid and the key by using the +/// `NEX_ACCOUNT_KEYPAIR` environment variable as an external non function input. +/// +/// Returns `None` if `NEX_ACCOUNT_KEYPAIR` is not set or was invalid. +/// +/// In addition `NEX_ACCOUNT_KEYPAIR` may only be set once, BEFORE this function executes. +/// Any further changes to the environment variable of any kind are concidered undefined +/// behavior. +pub fn gen_nexact_token(pid: PID, nex_key: NexKey) -> Option> { + let keypair = KEYPAIR.as_ref()?; + // Size of the token data + size of signature + let mut data = [0; size_of::() + 0x10]; + let (token_data_segment, signature_segment) = data.split_at_mut(size_of::()); + // fill in the first bytes as the nexact token + *bytemuck::from_bytes_mut(token_data_segment) = NexActToken { + pid, + nex_key, + // as far as i am aware this conversion to u64 is valid till the + // system clock exceeds the heat death of the universe so we should + // be fine(unless i miscalculated) + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock is before unix epoch") + .as_millis() as u64, + padding: 0, + }; + let block_data = bytemuck::cast_slice_mut(token_data_segment); + signature_segment.copy_from_slice(&encrypt_and_gen_signature_blocks(block_data, keypair)[..]); + + Some(STANDARD.encode(data).into_boxed_str()) +} + +/// Decode a nex-account token from a pid, and a key +/// +/// This function decodes a nex-account token and gives back the users pid and the key by using the +/// `NEX_ACCOUNT_KEYPAIR` environment variable as an external non function input. +/// This function does NOT validate wether the nex key is correct, it only validates wether the +/// message has not expired, if it is valid and if it was ecrypted and signed correctly with the +/// keypair. +/// +/// In addition `NEX_ACCOUNT_KEYPAIR` may only be set once, BEFORE this function executes. +/// Any further changes to the environment variable of any kind are concidered undefined +/// behavior. +pub fn decode_nexact_token(data: &str) -> Result<(PID, NexKey)> { + let keypair = KEYPAIR.as_ref().ok_or(KeypairUnset)?; + // Size of the token data + size of signature + let mut raw_data = [0; size_of::() + 0x10]; + STANDARD.decode_slice(data, &mut raw_data[..])?; + let decoded_data = decrypt_and_check(&mut raw_data[..], keypair)?; + let token_data: &NexActToken = bytemuck::from_bytes(&decoded_data[..]); + if token_data.padding != 0 { + return Err(PaddingNonzero); + } + let creation_time = UNIX_EPOCH + Duration::from_millis(token_data.timestamp); + let time_since_creation = SystemTime::now() + .duration_since(creation_time) + .map_err(|_| FutureToken)?; + if time_since_creation > TOKEN_EXPIERY_TIME { + return Err(TokenExpired); + } + Ok((token_data.pid, token_data.nex_key)) +} + +#[cfg(test)] +mod test { + use std::env::set_var; + + use rand::random; + use spbr_common::crypto::encsign::EncSignKeypair; + + use crate::{NexKey, decode_nexact_token, gen_nexact_token}; + + #[test] + fn fuzz_token() { + unsafe { + set_var( + "NEX_ACCOUNT_KEYPAIR", + EncSignKeypair::new_random().to_string(), + ); + } + for _ in 0..10000 { + let pid: i32 = random(); + let key: NexKey = random(); + let token = gen_nexact_token(pid, key).unwrap(); + let (dec_pid, dec_key) = decode_nexact_token(&token).unwrap(); + assert_eq!(pid, dec_pid); + assert_eq!(key, dec_key); + } + } +}