Conduit is a simple, fast and reliable chat server powered by Matrix https://conduit.rs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

351 lines
11 KiB

pub mod error;
use argon2::{Config, Variant};
use cmp::Ordering;
use rand::prelude::*;
use ring::digest;
use ruma::{
api::{client::error::ErrorKind, federation::membership::RawStrippedState},
canonical_json::try_from_json_map,
events::AnyStrippedStateEvent,
room_version_rules::RoomVersionRules,
serde::Raw,
signatures::Verified,
CanonicalJsonError, CanonicalJsonObject, CanonicalJsonValue, RoomId,
};
use serde_json::value::to_raw_value;
use std::{
cmp,
collections::BTreeMap,
fmt,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::sync::RwLock;
use tracing::warn;
use crate::{service::pdu::gen_event_id_canonical_json, services, Error, Result};
pub fn millis_since_unix_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time is valid")
.as_millis() as u64
}
pub fn secs_since_unix_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time is valid")
.as_secs()
}
pub fn increment(old: Option<&[u8]>) -> Option<Vec<u8>> {
let number = match old.map(|bytes| bytes.try_into()) {
Some(Ok(bytes)) => {
let number = u64::from_be_bytes(bytes);
number + 1
}
_ => 1, // Start at one. since 0 should return the first event in the db
};
Some(number.to_be_bytes().to_vec())
}
pub fn generate_keypair() -> Vec<u8> {
let mut value = random_string(8).as_bytes().to_vec();
value.push(0xff);
value.extend_from_slice(
&ruma::signatures::Ed25519KeyPair::generate()
.expect("Ed25519KeyPair generation always works (?)"),
);
value
}
/// Parses the bytes into an u64.
pub fn u64_from_bytes(bytes: &[u8]) -> Result<u64, std::array::TryFromSliceError> {
let array: [u8; 8] = bytes.try_into()?;
Ok(u64::from_be_bytes(array))
}
/// Parses the bytes into a string.
pub fn string_from_bytes(bytes: &[u8]) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(bytes.to_vec())
}
pub fn random_string(length: usize) -> String {
rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(length)
.map(char::from)
.collect()
}
/// Calculate a new hash for the given password
pub fn calculate_password_hash(password: &str) -> Result<String, argon2::Error> {
let hashing_config = Config {
variant: Variant::Argon2id,
..Default::default()
};
let salt = random_string(32);
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &hashing_config)
}
#[tracing::instrument(skip(keys))]
pub fn calculate_hash(keys: &[&[u8]]) -> Vec<u8> {
// We only hash the pdu's event ids, not the whole pdu
let bytes = keys.join(&0xff);
let hash = digest::digest(&digest::SHA256, &bytes);
hash.as_ref().to_owned()
}
pub fn common_elements(
mut iterators: impl Iterator<Item = impl Iterator<Item = Vec<u8>>>,
check_order: impl Fn(&[u8], &[u8]) -> Ordering,
) -> Option<impl Iterator<Item = Vec<u8>>> {
let first_iterator = iterators.next()?;
let mut other_iterators = iterators.map(|i| i.peekable()).collect::<Vec<_>>();
Some(first_iterator.filter(move |target| {
other_iterators.iter_mut().all(|it| {
while let Some(element) = it.peek() {
match check_order(element, target) {
Ordering::Greater => return false, // We went too far
Ordering::Equal => return true, // Element is in both iters
Ordering::Less => {
// Keep searching
it.next();
}
}
}
false
})
}))
}
/// Fallible conversion from any value that implements `Serialize` to a `CanonicalJsonObject`.
///
/// `value` must serialize to an `serde_json::Value::Object`.
pub fn to_canonical_object<T: serde::Serialize>(
value: T,
) -> Result<CanonicalJsonObject, CanonicalJsonError> {
use serde::ser::Error;
match serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)? {
serde_json::Value::Object(map) => try_from_json_map(map),
_ => Err(CanonicalJsonError::SerDe(serde_json::Error::custom(
"Value must be an object",
))),
}
}
pub fn deserialize_from_str<
'de,
D: serde::de::Deserializer<'de>,
T: FromStr<Err = E>,
E: fmt::Display,
>(
deserializer: D,
) -> Result<T, D::Error> {
struct Visitor<T: FromStr<Err = E>, E>(std::marker::PhantomData<T>);
impl<T: FromStr<Err = Err>, Err: fmt::Display> serde::de::Visitor<'_> for Visitor<T, Err> {
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a parsable string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(Visitor(std::marker::PhantomData))
}
// Copied from librustdoc:
// https://github.com/rust-lang/rust/blob/cbaeec14f90b59a91a6b0f17fc046c66fa811892/src/librustdoc/html/escape.rs
/// Wrapper struct which will emit the HTML-escaped version of the contained
/// string when passed to a format string.
pub struct HtmlEscape<'a>(pub &'a str);
impl fmt::Display for HtmlEscape<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
// Because the internet is always right, turns out there's not that many
// characters to escape: http://stackoverflow.com/questions/7381974
let HtmlEscape(s) = *self;
let pile_o_bits = s;
let mut last = 0;
for (i, ch) in s.char_indices() {
let s = match ch {
'>' => "&gt;",
'<' => "&lt;",
'&' => "&amp;",
'\'' => "&#39;",
'"' => "&quot;",
_ => continue,
};
fmt.write_str(&pile_o_bits[last..i])?;
fmt.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as we
// only match single byte characters
last = i + 1;
}
if last < s.len() {
fmt.write_str(&pile_o_bits[last..])?;
}
Ok(())
}
}
/// Converts `RawStrippedState` (federation format) into `Raw<AnyStrippedState>` (client format)
pub fn convert_stripped_state(
stripped_state: Vec<RawStrippedState>,
) -> Result<Vec<Raw<AnyStrippedStateEvent>>> {
stripped_state
.into_iter()
.map(|stripped_state| match stripped_state {
RawStrippedState::Stripped(state) => Ok(state),
RawStrippedState::Pdu(state) => {
let mut event: CanonicalJsonObject =
serde_json::from_str(state.get()).map_err(|e| {
warn!("Error parsing incoming event {:?}: {:?}", state, e);
Error::BadServerResponse("Invalid PDU in server response")
})?;
event.retain(|k, _| {
matches!(k.as_str(), "content" | "sender" | "state_key" | "type")
});
let raw_value = to_raw_value(&CanonicalJsonValue::Object(event))
.expect("To raw json should not fail since only change was adding signature");
Ok(Raw::<AnyStrippedStateEvent>::from_json(raw_value))
}
})
.collect()
}
/// Performs checks on incoming stripped state, as per [MSC4311]
///
/// [MSC4311]: https://github.com/matrix-org/matrix-spec-proposals/pull/4311
pub async fn check_stripped_state(
stripped_state: &[RawStrippedState],
room_id: &RoomId,
rules: &RoomVersionRules,
) -> Result<()> {
// Nothing needs to be done for legacy room ids
if room_id.server_name().is_some() && !rules.authorization.room_create_event_id_as_room_id {
return Ok(());
}
#[cfg(feature = "enforce_msc4311")]
let mut seen_create_event = false;
#[cfg(feature = "enforce_msc4311")]
if !stripped_state.iter().all(|state| match state {
RawStrippedState::Pdu(_) => true,
RawStrippedState::Stripped(_) => false,
}) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Non-pdu found in stripped state",
));
}
let stripped_state = stripped_state
.iter()
.filter_map(|event| {
if let RawStrippedState::Pdu(pdu) = event {
Some(pdu)
} else {
None
}
})
.map(|pdu| gen_event_id_canonical_json(pdu, rules))
.collect::<Result<Vec<_>>>()?;
let pub_key_map = RwLock::new(BTreeMap::new());
for (_, pdu) in &stripped_state {
services()
.rooms
.event_handler
.fetch_required_signing_keys(pdu, &pub_key_map)
.await?;
}
for (event_id, pdu) in stripped_state {
let filtered_keys = services()
.rooms
.event_handler
.filter_required_signing_keys(&pdu, &pub_key_map, rules)
.await?;
if !ruma::signatures::verify_event(&filtered_keys, &pdu, rules)
.is_ok_and(|verified| verified == Verified::All)
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Signature check on stripped state failed",
));
}
let Some(event_type) = pdu.get("type").and_then(|t| t.as_str()) else {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Event with no type returned",
));
};
if !(event_type == "m.room.create" && rules.authorization.room_create_event_id_as_room_id) {
let pdu_room_id = pdu
.get("room_id")
.ok_or_else(|| Error::BadRequest(ErrorKind::InvalidParam, "Event missing room ID"))
.map(|v| v.as_str())?
.ok_or_else(|| {
Error::BadRequest(ErrorKind::InvalidParam, "Event has non-string room id")
})
.map(RoomId::parse)?
.map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Event has invalid room ID")
})?;
if pdu_room_id != room_id {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Stripped state room ID does not match the one of the request",
));
}
}
if event_type == "m.room.create" {
#[allow(clippy::collapsible_if)]
if event_id.localpart() != room_id.strip_sigil()
&& rules.authorization.room_create_event_id_as_room_id
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room ID generated from create event does not match that from the request",
));
}
#[cfg(feature = "enforce_msc4311")]
{
seen_create_event = true;
}
}
}
#[cfg(feature = "enforce_msc4311")]
if !seen_create_event {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Stripped state contained no valid create PDUs",
));
}
Ok(())
}