Privacy-preserving location sharing with end-to-end encryption coord.is
at master 174 lines 5.1 kB view raw
1use axum::{ 2 extract::State, 3 http::StatusCode, 4 response::Html, 5 routing::{get, post, put}, 6 Json, Router, 7}; 8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 9use dashmap::DashMap; 10use ed25519_dalek::{Signature, Verifier, VerifyingKey}; 11use serde::{Deserialize, Serialize}; 12use std::{collections::HashMap, sync::Arc, time::SystemTime}; 13 14const MAX_BLOB_SIZE: usize = 64 * 1024; 15const MAX_TIMESTAMP_AGE_SECS: u64 = 300; 16 17#[derive(Clone, Serialize)] 18struct StoredLocation { 19 #[serde(serialize_with = "as_base64")] 20 blob: Vec<u8>, 21 updated: u64, 22} 23 24fn as_base64<S: serde::Serializer>(bytes: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> { 25 s.serialize_str(&URL_SAFE_NO_PAD.encode(bytes)) 26} 27 28type PostResponse = HashMap<String, Option<StoredLocation>>; 29 30type LocationStore = Arc<DashMap<[u8; 32], StoredLocation>>; 31 32#[derive(Deserialize)] 33struct PutRequest { 34 pubkey: String, 35 timestamp: u64, 36 blob: String, 37 signature: String, 38} 39 40#[derive(Deserialize)] 41struct PostRequest { 42 ids: Vec<String>, 43} 44 45 46const VERSION: &str = env!("CARGO_PKG_VERSION"); 47 48pub fn app() -> Router { 49 let store: LocationStore = Arc::new(DashMap::new()); 50 Router::new() 51 .route("/api/version", get(version_info)) 52 .route("/api/privacy", get(privacy)) 53 .route("/api/location", put(put_location)) 54 .route("/api/location", post(post_location)) 55 .with_state(store) 56} 57 58async fn version_info() -> Json<serde_json::Value> { 59 Json(serde_json::json!({ 60 "name": "coords", 61 "version": VERSION 62 })) 63} 64 65async fn privacy() -> Html<&'static str> { 66 Html(include_str!("privacy.html")) 67} 68 69async fn put_location( 70 State(store): State<LocationStore>, 71 Json(req): Json<PutRequest>, 72) -> Result<StatusCode, (StatusCode, &'static str)> { 73 let pubkey_prefix = req.pubkey.chars().take(8).collect::<String>(); 74 println!("[PUT /api/location] pubkey={}..., blob_size={}", pubkey_prefix, req.blob.len()); 75 76 // Decode pubkey 77 let pubkey_bytes: [u8; 32] = URL_SAFE_NO_PAD 78 .decode(&req.pubkey) 79 .map_err(|_| (StatusCode::BAD_REQUEST, "invalid pubkey encoding"))? 80 .try_into() 81 .map_err(|_| (StatusCode::BAD_REQUEST, "invalid pubkey length"))?; 82 83 let verifying_key = VerifyingKey::from_bytes(&pubkey_bytes) 84 .map_err(|_| (StatusCode::BAD_REQUEST, "invalid pubkey"))?; 85 86 // Decode blob 87 let blob = URL_SAFE_NO_PAD 88 .decode(&req.blob) 89 .map_err(|_| (StatusCode::BAD_REQUEST, "invalid blob encoding"))?; 90 91 if blob.len() > MAX_BLOB_SIZE { 92 return Err((StatusCode::PAYLOAD_TOO_LARGE, "blob exceeds 64KB")); 93 } 94 95 // Decode signature 96 let sig_bytes: [u8; 64] = URL_SAFE_NO_PAD 97 .decode(&req.signature) 98 .map_err(|_| (StatusCode::BAD_REQUEST, "invalid signature encoding"))? 99 .try_into() 100 .map_err(|_| (StatusCode::BAD_REQUEST, "invalid signature length"))?; 101 102 let signature = Signature::from_bytes(&sig_bytes); 103 104 // Verify timestamp 105 let now = SystemTime::now() 106 .duration_since(SystemTime::UNIX_EPOCH) 107 .unwrap() 108 .as_secs(); 109 110 if now.saturating_sub(req.timestamp) > MAX_TIMESTAMP_AGE_SECS { 111 return Err((StatusCode::UNAUTHORIZED, "timestamp too old")); 112 } 113 114 // Verify signature over blob || timestamp 115 let mut message = blob.clone(); 116 message.extend_from_slice(&req.timestamp.to_be_bytes()); 117 118 verifying_key 119 .verify(&message, &signature) 120 .map_err(|_| (StatusCode::UNAUTHORIZED, "invalid signature"))?; 121 122 // Store 123 store.insert( 124 pubkey_bytes, 125 StoredLocation { 126 blob, 127 updated: now, 128 }, 129 ); 130 131 println!("[PUT /api/location] stored successfully for {}...", pubkey_prefix); 132 Ok(StatusCode::NO_CONTENT) 133} 134 135async fn post_location( 136 State(store): State<LocationStore>, 137 Json(req): Json<PostRequest>, 138) -> Result<Json<PostResponse>, (StatusCode, &'static str)> { 139 println!("[POST /api/location] requesting {} ids", req.ids.len()); 140 for id in &req.ids { 141 let prefix = id.chars().take(8).collect::<String>(); 142 println!("[POST /api/location] - {}...", prefix); 143 } 144 145 if req.ids.len() > 50 { 146 return Err((StatusCode::BAD_REQUEST, "max 50 ids per request")); 147 } 148 149 let mut results = PostResponse::new(); 150 151 for id in req.ids { 152 let pubkey_bytes: Result<[u8; 32], _> = URL_SAFE_NO_PAD 153 .decode(&id) 154 .map_err(|_| ()) 155 .and_then(|b| b.try_into().map_err(|_| ())); 156 157 let id_prefix = id.chars().take(8).collect::<String>(); 158 let value = match pubkey_bytes { 159 Ok(key) => { 160 let found = store.get(&key).map(|entry| entry.clone()); 161 println!("[POST /api/location] {}... -> {}", id_prefix, if found.is_some() { "FOUND" } else { "NOT FOUND" }); 162 found 163 }, 164 Err(_) => { 165 println!("[POST /api/location] {}... -> INVALID KEY", id_prefix); 166 None 167 }, 168 }; 169 170 results.insert(id, value); 171 } 172 173 Ok(Json(results)) 174}