Skip to content

Commit

Permalink
Local did document storage
Browse files Browse the repository at this point in the history
Signed-off-by: Marlon Baeten <[email protected]>
  • Loading branch information
marlonbaeten committed Jul 30, 2024
1 parent d572c15 commit 2a598aa
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 66 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Cargo.lock
.tgops
.dockerignore
docs/book/
data/
2 changes: 1 addition & 1 deletion demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
deleteContact: (index: number) => void;
deleteIdentity: () => void;
deleteMessage: (contactIndex: number, index: number) => void;
Expand Down
15 changes: 10 additions & 5 deletions demo/src/Initialize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
}

export default function Initialize({ onClick }: InitializeProps) {
Expand All @@ -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');
}
Expand Down
51 changes: 29 additions & 22 deletions demo/src/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});

Expand Down
119 changes: 84 additions & 35 deletions examples/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<dyn std::error::Error>> {
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<Identity, Box<dyn std::error::Error>> {
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<HashMap<String, Identity>>,
timestamp_server: Store,
tx: broadcast::Sender<(String, String, Vec<u8>)>,
}
Expand All @@ -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,
});
Expand Down Expand Up @@ -171,10 +202,11 @@ struct CreateIdentityInput {
}

/// Create a new identity (private VID)
async fn create_identity(
State(state): State<Arc<AppState>>,
Form(form): Form<CreateIdentityInput>,
) -> impl IntoResponse {
async fn create_identity(Form(form): Form<CreateIdentityInput>) -> 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,
Expand All @@ -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)]
Expand All @@ -202,12 +237,15 @@ struct ResolveVidInput {
}

/// Resolve and verify a VID to JSON encoded key material
async fn verify_vid(
State(state): State<Arc<AppState>>,
Form(form): Form<ResolveVidInput>,
) -> Response {
async fn verify_vid(Form(form): Form<ResolveVidInput>) -> 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();
}

Expand All @@ -223,36 +261,47 @@ async fn verify_vid(
}

/// Add did document to the local state
async fn add_vid(State(state): State<Arc<AppState>>, Json(vid): Json<Vid>) -> Response {
async fn add_vid(Json(vid): Json<Vid>) -> 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());

Json(&vid).into_response()
}

/// Get the DID document of a user
async fn get_did_doc(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
async fn get_did_doc(Path(name): Path<String>) -> 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::<Vec<_>>();
eprintln!("{key} not found, stored identities: {:?}", keys);
Err(e) => {
tracing::error!("{key} not found: {e}");

(StatusCode::NOT_FOUND, "no user found").into_response()
}
Expand Down
6 changes: 4 additions & 2 deletions tsp/src/cesr/packet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down

0 comments on commit 2a598aa

Please sign in to comment.