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 } 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>(&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>(&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 }, // 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 }, 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 }, // 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 }, GroupUserRemove { group: Group, users: Vec }, GroupRoleCreate { group: Group, role: Role }, GroupRoleDelete { group: Group, role: Role }, GroupRoleUserAdd { group: Group, role: Role, users: Vec }, GroupRoleUserRemove { group: Group, role: Role, users: Vec }, // 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 }, 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 { 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), 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, }