From c51c0f2e8342639821448dfdf43452c92cc42570 Mon Sep 17 00:00:00 2001 From: Alois Date: Fri, 3 Jul 2026 00:36:15 +0200 Subject: [PATCH] (feat): improvements to the slow --- Cargo.lock | 1 + Cargo.toml | 3 +- README.md | 2 +- example/README.md | 2 + example/backend/Cargo.lock | 1 + example/backend/src/bin/generate_keys.rs | 1 + src/lib.rs | 87 ++++++++++++++++++------ 7 files changed, 76 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d0eb0b..52b83eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1888,6 +1888,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sha2 0.10.9", "thiserror 2.0.18", "tokio", "url", diff --git a/Cargo.toml b/Cargo.toml index 792ac7b..e755e8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "tauth-sdk" version = "0.1.0" edition = "2024" -license = "UNLICENSED" +license = "MIT" [lib] name = "tauth_sdk" @@ -14,6 +14,7 @@ mtp = { git = "https://git.methanium.net/methanium/mtp.git", features = ["client reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" thiserror = "2" tokio = { version = "1", features = ["rt", "time"] } url = "2" diff --git a/README.md b/README.md index 7e4242d..fd42762 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ let client = TAuthClient::from_config( "https://my.cool.app/tauth/callback", std::env::var("TAUTH_PRIVATE_KEY")?, ) - .frontend_url("https://tauth.tensamin.net/login"), + .frontend_url("https://app.tensamin.net"), )?; ``` diff --git a/example/README.md b/example/README.md index 990edad..2ebb57f 100644 --- a/example/README.md +++ b/example/README.md @@ -30,6 +30,8 @@ Set `TAUTH_PRIVATE_KEY` in `backend/.env`. Generate keys with: pnpm --filter @tauth-example/backend exec cargo run --bin generate_keys ``` +Publish the generated `TAUTH_PUBLIC_KEY_SHA256` value in the `tauth.` TXT record. TAuth receives the full public key during authorization and verifies it against that hash. + For local callback testing, `TAUTH_REDIRECT_URL` must be reachable by TAuth. Use a tunnel if needed. ## Run diff --git a/example/backend/Cargo.lock b/example/backend/Cargo.lock index a3274a8..4a5b7ee 100644 --- a/example/backend/Cargo.lock +++ b/example/backend/Cargo.lock @@ -1990,6 +1990,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sha2 0.10.9", "thiserror 2.0.18", "tokio", "url", diff --git a/example/backend/src/bin/generate_keys.rs b/example/backend/src/bin/generate_keys.rs index fbe7120..432f952 100644 --- a/example/backend/src/bin/generate_keys.rs +++ b/example/backend/src/bin/generate_keys.rs @@ -3,4 +3,5 @@ fn main() { println!("TAUTH_PRIVATE_KEY={}", keys.private); println!("TAUTH_PUBLIC_KEY={}", keys.public); + println!("TAUTH_PUBLIC_KEY_SHA256={}", keys.public_hash); } diff --git a/src/lib.rs b/src/lib.rs index 0786fa6..4514353 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,10 +5,11 @@ use mtp::crypto::{Keyring, PublicKeyBundle}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Value; +use sha2::{Digest, Sha256}; use std::time::Duration; use url::Url; -const DEFAULT_FRONTEND_URL: &str = "https://tauth.tensamin.net/login"; +const DEFAULT_FRONTEND_URL: &str = "https://app.tensamin.net"; const OMEGA_API: &str = "https://omega.tensamin.net/api"; #[derive(Debug, thiserror::Error)] @@ -39,6 +40,7 @@ pub enum TAuthError { pub struct TAuthKeyPair { pub private: String, pub public: String, + pub public_hash: String, } pub type TAuthKeys = TAuthKeyPair; @@ -168,7 +170,11 @@ impl TAuthClient { redirect_url: impl AsRef, private_key: impl AsRef, ) -> Result { - Self::from_config(TAuthConfig::new(identifier, redirect_url.as_ref(), private_key.as_ref())) + Self::from_config(TAuthConfig::new( + identifier, + redirect_url.as_ref(), + private_key.as_ref(), + )) } pub fn with_frontend_url( @@ -205,6 +211,10 @@ impl TAuthClient { encode_bytes(&self.public_key_bundle.as_bytes()) } + pub fn public_key_hash(&self) -> String { + sha256_hex(self.public_key().as_bytes()) + } + pub fn redirect_url(&self) -> &Url { &self.redirect_url } @@ -217,6 +227,7 @@ impl TAuthClient { TAuthKeyPair { private: encode_bytes(&self.keyring.to_bytes()), public: self.public_key(), + public_hash: self.public_key_hash(), } } @@ -238,7 +249,10 @@ impl TAuthClient { link.to_string() } - pub fn parse_callback(&self, callback_url: impl AsRef) -> Result { + pub fn parse_callback( + &self, + callback_url: impl AsRef, + ) -> Result { parse_callback(callback_url) } @@ -314,8 +328,13 @@ impl TAuthClient { host_public_key: impl AsRef, ) -> Result { let omikron = fetch_omikron(user_id).await?; - self.connect_or_register_to_omikron(user_id, session_id, omikron.ip_address, host_public_key) - .await + self.connect_or_register_to_omikron( + user_id, + session_id, + omikron.ip_address, + host_public_key, + ) + .await } pub async fn connect_or_register_to_omikron( @@ -329,13 +348,15 @@ impl TAuthClient { let config = ClientConfig::new(omikron_url.into()) .with_description(format!("tauth:{}:{user_id}", self.identifier)) .with_auth_timeout(self.auth_timeout); - Ok(MTPClient::auth_connect_or_register( - config, - session_id, - &self.keyring, - &host_public_key, + Ok( + MTPClient::auth_connect_or_register( + config, + session_id, + &self.keyring, + &host_public_key, + ) + .await?, ) - .await?) } pub async fn connect_session( @@ -359,9 +380,11 @@ impl TAuthClient { pub fn generate_key_pair() -> TAuthKeyPair { let keyring = Keyring::generate(); + let public = encode_bytes(&keyring.public_key_bundle().as_bytes()); TAuthKeyPair { private: encode_bytes(&keyring.to_bytes()), - public: encode_bytes(&keyring.public_key_bundle().as_bytes()), + public_hash: sha256_hex(public.as_bytes()), + public, } } @@ -440,6 +463,13 @@ fn decode_public_key_bundle(value: &str) -> Result .map_err(|error| TAuthError::Key(error.to_string())) } +fn sha256_hex(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + async fn fetch_omikron(user_id: u64) -> Result { let url = format!("{OMEGA_API}/get/omikron/{user_id}"); Ok(reqwest::get(url).await?.error_for_status()?.json().await?) @@ -456,6 +486,7 @@ mod tests { let public = decode_public_key_bundle(&keys.public).unwrap(); assert_eq!(public.as_bytes(), keyring.public_key_bundle().as_bytes()); + assert_eq!(keys.public_hash, sha256_hex(keys.public.as_bytes())); } #[test] @@ -470,9 +501,28 @@ mod tests { .unwrap(); let link = Url::parse(&client.generate_link(Some("abc123"))).unwrap(); - assert_eq!(link.query_pairs().find(|(key, _)| key == "identifier").unwrap().1, "test.app"); - assert_eq!(link.query_pairs().find(|(key, _)| key == "challenge").unwrap().1, "abc123"); - assert!(link.query_pairs().any(|(key, value)| key == "public_key" && !value.is_empty())); + assert_eq!( + link.query_pairs() + .find(|(key, _)| key == "identifier") + .unwrap() + .1, + "test.app" + ); + assert_eq!( + link.query_pairs() + .find(|(key, _)| key == "challenge") + .unwrap() + .1, + "abc123" + ); + assert!( + link.query_pairs() + .any(|(key, value)| key == "public_key" && !value.is_empty()) + ); + assert_eq!( + client.public_key_hash(), + sha256_hex(client.public_key().as_bytes()) + ); } #[test] @@ -491,10 +541,9 @@ mod tests { #[test] fn parses_snake_case_host_key_callback_url() { - let callback = parse_callback( - "https://app.example/callback?userId=42&sessionId=7&host_public_key=c", - ) - .unwrap(); + let callback = + parse_callback("https://app.example/callback?userId=42&sessionId=7&host_public_key=c") + .unwrap(); assert_eq!(callback.host_public_key.as_deref(), Some("c")); }