Files
fedichat-lib/src/client.rs
T

400 lines
12 KiB
Rust

use uuid::Uuid;
use crate::message::{MessageId,TaggedMessage,Relevance};
use crate::state::{self,StateValue,StatePath,StatePermission,StatePermissionKey};
use crate::{RoomId,Group,Role,User,GroupPower,ServerAddr};
use ed25519::signature::{Signer,Verifier};
use ed25519::Signature;
use rmp_serde::encode::Serializer;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use thiserror::Error;
//StatePath [String]
//octal is fine for now
//maybe do ACL late???
//StatePermissions (u8,u8,u8)
//RoomId [i64]
//#[derive(Serialize,Deserialize,Clone,Debug)]
///// This exists solely to allow the DB to finish tagging the message. It is every field of a
///// message except for the ID which can be created using a Postgres serial type
//pub struct InsertableClientMessage {
// pub message: ClientMessage,
// #[serde(with="time::serde::timestamp")]
// pub client_timestamp: OffsetDateTime,
// #[serde(with="time::serde::timestamp")]
// pub server_timestamp: OffsetDateTime,
// #[serde(with = "serde_bytes")]
// pub signature: Box<[u8]>,
// pub user: User
//}
#[derive(Serialize,Deserialize,Clone,Debug)]
pub struct SignedClientMessage {
pub message: ClientMessage,
// timestamp sent by client protects against replay attacks by untrusted server
#[serde(with="time::serde::timestamp")]
pub timestamp: OffsetDateTime,
// Should I enforce this being a ecdsa signature?
// What is the signature of????
// Can I reserialize the message and check the signature that way??
// That would be very brittle but there isn't a good way to extract the
// bytes using serde. I'll write something better at some point
//
// Should this be optional?
#[serde(with = "serde_bytes")]
pub signature: Box<[u8]>,
// Which server should process the request. Most messages are forwardable but some are not,
// like auth requests and media uploads.
//
// Setting this to None makes it a local message
pub target: Option<ServerAddr>
}
impl SignedClientMessage {
pub fn tag(self, username: User, servername: ServerAddr) -> TaggedMessage {
TaggedMessage {
message: self.message,
client_timestamp: self.timestamp,
server_timestamp: OffsetDateTime::now_utc(),
signature: self.signature,
user: username,
target: self.target.unwrap_or(servername)
}
}
// Canonical way to sign messages. Glue the message, target, and timestamp together. Serialize
// them, then sign those bytes. It is slightly ambiguous how target=None messages should
// by signed vs target=Some(local_server) especially if rewriting of the target happens.
// This makes signature verification harder as you would have to check both
// cases to see if either correctly verifies.
pub fn sign<S: Signer<ed25519::Signature>>(&mut self, signer: S) -> Result<(),SignatureError> {
let mut bytes = Vec::new();
(&self.message,&self.target,&self.timestamp).serialize(&mut Serializer::new(&mut bytes).with_struct_map())?;
let result = signer.try_sign(&bytes)?;
self.signature = Box::new(result.to_bytes());
Ok(())
}
pub fn verify<V: Verifier<ed25519::Signature>>(&self, verifier: V) -> Result<(),SignatureError> {
let mut bytes = Vec::new();
(&self.message,&self.target,&self.timestamp).serialize(&mut Serializer::new(&mut bytes).with_struct_map())?;
let sig = Signature::from_slice(&self.signature)?;
Ok(verifier.verify(&bytes,&sig)?)
}
}
#[derive(Debug,Error)]
pub enum SignatureError {
#[error("Problem while making eliptic curve {0}")]
Ed25519(#[from] ed25519::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] rmp_serde::encode::Error),
}
#[derive(Serialize,Deserialize,Clone,Debug)]
pub enum AuthMethod {
Password(String),
Token(String)
}
#[derive(Serialize,Deserialize,Clone,Debug)]
pub enum ClientMessage {
Auth{
username: String,
password: AuthMethod
},
// Create a token with a default expiry window
CreateToken,
// Maybe ask for email too? Or a potential invite code
UserCreate {
username: String,
password: String,
},
// Used to require accounts to complete some kind of challenge. Simplest
// is giving a password/invite code to create an account or join a room
ChallengeAnswer {
response: String
},
// TODO I still dont have key management commands
// Should it be one message type or mutiple?
Message {
body: String,
room_id: RoomId,
// Can I overload this to have an optional ID? And reject any
// incoming messages that specify an ID? How else do I return an ID
// only for this message
//
// This is a hacky way to do it but idk how else to
id: Option<MessageId>
},
// Private message/invite mechanism
MessagePost {
body: String,
user: User
},
// Replace the body of the message with a new one
MessageEdit {
room_id: RoomId,
body: String,
id: MessageId
},
MessageDelete {
room_id: RoomId,
id: MessageId
},
// State Actions
StateCreate {
room_id: RoomId,
path: StatePath,
content: StateValue,
permissions: Option<state::PermissionTable>
},
StateWrite {
room_id: RoomId,
path: StatePath,
content: StateValue
},
StateDelete {
room_id: RoomId,
path: StatePath,
},
StateAppend {
room_id: RoomId,
path: StatePath,
content: StateValue
},
StateMove {
room_id: RoomId,
path: StatePath,
target: StatePath,
},
StateRead {
room_id: RoomId,
path: StatePath
},
PermissionAdd {
room_id: RoomId,
path: StatePath,
permission: StatePermission
},
PermissionRead {
room_id: RoomId,
path: StatePath,
},
PermissionDelete {
room_id: RoomId,
path: StatePath,
permission: StatePermissionKey
},
// Groups really should have a way to add permissions by user
// specifically for who can join or invite others
//
// Maybe make a group -> role -> member hierarchy?
//
//
// Could always do this through a bot that owns a group??
GroupCreate {
group: Group,
users: Vec<User>
},
// Only the creator of a group or a server admin can delete groups
GroupDelete {
group: Group
},
// Only the creator of a group or a server admin can delete groups
// same with adding, though there should be a way to add group officers
// at some point
GroupUserAdd {
group: Group,
users: Vec<User>
},
GroupUserRemove {
group: Group,
users: Vec<User>
},
GroupRoleCreate {
group: Group,
role: Role
},
GroupRoleDelete {
group: Group,
role: Role
},
GroupRoleUserAdd {
group: Group,
role: Role,
users: Vec<User>
},
GroupRoleUserRemove {
group: Group,
role: Role,
users: Vec<User>
},
// Should work like discord roles
// Can control who can invite to the group
// Can be used with permissions to make rooms that are private for individual roles
GroupRolePowerAdd {
group: Group,
role: Role,
power: GroupPower
},
GroupRolePowerRemove {
group: Group,
role: Role,
power: GroupPower
},
// Returns an ID to use for message sending
// The server can potentially use the current username to associate media uploads
// with users
MediaUpload {
#[serde(with = "serde_bytes")]
bytes: Vec<u8>
},
MediaFetch {
id: Uuid,
},
// Join and subscribe
RoomJoin {
room_id: RoomId,
},
RoomLeave {
room_id: RoomId,
},
RoomCreate {
room_id: RoomId,
},
SubscribeMessages {
room_id: RoomId,
},
SubscribeState {
room_id: RoomId,
state: StatePath
},
FetchMessages {
room_id: RoomId,
count: u64,
end: MessageId,
}
}
impl ClientMessage<> {
pub fn is_forwardable(&self) -> bool {
use ClientMessage::*;
match self {
MediaUpload{bytes: _}
| Auth { .. }
| CreateToken { .. }
| UserCreate { .. }
// Groups can only be created locally. However, you can still
// grant admin permissions to users on other servers
| GroupCreate { .. } => false,
// In the future challenges might be forwardable but right now they are
// only used for signups and auth is local.
//| ChallengeAnswer {
// response: _
//} => false,
_ => true
}
}
pub fn get_relevance(&self) -> Option<Relevance> {
use ClientMessage::*;
Some(match self {
Message {
body: _,
id: _,
room_id
// These don't necessarily need cloned, it might make sense to make an owned
// version of Relevance and a reference version of it
} => Relevance::Message(room_id.clone()),
// Private message/invite mechanism
MessagePost {
body: _,
user
} => Relevance::Post(user.clone()),
// Replace the body of the message with a new one
MessageEdit {
room_id,
body: _,
id: _
} => Relevance::Message(room_id.clone()),
MessageDelete {
room_id,
id: _,
} => Relevance::Message(room_id.clone()),
StateWrite {
room_id,
path,
content: _
} => Relevance::State(room_id.clone(),path.clone()),
StateDelete {
room_id,
path,
} => Relevance::State(room_id.clone(),path.clone()),
StateAppend {
room_id,
path,
content: _
} => Relevance::State(room_id.clone(),path.clone()),
_ => return None
})
}
}
//TODO: Implement thiserror for both of these for better info
#[derive(Serialize,Deserialize,Debug)]
pub enum ServerError {
InvalidPermission,
// Server can specify the required challenge
ChallengeInviteCode,
NotAuthenticated,
AlreadyAuthenticated,
AuthenticationFailed,
NotInChallenge,
NotJoined(RoomId),
RoomExists(RoomId),
RoomNotFound(RoomId),
ValueNotFound(StatePath),
MismatchedTypes,
TokenExpired,
DeleteNotEmpty(StatePath),
RemoveLastOwner,
MessageNotForwardable,
UserAlreadyExists,
MissingPermission,
Generic
}
#[derive(Serialize,Deserialize,Debug)]
pub enum ServerMessage {
// Returned on fetch or naturally from subscribe
// This should be
Messages(Vec<TaggedMessage>),
MediaId(Uuid),
#[serde(with = "serde_bytes")]
Media(Box<[u8]>),
Error(ServerError),
// Returned on read
State(StatePath,StateValue),
// Returned on permission read
StatePermission(StatePath,crate::state::PermissionTable),
// Returned on subscribe, forwards state change message to client
StatePub(TaggedMessage),
MessagePub(TaggedMessage),
Post(TaggedMessage),
OkMessage(MessageId),
Token(String),
Ok,
}