From eabc84c8e290163d25ea21f153ae811bf09fc23a Mon Sep 17 00:00:00 2001 From: core Date: Tue, 16 Jun 2026 18:20:15 -0400 Subject: [PATCH] feat: add basic save file authentication infrastructure --- Cargo.lock | 182 ++++++++++++++++++++++- crates/common/Cargo.toml | 11 ++ crates/common/src/lib.rs | 16 ++ crates/common/src/save/authentication.rs | 37 +++++ crates/common/src/save/error.rs | 13 ++ crates/common/src/save/mod.rs | 26 ++++ crates/common/src/save/v1.rs | 6 + 7 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 crates/common/Cargo.toml create mode 100644 crates/common/src/lib.rs create mode 100644 crates/common/src/save/authentication.rs create mode 100644 crates/common/src/save/error.rs create mode 100644 crates/common/src/save/mod.rs create mode 100644 crates/common/src/save/v1.rs diff --git a/Cargo.lock b/Cargo.lock index 4c8f682e4244a8a9d218f7c2cdc44e50ad885759..a5f9618957335f81cb85fdc9d154ad6e9d134cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,6 +372,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -465,6 +474,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bevy" version = "0.18.1" @@ -1210,7 +1225,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "spin", + "spin 0.10.0", "wasm-bindgen", "wasm-bindgen-futures", "web-time 1.1.0", @@ -1539,7 +1554,7 @@ dependencies = [ "crossbeam-queue", "derive_more", "futures-lite", - "heapless", + "heapless 0.9.2", "pin-project", ] @@ -2161,6 +2176,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.36" @@ -2474,6 +2495,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.14.4" @@ -2515,6 +2563,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -2782,12 +2840,49 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fdab65db9274e0168143841eb8f864a0a21f8b1b8d2ba6812bbe6024346e99e" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ena" version = "0.14.4" @@ -2923,6 +3018,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "file-id" version = "0.2.3" @@ -3463,6 +3564,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hash32" version = "0.3.1" @@ -3500,13 +3610,27 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heapless" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" dependencies = [ - "hash32", + "hash32 0.3.1", "portable-atomic", "stable_deref_trait", ] @@ -4930,6 +5054,16 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -5030,6 +5164,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" dependencies = [ "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless 0.7.17", "postcard-derive", "serde", ] @@ -5915,6 +6052,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "simba" version = "0.9.1" @@ -6069,6 +6215,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spin" version = "0.10.0" @@ -6087,6 +6242,16 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6131,6 +6296,17 @@ dependencies = [ "web-time 1.1.0", ] +[[package]] +name = "starkingdoms-common" +version = "0.1.0" +dependencies = [ + "base64", + "ed25519-dalek", + "postcard", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..5c1745ede8bfa25ce328281226de4c177e963bad --- /dev/null +++ b/crates/common/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "starkingdoms-common" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1", features = ["derive"] } +postcard = { version = "1", features = ["alloc"] } +ed25519-dalek = { version = "2", features = ["serde"] } +base64 = { version = "0.22" } +thiserror = "2" \ No newline at end of file diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..09d8c032467b76e9ab97e09b854f1baf14401cb8 --- /dev/null +++ b/crates/common/src/lib.rs @@ -0,0 +1,16 @@ +pub mod save; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/crates/common/src/save/authentication.rs b/crates/common/src/save/authentication.rs new file mode 100644 index 0000000000000000000000000000000000000000..5f1a92f877083819562ed484504e0d8e6578fb16 --- /dev/null +++ b/crates/common/src/save/authentication.rs @@ -0,0 +1,37 @@ +use base64::Engine; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use serde::{Deserialize, Serialize}; +use crate::save::error::SaveError; + +#[derive(Serialize, Deserialize)] +pub struct SignedSavefile { + pub authenticating_server: AuthenticatingServer, + pub save_data: crate::save::Savefile, + pub signature: Signature +} + +#[derive(Serialize, Deserialize)] +pub struct AuthenticatingServer { + pub server: String, + pub server_build_id: String, + pub server_public_key: VerifyingKey, +} + +impl SignedSavefile { + pub fn encoded_save_data(&self) -> Result, SaveError> { + Ok(postcard::to_allocvec(&self.save_data)?) + } + pub fn is_valid(&self) -> Result { + let encoded_savefile = self.encoded_save_data()?; + Ok(self.authenticating_server.server_public_key.verify(&encoded_savefile, &self.signature).is_ok()) + } +} + +pub fn encode_public_key(public_key: &VerifyingKey) -> String { + base64::engine::general_purpose::URL_SAFE.encode(public_key) +} +pub fn decode_public_key(public_key: &str) -> Result { + let bytes = base64::engine::general_purpose::URL_SAFE.decode(public_key)?; + let key: [u8; 32] = bytes.try_into().ok().ok_or(SaveError::InvalidKeyLength(public_key.len()))?; + VerifyingKey::from_bytes(&key).ok().ok_or(SaveError::Ed25519Error) +} \ No newline at end of file diff --git a/crates/common/src/save/error.rs b/crates/common/src/save/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..e461b1daa8f3ab65b86f3f5187e4aeede2ad0e40 --- /dev/null +++ b/crates/common/src/save/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Clone, Debug, Error)] +pub enum SaveError { + #[error("postcard ser/de error: {0}")] + PostcardError(#[from] postcard::Error), + #[error("invalid base64: {0}")] + Base64(#[from] base64::DecodeError), + #[error("incorrect key length: should be 32, got {0}")] + InvalidKeyLength(usize), + #[error("ed25519 error")] + Ed25519Error +} \ No newline at end of file diff --git a/crates/common/src/save/mod.rs b/crates/common/src/save/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..8cce9bc7e0e1d61d01204579d960425b2c69be8c --- /dev/null +++ b/crates/common/src/save/mod.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +pub mod authentication; +pub mod v1; +pub mod error; + +#[derive(Serialize, Deserialize)] +pub enum Savefile { + V1(v1::Savefile), +} + +pub trait SaveContainer { + type LatestFormat; + + fn as_latest(&self) -> &Self::LatestFormat; +} + +impl SaveContainer for Savefile { + type LatestFormat = v1::Savefile; + + fn as_latest(&self) -> &Self::LatestFormat { + match self { + Savefile::V1(s) => s, + } + } +} \ No newline at end of file diff --git a/crates/common/src/save/v1.rs b/crates/common/src/save/v1.rs new file mode 100644 index 0000000000000000000000000000000000000000..0073730f9985af96e4c7edd3debc8ce77a34c071 --- /dev/null +++ b/crates/common/src/save/v1.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Savefile { + +} \ No newline at end of file