400 lines
12 KiB
Rust
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,
|
|
}
|