use ed25519_dalek::SigningKey;
use iroh_docs::{AuthorId, api::Doc};
use serde::{Deserialize, Serialize};
use crate::{
Result, SyncwebError,
constants::{MEMBER_LIST_SIGNATURE_CONTEXT, NETWORK_MEMBERS_KEY},
node::{blob_store::BlobStore, docs_engine::DocsEngine},
parsing::current_unix_secs,
};
use super::network::Network;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SignedMemberList {
pub network_id: String,
pub owner: String,
pub sequence: u64,
pub members: Vec<MemberEntry>,
pub updated_at: u64,
pub signature: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MemberEntry {
pub key: String,
pub joined_at: u64,
pub role: MemberRole,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MemberRole {
Admin,
Member,
}
impl SignedMemberList {
#[must_use]
pub fn new(network_id: &str, owner: &str) -> Self {
Self {
network_id: network_id.to_owned(),
owner: owner.to_owned(),
sequence: 0,
members: Vec::new(),
updated_at: current_unix_secs(),
signature: String::new(),
}
}
#[must_use]
pub fn from_network(network: &Network) -> Self {
let owner = hex::encode(network.owner.as_bytes());
let now = current_unix_secs();
let mut members = network
.members
.iter()
.map(|key| MemberEntry {
key: key.to_string(),
joined_at: now,
role: if *key == network.owner {
MemberRole::Admin
} else {
MemberRole::Member
},
})
.collect::<Vec<_>>();
members.sort_by(|left, right| left.key.cmp(&right.key));
Self {
network_id: network.id.to_string(),
owner,
sequence: 0,
members,
updated_at: now,
signature: String::new(),
}
}
pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
let owner_hex = crate::signature::public_key_hex(signing_key);
if self.owner != owner_hex {
return Err(SyncwebError::InvalidSignature(
"signing key does not match owner field".to_owned(),
));
}
let message = self.serialize_unsigned()?;
let mut signed_bytes = Vec::new();
signed_bytes.extend_from_slice(MEMBER_LIST_SIGNATURE_CONTEXT);
signed_bytes.extend_from_slice(&message);
self.signature = crate::signature::sign_hex(signing_key, &signed_bytes);
Ok(())
}
pub fn verify(&self) -> Result<()> {
let message = self.serialize_unsigned()?;
let mut signed_bytes = Vec::new();
signed_bytes.extend_from_slice(MEMBER_LIST_SIGNATURE_CONTEXT);
signed_bytes.extend_from_slice(&message);
crate::signature::verify_hex(&self.owner, &signed_bytes, &self.signature)
}
fn serialize_unsigned(&self) -> Result<Vec<u8>> {
let mut unsigned = self.clone();
unsigned.signature = String::new();
serde_json::to_vec(&unsigned).map_err(|error| SyncwebError::operation("failed to serialize member list", error))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NetworkInfo {
pub name: String,
pub label: String,
pub created_at: u64,
}
pub async fn write_member_list(
docs: &DocsEngine,
blobs: &BlobStore,
doc: &Doc,
author: AuthorId,
signing: &SigningKey,
network: &Network,
) -> Result<u64> {
let sequence = next_member_list_sequence(docs, blobs, doc).await?;
let mut list = SignedMemberList::from_network(network);
list.sequence = sequence;
list.sign(signing)?;
let bytes =
serde_json::to_vec(&list).map_err(|error| SyncwebError::operation("failed to serialize member list", error))?;
docs.set(doc, author, NETWORK_MEMBERS_KEY, bytes).await?;
Ok(sequence)
}
async fn next_member_list_sequence(docs: &DocsEngine, blobs: &BlobStore, doc: &Doc) -> Result<u64> {
let Some(entry) = docs.get_any(doc, NETWORK_MEMBERS_KEY).await? else {
return Ok(1);
};
let bytes = blobs.get(entry.content_hash()).await?;
let current: SignedMemberList = serde_json::from_slice(&bytes)
.map_err(|error| SyncwebError::operation("failed to deserialize member list", error))?;
Ok(current.sequence.saturating_add(1))
}