Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption location-sharing privacy self-hosted federated

added formatting config and formatted code to follow it

+101 -137
+16
.editorconfig
··· 1 + root = true 2 + 3 + [*] 4 + charset = utf-8 5 + end_of_line = lf 6 + insert_final_newline = true 7 + trim_trailing_whitespace = true 8 + 9 + indent_style = tab 10 + indent_size = 4 11 + tab_width = 4 12 + max_line_length = 100 13 + 14 + 15 + [*.{html,htm}] 16 + max_line_length = 1000 # easier to know where you are
+5
.rustfmt.toml
··· 1 + # because rustfmt refuses to follow .editorconfig 2 + hard_tabs = true 3 + tab_spaces = 4 4 + max_width = 100 5 + use_small_heuristics = "Max"
+24 -24
app/src-tauri/src/lib.rs
··· 5 5 // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 6 6 #[tauri::command] 7 7 fn greet(name: &str) -> String { 8 - format!("Hello, {}! You've been greeted from Rust!", name) 8 + format!("Hello, {}! You've been greeted from Rust!", name) 9 9 } 10 10 11 11 #[cfg_attr(mobile, tauri::mobile_entry_point)] 12 12 pub fn run() { 13 - tauri::Builder::default() 14 - .setup(|app| { 15 - let app_handle = app.handle(); 16 - let store_path = PathBuf::from("settings.json"); 17 - let store = StoreBuilder::new(app_handle, store_path).build()?; 13 + tauri::Builder::default() 14 + .setup(|app| { 15 + let app_handle = app.handle(); 16 + let store_path = PathBuf::from("settings.json"); 17 + let store = StoreBuilder::new(app_handle, store_path).build()?; 18 18 19 - let user_id = store.get("user_id"); 19 + let user_id = store.get("user_id"); 20 20 21 - let page = if user_id.is_some() { 22 - "/src/home-page/home.html" 23 - } else { 24 - "/src/signup-page/signup.html" 25 - }; 21 + let page = if user_id.is_some() { 22 + "/src/home-page/home.html" 23 + } else { 24 + "/src/signup-page/signup.html" 25 + }; 26 26 27 - // create and open window directly at the correct page 28 - WebviewWindowBuilder::new(app_handle, "main", WebviewUrl::App(page.into())) 29 - .title("privacypin") 30 - .inner_size(412.0, 715.0) 31 - .resizable(false) 32 - .build()?; 27 + // create and open window directly at the correct page 28 + WebviewWindowBuilder::new(app_handle, "main", WebviewUrl::App(page.into())) 29 + .title("privacypin") 30 + .inner_size(412.0, 715.0) 31 + .resizable(false) 32 + .build()?; 33 33 34 - Ok(()) 35 - }) 36 - .plugin(tauri_plugin_store::Builder::default().build()) 37 - .invoke_handler(tauri::generate_handler![greet]) 38 - .run(tauri::generate_context!()) 39 - .expect("error while running tauri application"); 34 + Ok(()) 35 + }) 36 + .plugin(tauri_plugin_store::Builder::default().build()) 37 + .invoke_handler(tauri::generate_handler![greet]) 38 + .run(tauri::generate_context!()) 39 + .expect("error while running tauri application"); 40 40 }
+1 -1
app/src-tauri/src/main.rs
··· 2 2 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 3 4 4 fn main() { 5 - privacypin_lib::run() 5 + privacypin_lib::run() 6 6 }
+4 -25
app/src/add-friend-page/add-friend.html
··· 28 28 29 29 <div class="card"> 30 30 <label for="friendUserId">Friend user id</label> 31 - <input 32 - id="friendUserId" 33 - type="text" 34 - x-model="friend_id" 35 - placeholder="Enter their user id" 36 - /> 31 + <input id="friendUserId" type="text" x-model="friend_id" placeholder="Enter their user id" /> 37 32 38 33 <label for="friendKey">Friend key</label> 39 - <input 40 - id="friendKey" 41 - type="text" 42 - x-model="friend_rand_key" 43 - placeholder="Enter their key" 44 - /> 34 + <input id="friendKey" type="text" x-model="friend_rand_key" placeholder="Enter their key" /> 45 35 46 36 <label for="friendName">Friend Name</label> 47 - <input 48 - id="friendName" 49 - type="text" 50 - x-model="friend_name" 51 - placeholder="Enter their name" 52 - /> 37 + <input id="friendName" type="text" x-model="friend_name" placeholder="Enter their name" /> 53 38 54 - <button 55 - class="primary" 56 - x-bind:disabled="is_doing_stuff" 57 - @click="sendFriendRequest()" 58 - > 59 - Send friend request 60 - </button> 39 + <button class="primary" x-bind:disabled="is_doing_stuff" @click="sendFriendRequest()">Send friend request</button> 61 40 </div> 62 41 </div> 63 42 </div>
+6 -25
app/src/home-page/home.html
··· 15 15 <!-- <@azom.dev> somehow the "+" emoji does not display in the code for me, but it's temporary anyways --> 16 16 <!-- <@kishka.cc> we will need to replace these with svgs, as it's the font that messes up the emoji --> 17 17 <button class="icon-btn" @click="updateServer()"> 18 - <img 19 - class="svg-icon" 20 - src="/src/assets/paperplane.svg" 21 - alt="Paperplane Flying Icon" 22 - /> 18 + <img class="svg-icon" src="/src/assets/paperplane.svg" alt="Paperplane Flying Icon" /> 23 19 </button> 24 20 <button class="icon-btn" @click="addFriend()"> 25 21 <img class="svg-icon" src="/src/assets/user+.svg" alt="Friend Add Icon" /> ··· 41 37 <div class="content"> 42 38 <div class="friends-header"> 43 39 <h2 style="font-size: 1rem; margin: 0">Friends</h2> 44 - <span style="color: #6b7280; font-size: 0.9rem" 45 - >(<span x-text="friends.length"></span>)</span 46 - > 40 + <span style="color: #6b7280; font-size: 0.9rem">(<span x-text="friends.length"></span>)</span> 47 41 </div> 48 42 <!--TODO idk why but sometimes when i launch the app I see the stuff when you have no friends for a split second before I see them appear, even tho we get them in init --> 49 43 <template x-if="friends.length > 0"> ··· 52 46 <div class="friend-card"> 53 47 <strong x-text="friend.name"></strong> 54 48 <div class="friend-actions"> 55 - <button class="view-btn" @click="viewLocation(friend.id)"> 56 - <img 57 - class="svg-icon" 58 - src="/src/assets/pin-location.svg" 59 - alt="Pin Icon" 60 - />View 49 + <button class="view-btn" @click="viewLocation(friend.id)"><img class="svg-icon" src="/src/assets/pin-location.svg" alt="Pin Icon" />View</button> 50 + 51 + <button class="menu-icon" style="margin-bottom: auto" @click="friendOptions(friend.id)"> 52 + <img class="svg-icon" src="/src/assets/ellipsis-vertical.svg" alt="Options menu" /> 61 53 </button> 62 - <a 63 - class="menu-icon" 64 - style="margin-bottom: auto" 65 - @click="friendOptions(friend.id)" 66 - > 67 - <img 68 - class="svg-icon" 69 - src="/src/assets/ellipsis-vertical.svg" 70 - alt="Options menu" 71 - /> 72 - </a> 73 54 </div> 74 55 </div> 75 56 </template>
+2 -6
app/src/settings-page/settings.html
··· 11 11 <div class="actions" x-data="settingsPageState"> 12 12 <h3>Settings</h3> 13 13 14 - <button class="btn-primary" @click="goto('home')"> 15 - Back to Home 16 - </button> 14 + <button class="btn-primary" @click="goto('home')">Back to Home</button> 17 15 18 - <button class="btn-secondary" @click="await debugLogout()"> 19 - Signout 20 - </button> 16 + <button class="btn-secondary" @click="await debugLogout()">Signout</button> 21 17 </div> 22 18 </div> 23 19 </body>
+4 -1
app/src/signup-page/signup.html
··· 35 35 Scan QR Code 36 36 </button> 37 37 38 - <button class="btn-primary" x-bind:disabled="isDoingStuff" @click="await signup()"><span x-show="isDoingStuff">Connecting...</span> <span x-show="!isDoingStuff">Connect</span></button> 38 + <button class="btn-primary" x-bind:disabled="isDoingStuff" @click="await signup()"> 39 + <span x-show="isDoingStuff">Connecting...</span> 40 + <span x-show="!isDoingStuff">Connect</span> 41 + </button> 39 42 </div> 40 43 </div> 41 44 </body>
+1 -2
app/src/utils/tools.ts
··· 1 1 export function goto(newLocation: string) { 2 - window.location.href = 3 - "/src/" + newLocation + "-page/" + newLocation + ".html"; 2 + window.location.href = "/src/" + newLocation + "-page/" + newLocation + ".html"; 4 3 }
+3 -3
app/tsconfig.json
··· 22 22 // TODO: does that break stuff? 23 23 "baseUrl": "./src", 24 24 "paths": { 25 - "@utils/*": ["utils/*"] 26 - } 25 + "@utils/*": ["utils/*"], 26 + }, 27 27 }, 28 - "include": ["src"] 28 + "include": ["src"], 29 29 }
-1
server/.rustfmt.toml
··· 1 - hard_tabs = true
+3 -7
server/src/auth.rs
··· 33 33 34 34 let users = state.users.lock().await; 35 35 let user_id = auth_data.user_id; 36 - let user = users 37 - .iter() 38 - .find(|u| u.id == user_id) 39 - .ok_or(SrvErr!("User not found"))?; 36 + let user = users.iter().find(|u| u.id == user_id).ok_or(SrvErr!("User not found"))?; 40 37 let verifying_key = user.pub_key.clone(); 41 38 42 39 // NOTE (key chaining): ··· 62 59 let sig_vec = BASE64_STANDARD 63 60 .decode(&auth_data.signature) 64 61 .map_err(|e| SrvErr!("base64 decode fail", e))?; 65 - let sig_bytes: [u8; 64] = sig_vec 66 - .try_into() 67 - .map_err(|e| SrvErr!("invalid signature length", e))?; 62 + let sig_bytes: [u8; 64] = 63 + sig_vec.try_into().map_err(|e| SrvErr!("invalid signature length", e))?; 68 64 let signature = Signature::from_bytes(&sig_bytes); 69 65 70 66 if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) {
+6 -16
server/src/handlers.rs
··· 22 22 }; 23 23 24 24 // todo check 25 - let pub_key_arr: [u8; 32] = pub_key_bytes 26 - .as_slice() 27 - .try_into() 28 - .map_err(|_| SrvErr!("Invalid pubkey length"))?; 25 + let pub_key_arr: [u8; 32] = 26 + pub_key_bytes.as_slice().try_into().map_err(|_| SrvErr!("Invalid pubkey length"))?; 29 27 let pub_key = VerifyingKey::from_bytes(&pub_key_arr) 30 28 .map_err(|e| SrvErr!("Invalid public key bytes", e))?; 31 29 ··· 38 36 admin_id_guard.replace(user_id.clone()); 39 37 } 40 38 41 - users.push(User { 42 - id: user_id.clone(), 43 - pub_key, 44 - }); 39 + users.push(User { id: user_id.clone(), pub_key }); 45 40 46 41 return Ok(Json(CreateAccountResponse { user_id, is_admin })); 47 42 } ··· 66 61 } 67 62 68 63 let mut friend_requests = state.friend_requests.lock().await; 69 - let mut friend_request_link = DirectedFriendRequestLink { 70 - sender_id: friend_id, 71 - accepter_id: user_id, 72 - }; 64 + let mut friend_request_link = 65 + DirectedFriendRequestLink { sender_id: friend_id, accepter_id: user_id }; 73 66 74 67 // if we remove sucessfully the link, it means a request already existed 75 68 // so we are making the friendship official ··· 124 117 125 118 for ping in pings { 126 119 let link = UndirectedLink::new(user_id.clone(), ping.receiver_id.clone()); 127 - pings_state 128 - .get_mut(&link) 129 - .unwrap() 130 - .add(EncryptedPing(ping.encrypted_ping)); // We assured that a ringbuffer exists because it was created when the link was created, hence the .unwrap() 120 + pings_state.get_mut(&link).unwrap().add(EncryptedPing(ping.encrypted_ping)); // We assured that a ringbuffer exists because it was created when the link was created, hence the .unwrap() 131 121 } 132 122 133 123 return Ok(());
+2 -5
server/src/log.rs
··· 9 9 middleware::Next, 10 10 response::Response, 11 11 }; 12 + 12 13 use base64::{Engine, prelude::BASE64_STANDARD}; 13 14 use serde_json::Value; 14 15 ··· 19 20 let s = v.to_str().ok()?.to_string(); 20 21 21 22 const MAX: usize = 120; 22 - if s.len() > MAX { 23 - Some(format!("{}… (len={})", &s[..MAX], s.len())) 24 - } else { 25 - Some(s) 26 - } 23 + if s.len() > MAX { Some(format!("{}… (len={})", &s[..MAX], s.len())) } else { Some(s) } 27 24 } 28 25 29 26 fn status_emoji(status: axum::http::StatusCode) -> &'static str {
+1 -4
server/src/main.rs
··· 51 51 .route("/create-account", post(create_user)) 52 52 .route("/generate-signup-key", post(generate_signup_key)) 53 53 .route("/request-friend-request", post(request_friend_request)) 54 - .route( 55 - "/is-friend-request-accepted", 56 - post(is_friend_request_accepted), 57 - ) 54 + .route("/is-friend-request-accepted", post(is_friend_request_accepted)) 58 55 .route("/send-pings", post(send_pings)) 59 56 .route("/get-pings", post(get_pings)) 60 57 .with_state(state.clone())
+3 -13
server/src/types.rs
··· 39 39 // represents a ring buffer for a directed friend connection (ex.: user1 sending to user2, which in that case it's only user1's positions) 40 40 impl RingBuffer { 41 41 pub fn new(capacity: usize) -> Self { 42 - return Self { 43 - ring: vec![None; capacity].into_boxed_slice(), 44 - idx: 0, 45 - }; 42 + return Self { ring: vec![None; capacity].into_boxed_slice(), idx: 0 }; 46 43 } 47 44 48 45 pub fn add(&mut self, p: EncryptedPing) { ··· 92 89 93 90 impl UndirectedLink { 94 91 pub fn new(a: String, b: String) -> Self { 95 - if a < b { 96 - UndirectedLink(a, b) 97 - } else { 98 - UndirectedLink(b, a) 99 - } // normalize order 92 + if a < b { UndirectedLink(a, b) } else { UndirectedLink(b, a) } // normalize order 100 93 } 101 94 102 95 pub fn from(dfrl: DirectedFriendRequestLink) -> Self { ··· 173 166 174 167 /// Central policy: what gets logged, what gets returned. 175 168 pub fn mk_srv_err(msg: impl Into<String>, cause: Option<String>) -> SrvErr { 176 - SrvErr { 177 - msg: msg.into(), 178 - cause, 179 - } 169 + SrvErr { msg: msg.into(), cause } 180 170 } 181 171 182 172 #[macro_export]
+4 -1
server/test/test.test.ts
··· 47 47 48 48 const res3 = await post("get-pings", user, admin.user_id); 49 49 50 - expect(JSON.parse(res3)).toEqual(["this is definitely encrypted trust #2", "this is definitely encrypted trust #1"]); 50 + expect(JSON.parse(res3)).toEqual([ 51 + "this is definitely encrypted trust #2", 52 + "this is definitely encrypted trust #1", 53 + ]); 51 54 }); 52 55 });
+16 -3
server/test/utils.ts
··· 3 3 export const URL = "http://127.0.0.1:3000"; 4 4 5 5 // Generate an Ed25519 keypair and register it with the server. 6 - export async function generateUser(signup_key: string | undefined, should_be_admin: boolean = false): Promise<{ user_id: string; pubKey: Uint8Array; privKey: Uint8Array }> { 6 + export async function generateUser( 7 + signup_key: string | undefined, 8 + should_be_admin: boolean = false, 9 + ): Promise<{ user_id: string; pubKey: Uint8Array; privKey: Uint8Array }> { 7 10 if (!signup_key) { 8 11 throw new Error("signup_key was not provided or captured from server output"); 9 12 } ··· 35 38 return { user_id: json.user_id, pubKey, privKey }; 36 39 } 37 40 38 - export async function post(endpoint: string, user: { user_id: string; privKey: Uint8Array }, data: Object | string | undefined): Promise<any> { 41 + export async function post( 42 + endpoint: string, 43 + user: { user_id: string; privKey: Uint8Array }, 44 + data: Object | string | undefined, 45 + ): Promise<any> { 39 46 let bodyBytes: Uint8Array; 40 47 41 48 if (typeof data === "object") { ··· 48 55 } 49 56 50 57 // Sign body using private key 51 - const privateKey = await crypto.subtle.importKey("pkcs8", user.privKey.buffer, { name: "Ed25519" }, false, ["sign"]); 58 + const privateKey = await crypto.subtle.importKey( 59 + "pkcs8", 60 + user.privKey.buffer, 61 + { name: "Ed25519" }, 62 + false, 63 + ["sign"], 64 + ); 52 65 53 66 const signature = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, bodyBytes)); 54 67 const signature_b64 = btoa(String.fromCharCode(...signature));