From 2a598aa45b4feac511e59ee6f234f32e91fec993 Mon Sep 17 00:00:00 2001 From: Marlon Baeten Date: Tue, 30 Jul 2024 19:34:53 +0200 Subject: [PATCH] Local did document storage Signed-off-by: Marlon Baeten --- .github/workflows/check.yml | 27 ++++++++ .gitignore | 1 + demo/src/App.tsx | 2 +- demo/src/Initialize.tsx | 15 +++-- demo/src/useStore.ts | 51 +++++++++------- examples/script.js | 2 +- examples/src/server.rs | 119 +++++++++++++++++++++++++----------- tsp/src/cesr/packet.rs | 6 +- 8 files changed, 157 insertions(+), 66 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a6c84d4..f9b5d79 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -90,6 +90,33 @@ jobs: maturin develop python3 test.py + check-node: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '20' + + - name: Install wasm-pack + run: | + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + wasm-pack --version + - name: Build Wasm extension + run: wasm-pack build --target nodejs tsp-javascript/ + + - name: Install npm dependencies + working-directory: tsp-node/ + run: npm install + + - name: Run tests with Mocha + working-directory: tsp-node/ + run: npm test + fuzz: name: run cargo-fuzz runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a72185a..74a9fdc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Cargo.lock .tgops .dockerignore docs/book/ +data/ diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 43584fe..0b6137f 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -11,7 +11,7 @@ interface ContentProps { active: number | null; mobile: boolean; contacts: Contact[]; - createIdentity: (name: string, web: boolean) => void; + createIdentity: (name: string, web: boolean) => Promise; deleteContact: (index: number) => void; deleteIdentity: () => void; deleteMessage: (contactIndex: number, index: number) => void; diff --git a/demo/src/Initialize.tsx b/demo/src/Initialize.tsx index fd6e314..01acb87 100644 --- a/demo/src/Initialize.tsx +++ b/demo/src/Initialize.tsx @@ -5,7 +5,7 @@ import { FormEvent, useState } from 'react'; import logo from './trust-over-ip.svg'; interface InitializeProps { - onClick: (name: string, web: boolean) => void; + onClick: (name: string, web: boolean) => Promise; } export default function Initialize({ onClick }: InitializeProps) { @@ -19,10 +19,15 @@ export default function Initialize({ onClick }: InitializeProps) { e.preventDefault(); if (label.length > 0) { - onClick(label, web); - setLabel(''); - setError(''); - close(); + onClick(label, web).then((result) => { + if (!result) { + setError("This username is already taken"); + } else { + setLabel(''); + setError(''); + close(); + } + }); } else { setError('Label is required'); } diff --git a/demo/src/useStore.ts b/demo/src/useStore.ts index fed6bdd..d6bd5e1 100644 --- a/demo/src/useStore.ts +++ b/demo/src/useStore.ts @@ -185,29 +185,36 @@ export default function useStore() { const [state, dispatch] = useReducer(reducer, loadState()); const createIdentity = async (label: string, web: boolean) => { - if (web) { - const data = new URLSearchParams(); - data.append('name', label); - let result = await fetch('https://tsp-test.org/create-identity', { - method: 'POST', - body: data, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - let vidData = await result.json(); - const vid = OwnedVid.from_json(JSON.stringify(vidData)); - const id = { label, vid: vidData }; - store.current.add_private_vid(vid.create_clone()); - dispatch({ type: 'setId', id }); - } else { - const vid = OwnedVid.new_did_peer( - `https://tsp-test.org/user/${label.toLowerCase()}` - ); - const id = { label, vid: JSON.parse(vid.to_json()) }; - store.current.add_private_vid(vid.create_clone()); - dispatch({ type: 'setId', id }); + try { + if (web) { + const data = new URLSearchParams(); + data.append('name', label); + let result = await fetch('https://tsp-test.org/create-identity', { + method: 'POST', + body: data, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + let vidData = await result.json(); + const vid = OwnedVid.from_json(JSON.stringify(vidData)); + const id = { label, vid: vidData }; + store.current.add_private_vid(vid.create_clone()); + dispatch({ type: 'setId', id }); + } else { + const vid = OwnedVid.new_did_peer( + `https://tsp-test.org/user/${label.toLowerCase()}` + ); + const id = { label, vid: JSON.parse(vid.to_json()) }; + store.current.add_private_vid(vid.create_clone()); + dispatch({ type: 'setId', id }); + } + } catch (e) { + console.error(e); + return false; } + + return true; }; const addContact = async (vidString: string, label: string) => { diff --git a/examples/script.js b/examples/script.js index 2676668..a01e987 100644 --- a/examples/script.js +++ b/examples/script.js @@ -209,7 +209,7 @@ createForm.addEventListener('submit', async (event) => { updateIdentities(); ws.send(JSON.stringify(identity)); } else { - window.alert('Failed to create identity'); + window.alert('Failed to create identity, this VID might already exist'); } }); diff --git a/examples/src/server.rs b/examples/src/server.rs index 6a67c25..cad52c3 100644 --- a/examples/src/server.rs +++ b/examples/src/server.rs @@ -12,7 +12,7 @@ use axum::{ use base64ct::{Base64UrlUnpadded, Encoding}; use core::time; use futures::{sink::SinkExt, stream::StreamExt}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::json; use std::{ collections::HashMap, @@ -35,14 +35,46 @@ mod intermediary; const DOMAIN: &str = "tsp-test.org"; /// Identity struct, used to store the DID document and VID of a user +#[derive(Debug, Serialize, Deserialize)] struct Identity { did_doc: serde_json::Value, vid: Vid, } +async fn write_id(id: Identity) -> Result<(), Box> { + let name = id + .vid + .identifier() + .split(':') + .last() + .ok_or("invalid name")?; + let did = serde_json::to_string_pretty(&id)?; + let path = format!("data/{name}.json"); + + if std::path::Path::new(&path).exists() { + return Err("identity already exists".into()); + } + + tokio::fs::write(path, did).await?; + + Ok(()) +} + +async fn read_id(vid: &str) -> Result> { + let name = vid.split(':').last().ok_or("invalid name")?; + let path = format!("data/{name}.json"); + let did = tokio::fs::read_to_string(path).await?; + let id = serde_json::from_str(&did)?; + + Ok(id) +} + +fn verify_name(name: &str) -> bool { + !name.is_empty() && name.len() < 64 && name.chars().all(|c| c.is_alphanumeric()) +} + /// Application state, used to store the identities and the broadcast channel struct AppState { - db: RwLock>, timestamp_server: Store, tx: broadcast::Sender<(String, String, Vec)>, } @@ -68,7 +100,6 @@ async fn main() { timestamp_server.add_private_vid(piv).unwrap(); let state = Arc::new(AppState { - db: Default::default(), timestamp_server, tx: broadcast::channel(100).0, }); @@ -171,10 +202,11 @@ struct CreateIdentityInput { } /// Create a new identity (private VID) -async fn create_identity( - State(state): State>, - Form(form): Form, -) -> impl IntoResponse { +async fn create_identity(Form(form): Form) -> Response { + if !verify_name(&form.name) { + return (StatusCode::BAD_REQUEST, "invalid name").into_response(); + } + let (did_doc, _, private_vid) = tsp::vid::create_did_web( &form.name, DOMAIN, @@ -183,17 +215,20 @@ async fn create_identity( let key = private_vid.identifier(); - state.db.write().await.insert( - key.to_string(), - Identity { - did_doc: did_doc.clone(), - vid: private_vid.vid().clone(), - }, - ); + if let Err(e) = write_id(Identity { + did_doc: did_doc.clone(), + vid: private_vid.vid().clone(), + }) + .await + { + tracing::error!("error writing identity {key}: {e}"); + + return (StatusCode::INTERNAL_SERVER_ERROR, "error writing identity").into_response(); + } tracing::debug!("created identity {key}"); - Json(private_vid) + Json(private_vid).into_response() } #[derive(Deserialize, Debug)] @@ -202,12 +237,15 @@ struct ResolveVidInput { } /// Resolve and verify a VID to JSON encoded key material -async fn verify_vid( - State(state): State>, - Form(form): Form, -) -> Response { +async fn verify_vid(Form(form): Form) -> Response { + let name = form.vid.split(':').last().unwrap_or_default(); + + if !verify_name(name) { + return (StatusCode::BAD_REQUEST, "invalid name").into_response(); + } + // local state lookup - if let Some(identity) = state.db.read().await.get(&form.vid) { + if let Ok(identity) = read_id(&form.vid).await { return Json(&identity.vid).into_response(); } @@ -223,16 +261,25 @@ async fn verify_vid( } /// Add did document to the local state -async fn add_vid(State(state): State>, Json(vid): Json) -> Response { +async fn add_vid(Json(vid): Json) -> Response { + let name = vid.identifier().split(':').last().unwrap_or_default(); + + if !verify_name(name) { + return (StatusCode::BAD_REQUEST, "invalid name").into_response(); + } + let did_doc = tsp::vid::vid_to_did_document(&vid); - state.db.write().await.insert( - vid.identifier().to_string(), - Identity { - did_doc, - vid: vid.clone(), - }, - ); + if let Err(e) = write_id(Identity { + did_doc, + vid: vid.clone(), + }) + .await + { + tracing::error!("error writing identity {}: {e}", vid.identifier()); + + return (StatusCode::INTERNAL_SERVER_ERROR, "error writing identity").into_response(); + } tracing::debug!("added VID {}", vid.identifier()); @@ -240,19 +287,21 @@ async fn add_vid(State(state): State>, Json(vid): Json) -> Re } /// Get the DID document of a user -async fn get_did_doc(State(state): State>, Path(name): Path) -> Response { +async fn get_did_doc(Path(name): Path) -> Response { + if !verify_name(&name) { + return (StatusCode::BAD_REQUEST, "invalid name").into_response(); + } + let key = format!("did:web:{DOMAIN}:user:{name}"); - match state.db.read().await.get(&key) { - Some(identity) => { + match read_id(&key).await { + Ok(identity) => { tracing::debug!("served did.json for {key}"); Json(identity.did_doc.clone()).into_response() } - None => { - let keys = state.db.read().await; - let keys = keys.keys().collect::>(); - eprintln!("{key} not found, stored identities: {:?}", keys); + Err(e) => { + tracing::error!("{key} not found: {e}"); (StatusCode::NOT_FOUND, "no user found").into_response() } diff --git a/tsp/src/cesr/packet.rs b/tsp/src/cesr/packet.rs index 55178e3..28cee34 100644 --- a/tsp/src/cesr/packet.rs +++ b/tsp/src/cesr/packet.rs @@ -30,8 +30,10 @@ mod msgtype { } use super::{ - decode::{decode_count, decode_fixed_data, decode_variable_data, decode_variable_data_index}, - decode::{decode_count_mut, decode_fixed_data_mut, decode_variable_data_mut}, + decode::{ + decode_count, decode_count_mut, decode_fixed_data, decode_fixed_data_mut, + decode_variable_data, decode_variable_data_index, decode_variable_data_mut, + }, encode::{encode_count, encode_fixed_data}, error::{DecodeError, EncodeError}, };