diff --git a/docs/manifest-v1.md b/docs/manifest-v1.md index 95a1bc6..4adb788 100644 --- a/docs/manifest-v1.md +++ b/docs/manifest-v1.md @@ -1,8 +1,21 @@ # ShaCraft manifest v1 -Каждая игровая сборка описывается одним JSON-документом. Лаунчер принимает -документ только после проверки подписи релизного ключа и затем валидирует эту -схему до начала загрузки. +Каждая игровая сборка описывается одним JSON-документом. API передаёт его в +Ed25519-конверте: `payload` — base64 от UTF-8 JSON, `signature` — подпись +точно этих байтов. Лаунчер принимает документ только после проверки +встроенного публичного релизного ключа и затем валидирует эту схему до начала +загрузки. + +```json +{ + "schemaVersion": 1, + "keyId": "2026-09-06", + "payload": "eyJkaXNwbGF5TmFtZSI6Ii4uLiJ9", + "signature": "base64-ed25519-signature" +} +``` + +После проверки и декодирования `payload` имеет следующий вид: ```json { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a106c28..e8bcf28 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -94,6 +94,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 = "bit-set" version = "0.8.0" @@ -323,7 +329,7 @@ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures 0.3.1", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -348,6 +354,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cookie" version = "0.18.2" @@ -489,6 +501,33 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[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.119", +] + [[package]] name = "darling" version = "0.23.0" @@ -565,6 +604,16 @@ dependencies = [ "thiserror 2.0.20", ] +[[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 = "deranged" version = "0.5.8" @@ -738,6 +787,30 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.11" @@ -790,6 +863,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 = "field-offset" version = "0.3.6" @@ -1081,7 +1160,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -2279,6 +2358,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.34" @@ -2516,7 +2605,16 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -2531,7 +2629,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -3040,6 +3138,8 @@ dependencies = [ name = "shacraft-launcher" version = "0.1.0" dependencies = [ + "base64 0.22.1", + "ed25519-dalek", "reqwest 0.12.28", "serde", "serde_json", @@ -3055,6 +3155,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[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 = "simd-adler32" version = "0.3.10" @@ -3137,6 +3246,16 @@ dependencies = [ "system-deps", ] +[[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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index abc1d59..bffc7cb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,6 +16,8 @@ tauri-build = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" +base64 = "0.22" +ed25519-dalek = "2" tauri = { version = "2", features = [] } url = "2" reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs index aa02500..ebfe07d 100644 --- a/src-tauri/src/remote.rs +++ b/src-tauri/src/remote.rs @@ -1,9 +1,22 @@ use crate::manifest::{self, Manifest}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use reqwest::{blocking::Client, redirect::Policy}; +use serde::Deserialize; use std::{fmt, time::Duration}; const AERONAUTICS_MANIFEST: &str = - "https://shacraft.ru/api/launcher/v2/profiles/aeronautics/manifest"; + "https://shacraft.ru/api/launcher/v2/profiles/aeronautics/signed-manifest"; +const MANIFEST_PUBLIC_KEY: &str = "2S3FRdZj4Xw5nJpZ3IhqVITBg3nTH9AtGSo1Ew9+qVQ="; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SignedManifest { + schema_version: u32, + key_id: String, + payload: String, + signature: String, +} #[derive(Debug)] pub enum RemoteError { @@ -11,6 +24,7 @@ pub enum RemoteError { Network(reqwest::Error), Status(reqwest::StatusCode), TooLarge, + InvalidSignature, InvalidManifest(manifest::ManifestError), } @@ -21,6 +35,7 @@ impl fmt::Display for RemoteError { Self::Network(error) => write!(formatter, "Cannot load ShaCraft manifest: {error}"), Self::Status(status) => write!(formatter, "ShaCraft manifest request failed: {status}"), Self::TooLarge => formatter.write_str("ShaCraft manifest is too large"), + Self::InvalidSignature => formatter.write_str("ShaCraft manifest signature is invalid"), Self::InvalidManifest(error) => write!(formatter, "ShaCraft manifest is invalid: {error}"), } } @@ -44,5 +59,18 @@ pub fn fetch_manifest(profile_id: &str) -> Result { return Err(RemoteError::TooLarge); } let source = response.text().map_err(RemoteError::Network)?; - manifest::validate_json(&source).map_err(RemoteError::InvalidManifest) + let envelope = serde_json::from_str::(&source) + .map_err(|_| RemoteError::InvalidSignature)?; + if envelope.schema_version != 1 || envelope.key_id != "2026-09-06" { + return Err(RemoteError::InvalidSignature); + } + let payload = STANDARD.decode(envelope.payload).map_err(|_| RemoteError::InvalidSignature)?; + let signature_bytes = STANDARD.decode(envelope.signature).map_err(|_| RemoteError::InvalidSignature)?; + let public_key_bytes = STANDARD.decode(MANIFEST_PUBLIC_KEY).expect("embedded public key must be valid"); + let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().expect("embedded public key must be 32 bytes")) + .expect("embedded public key must be valid"); + let signature = Signature::from_slice(&signature_bytes).map_err(|_| RemoteError::InvalidSignature)?; + public_key.verify(&payload, &signature).map_err(|_| RemoteError::InvalidSignature)?; + let payload = String::from_utf8(payload).map_err(|_| RemoteError::InvalidSignature)?; + manifest::validate_json(&payload).map_err(RemoteError::InvalidManifest) }