~yujiri/libsufec

cca6e05f2cc92671421a102216dcdc1464507a22 — Evin Yulo 2 years ago 2b73415
add room and store
8 files changed, 246 insertions(+), 34 deletions(-)

M Cargo.lock
M Cargo.toml
M README.md
M src/lib.rs
M src/prelude.rs
A src/room.rs
M src/server.rs
A src/store.rs
M Cargo.lock => Cargo.lock +56 -25
@@ 10,24 10,24 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"

[[package]]
name = "cc"
version = "1.0.70"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"

[[package]]
name = "ed25519"
version = "1.2.0"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc"
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
dependencies = [
 "signature",
]

[[package]]
name = "libc"
version = "0.2.101"
version = "0.2.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"

[[package]]
name = "libsodium-sys"


@@ 48,28 48,29 @@ dependencies = [
 "base64",
 "serde",
 "sodiumoxide",
 "sqlite",
]

[[package]]
name = "pkg-config"
version = "0.3.19"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"

[[package]]
name = "proc-macro2"
version = "1.0.29"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [
 "unicode-xid",
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.9"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
 "proc-macro2",
]


@@ 85,18 86,18 @@ dependencies = [

[[package]]
name = "serde"
version = "1.0.130"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.130"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
 "proc-macro2",
 "quote",


@@ 105,9 106,9 @@ dependencies = [

[[package]]
name = "signature"
version = "1.3.1"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"

[[package]]
name = "sodiumoxide"


@@ 122,21 123,51 @@ dependencies = [
]

[[package]]
name = "sqlite"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2df8edd55685048550daaaf2be9024182f3523086cc86f7d50c136e55173e8c"
dependencies = [
 "libc",
 "sqlite3-sys",
]

[[package]]
name = "sqlite3-src"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1815a7a02c996eb8e5c64f61fcb6fd9b12e593ce265c512c5853b2513635691"
dependencies = [
 "cc",
 "pkg-config",
]

[[package]]
name = "sqlite3-sys"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d47c99824fc55360ba00caf28de0b8a0458369b832e016a64c13af0ad9fbb9ee"
dependencies = [
 "libc",
 "sqlite3-src",
]

[[package]]
name = "syn"
version = "1.0.76"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-xid",
 "unicode-ident",
]

[[package]]
name = "unicode-xid"
version = "0.2.2"
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"

[[package]]
name = "walkdir"

M Cargo.toml => Cargo.toml +1 -0
@@ 8,3 8,4 @@ edition = "2021"
sodiumoxide = "0.2.7"
serde = { version = "1", features = ["derive"] }
base64 = "0.13.0"
sqlite = "0.27.0"

M README.md => README.md +3 -1
@@ 1,1 1,3 @@
This is a library for [the Sufec messaging protocol](https://yujiri.xyz/sufec), minimizing the cost of implementing a client.
Library for [the Sufec messaging protocol](https://yujiri.xyz/sufec).

Currently this library includes some structs that aren't related to the protocol but that most clients would want, like `Room` and `Store`. In the future, this library cold be separted into a core "libsufec" and a "sufec-client-utils" or something.

M src/lib.rs => src/lib.rs +9 -7
@@ 5,16 5,18 @@ mod message;
mod crypto;
mod error;
mod server;
mod store;
mod room;

pub use account::{Account, DeviceId};
pub use addr::SufecAddr;
pub use message::{Message, MessageContent};
pub use error::ServerError;
pub use server::{PORT, MAX_FILE_SIZE, connect, send, login, ListeningConn};
pub use account::*;
pub use addr::*;
pub use message::*;
pub use error::*;
pub use server::*;
pub use room::*;
pub use store::*;
use prelude::*;

pub const MAX_HASHES_PER_MESSAGE: usize = u8::MAX as usize;

#[derive(Serialize, Deserialize, Clone)]
pub struct Contact {
	pub name: String,

M src/prelude.rs => src/prelude.rs +2 -0
@@ 6,3 6,5 @@ pub use sodiumoxide::crypto::hash::sha512::{hash, Digest, DIGESTBYTES};
pub use std::io::{self, Write, Read};
pub use std::net::TcpStream;
pub use std::fmt::{self, Display, Debug};
pub use std::collections::HashMap;
pub use std::path::Path;

A src/room.rs => src/room.rs +12 -0
@@ 0,0 1,12 @@
use crate::prelude::*;
use crate::addr::*;

#[derive(Serialize, Deserialize, Clone)]
pub struct Room {
	pub id: RoomId,
	pub name: String,
	pub members: Vec<SufecAddr>,
	pub unseen: usize,
}

pub type RoomId = i64;

M src/server.rs => src/server.rs +1 -1
@@ 101,7 101,7 @@ impl ListeningConn {
	}
}

pub struct EncryptedStream {
struct EncryptedStream {
	pub stream: TcpStream,
	pub key: PrecomputedKey,
	pub nonce: Nonce,

A src/store.rs => src/store.rs +162 -0
@@ 0,0 1,162 @@
use crate::prelude::*;
use crate::addr::*;
use crate::message::*;
use crate::room::*;

/// An SQLite database storing messages and hashes, and an in-memory cache of loaded messages.
/// Panics if it fails to access the database.
pub struct Store {
	conn: sqlite::Connection,
	cache: HashMap<RoomId, Vec<HistoryEntry>>,
}
impl Store {
	/// Creates the database file, including all parent directories, if it doesn't exist.
	/// Panics if that fails.
	pub fn open(path: &Path) -> Self {
		std::fs::create_dir_all(&path.parent().unwrap()).unwrap();
		let exists = Path::try_exists(path).unwrap();
		let conn = sqlite::open(path).expect("can't create database");
		if !exists { conn.execute(CREATE_TABLES).unwrap() }
		Store{conn, cache: HashMap::new()}
	}
	pub fn add_message(&mut self, room_id: RoomId, m: &HistoryEntry) {
		if let Some(cache) = self.cache.get_mut(&room_id) {
			cache.push(m.clone());
		}
		self.conn.prepare(ADD_MESSAGE).unwrap().into_cursor().bind(&[
			sqlite::Value::Integer(room_id),
			sqlite::Value::Binary(m.sender.to_bytes()),
			sqlite::Value::Integer(m.timestamp as i64),
			sqlite::Value::Binary(m.content.to_bytes()),
			sqlite::Value::Integer(m.hash_sent as i64),
			sqlite::Value::Integer(m.all_verified as i64),
		]).unwrap().try_next().unwrap();
	}
	pub fn get(&mut self, room_id: RoomId) -> &[HistoryEntry] {
		if self.cache.get(&room_id).is_none() {
			self.cache.insert(room_id, self.load_messages(room_id));
		}
		self.cache.get(&room_id).unwrap()
	}
	pub fn has_message(&self, room_id: RoomId, timestamp: u64, sender: &SufecAddr) -> bool {
		self.conn.prepare("SELECT room_id FROM messages WHERE room_id = ? AND timestamp = ? AND sender = ?").unwrap()
			.into_cursor().bind(&[
				sqlite::Value::Integer(room_id),
				sqlite::Value::Integer(timestamp as i64),
				sqlite::Value::Binary(sender.to_bytes()),
			]).unwrap().try_next().unwrap().is_some()
	}
	fn load_messages(&self, room_id: RoomId) -> Vec<HistoryEntry> {
		let mut results = vec![];
		let query = format!("{} {}", SELECT_MESSAGES, "WHERE room_id = ? ORDER BY timestamp ASC");
		let mut cursor = self.conn.prepare(&query).unwrap().into_cursor().bind(&[
			sqlite::Value::Integer(room_id),
		]).unwrap();
		while let Some(row) = cursor.try_next().unwrap() {
			results.push(self.scan_message(row));
		}
		results
	}
	fn scan_message(&self, row: &[sqlite::Value]) -> HistoryEntry {
		HistoryEntry{
			sender: SufecAddr::from_bytes(row[0].as_binary().unwrap()).unwrap(),
			timestamp: row[1].as_integer().unwrap() as u64,
			content: MessageContent::from_bytes(row[2].as_binary().unwrap()).unwrap(),
			hash_sent: row[3].as_integer().unwrap() == 1,
			all_verified: row[4].as_integer().unwrap() == 1,
		}
	}
	pub fn check_hashes(&self, room: &Room, entry: &HistoryEntry) -> bool {
		let mut need: Vec<SufecAddr> = room.members.iter().cloned().filter(|m| *m != entry.sender).collect();
		let query = "SELECT sender, hash FROM hashes WHERE room_id = ? AND timestamp = ?";
		let mut cursor = self.conn.prepare(&query).unwrap().into_cursor().bind(&[
			sqlite::Value::Integer(room.id),
			sqlite::Value::Integer(entry.timestamp as i64),
		]).unwrap();
		while let Some(row) = cursor.try_next().unwrap() {
			let hasher = SufecAddr::from_bytes(row[0].as_binary().unwrap()).unwrap();
			let hash = Digest::from_slice(row[1].as_binary().unwrap()).unwrap();
			if hash != entry.hash() { return false }
			need.retain(|a| *a != hasher);
		}
		need.is_empty()
	}
	pub fn add_hash(&mut self, room_id: RoomId, sender: &SufecAddr, timestamp: u64, hash: Digest) {
		self.conn.prepare(ADD_HASH).unwrap().into_cursor().bind(&[
			sqlite::Value::Integer(room_id),
			sqlite::Value::Integer(timestamp as i64),
			sqlite::Value::Binary(sender.to_bytes()),
			sqlite::Value::Binary(hash.as_ref().to_vec()),
		]).unwrap().try_next().unwrap();
	}
	// Note the messages are marked as hash_sent immediately, so if sending these hashes fails,
	// the client will falsely think these messages have been sent.
	pub fn prepare_outgoing_hashes(&self, room_id: RoomId) -> Vec<(u64, Digest)> {
		let mut messages = vec![];
		let query = format!("{} {}", SELECT_MESSAGES,
			"WHERE room_id = ? AND NOT hash_sent ORDER BY timestamp ASC LIMIT ?");
		let mut cursor = self.conn.prepare(&query).unwrap().into_cursor().bind(&[
			sqlite::Value::Integer(room_id),
			sqlite::Value::Integer(MAX_HASHES_PER_MESSAGE as i64),
		]).unwrap();
		while let Some(row) = cursor.try_next().unwrap() {
			let msg = self.scan_message(row);
			self.conn.prepare("UPDATE messages SET hash_sent = 1 WHERE room_id = ? AND timestamp = ?").unwrap()
				.into_cursor().bind(&[
					sqlite::Value::Integer(room_id),
					sqlite::Value::Integer(msg.timestamp as i64),
				]).unwrap().try_next().unwrap();
			messages.push(msg);
		}
		messages.iter().map(|m| (m.timestamp, m.hash())).collect()
	}
	pub fn mark_verified(&mut self, room_id: RoomId, timestamp: u64) {
		if let Some(cache) = self.cache.get_mut(&room_id) {
			let msg = cache.iter_mut().find(|m| m.timestamp == timestamp).unwrap();
			msg.all_verified = true;
		}
		self.conn.prepare("UPDATE messages SET all_verified = 1 WHERE room_id = ? AND timestamp = ?").unwrap()
			.into_cursor().bind(&[
				sqlite::Value::Integer(room_id),
				sqlite::Value::Integer(timestamp as i64),
			]).unwrap().try_next().unwrap();
	}
}

const CREATE_TABLES: &str = "
CREATE TABLE messages (
	room_id integer not null,
	sender blob not null,
	timestamp integer not null,
	content blob not null,
	hash_sent integer not null,
	all_verified integer not null
);
CREATE TABLE hashes (
	room_id integer not null,
	timestamp integer not null,
	sender blob not null,
	hash blob not null
)";

const ADD_MESSAGE: &str = "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?)";

const SELECT_MESSAGES: &str = "SELECT sender, timestamp, content, hash_sent, all_verified FROM messages";

const ADD_HASH: &str = "INSERT INTO hashes VALUES (?, ?, ?, ?)";

const MAX_HASHES_PER_MESSAGE: usize = u8::MAX as usize;

#[derive(Clone, Debug)]
pub struct HistoryEntry {
	pub sender: SufecAddr,
	pub timestamp: u64,
	pub content: MessageContent,
	pub all_verified: bool,
	pub hash_sent: bool,
}
impl HistoryEntry {
	pub fn hash(&self) -> Digest {
		hash(&[self.sender.to_bytes(), self.content.to_bytes()].concat())
	}
}