From cb72d049e0dc0aec5c9710eb2d7813604c651db3 Mon Sep 17 00:00:00 2001 From: oxalica Date: Tue, 17 Sep 2024 17:31:20 -0400 Subject: [PATCH] feat(types,webapi): impl id_key/act_key for all APIs and update docs --- blah-types/Cargo.toml | 2 +- blah-types/src/lib.rs | 57 +++++---- blahctl/src/main.rs | 28 +++-- blahd/schema.sql | 14 ++- blahd/src/database.rs | 32 ++++- blahd/src/event.rs | 22 ++-- blahd/src/lib.rs | 191 +++++++++++++--------------- blahd/src/register.rs | 29 +++-- blahd/tests/webapi.rs | 287 +++++++++++++++++++++++------------------- docs/webapi.yaml | 94 ++++++++------ 10 files changed, 426 insertions(+), 330 deletions(-) diff --git a/blah-types/Cargo.toml b/blah-types/Cargo.toml index a716980..7b8925c 100644 --- a/blah-types/Cargo.toml +++ b/blah-types/Cargo.toml @@ -16,7 +16,7 @@ serde = { version = "1", features = ["derive"] } serde_jcs = "0.1" serde_json = "1" serde_with = "3.9.0" -url = "2" +url = { version = "2", features = ["serde"] } [dev-dependencies] expect-test = "1.5.0" diff --git a/blah-types/src/lib.rs b/blah-types/src/lib.rs index 3fd9606..76d68b4 100644 --- a/blah-types/src/lib.rs +++ b/blah-types/src/lib.rs @@ -23,7 +23,7 @@ pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UserIdentityDesc { /// User primary identity key, only for signing action keys. - pub id_key: UserKey, + pub id_key: PubKey, /// User action subkeys, signed by the identity key. pub act_keys: Vec>, /// User profile, signed by any valid action key. @@ -38,7 +38,7 @@ impl UserIdentityDesc { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "typ", rename = "user_act_key")] pub struct UserActKeyDesc { - pub act_key: UserKey, + pub act_key: PubKey, pub expire_time: u64, pub comment: String, } @@ -83,10 +83,16 @@ impl WithMsgId { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct UserKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]); +pub struct UserKey { + pub id_key: PubKey, + pub act_key: PubKey, +} -impl fmt::Display for UserKey { +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PubKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]); + +impl fmt::Display for PubKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut buf = [0u8; PUBLIC_KEY_LENGTH * 2]; hex::encode_to_slice(self.0, &mut buf).expect("buf size is correct"); @@ -108,6 +114,7 @@ pub struct Signee { pub nonce: u32, pub payload: T, pub timestamp: u64, + #[serde(flatten)] pub user: UserKey, } @@ -126,7 +133,8 @@ impl Signed { /// Sign the payload with the given `key`. pub fn sign( - key: &SigningKey, + id_key: &PubKey, + act_key: &SigningKey, timestamp: u64, rng: &mut (impl RngCore + ?Sized), payload: T, @@ -135,10 +143,13 @@ impl Signed { nonce: rng.next_u32(), payload, timestamp, - user: UserKey(key.verifying_key().to_bytes()), + user: UserKey { + act_key: PubKey(act_key.verifying_key().to_bytes()), + id_key: id_key.clone(), + }, }; let canonical_signee = serde_jcs::to_vec(&signee).expect("serialization cannot fail"); - let sig = key.try_sign(&canonical_signee)?.to_bytes(); + let sig = act_key.try_sign(&canonical_signee)?.to_bytes(); Ok(Self { sig, signee }) } @@ -146,7 +157,7 @@ impl Signed { /// /// Note that this does not check validity of timestamp and other data. pub fn verify(&self) -> Result<(), SignatureError> { - VerifyingKey::from_bytes(&self.signee.user.0)? + VerifyingKey::from_bytes(&self.signee.user.act_key.0)? .verify_strict(&self.canonical_signee(), &Signature::from_bytes(&self.sig))?; Ok(()) } @@ -158,7 +169,7 @@ impl Signed { pub struct UserRegisterPayload { pub server_url: Url, pub id_url: Url, - pub id_key: UserKey, + pub id_key: PubKey, pub challenge_nonce: u32, } @@ -387,7 +398,7 @@ pub struct RoomMetadata { pub member_permission: Option, /// The peer user, if this is a peer chat room. #[serde(skip_serializing_if = "Option::is_none")] - pub peer_user: Option, + pub peer_user: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -409,7 +420,7 @@ pub struct CreateGroup { /// Peer-to-peer chat room with exactly two symmetric users. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CreatePeerChat { - pub peer: UserKey, + pub peer: PubKey, } /// A collection of room members, with these invariants: @@ -443,7 +454,7 @@ impl TryFrom> for RoomMemberList { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoomMember { pub permission: MemberPermission, - pub user: UserKey, + pub user: PubKey, } /// Proof of room membership for read-access. @@ -466,10 +477,10 @@ pub struct RoomAdminPayload { pub enum RoomAdminOp { AddMember { permission: MemberPermission, - user: UserKey, + user: PubKey, }, RemoveMember { - user: UserKey, + user: PubKey, }, // TODO: RU } @@ -533,19 +544,19 @@ mod sql_impl { } } - impl ToSql for UserKey { + impl ToSql for PubKey { fn to_sql(&self) -> Result> { // TODO: Extensive key format? self.0.to_sql() } } - impl FromSql for UserKey { + impl FromSql for PubKey { fn column_result(value: ValueRef<'_>) -> FromSqlResult { let rawkey = <[u8; PUBLIC_KEY_LENGTH]>::column_result(value)?; let key = VerifyingKey::from_bytes(&rawkey) .map_err(|err| FromSqlError::Other(format!("invalid pubkey: {err}").into()))?; - Ok(UserKey(key.to_bytes())) + Ok(PubKey(key.to_bytes())) } } @@ -596,10 +607,12 @@ mod tests { #[test] fn canonical_msg() { let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1); - let signing_key = SigningKey::from_bytes(&[0x42; 32]); + let id_key = SigningKey::from_bytes(&[0x42; 32]); + let act_key = SigningKey::from_bytes(&[0x43; 32]); let timestamp = 0xDEAD_BEEF; let msg = Signed::sign( - &signing_key, + &PubKey(id_key.verifying_key().to_bytes()), + &act_key, timestamp, &mut fake_rng, ChatPayload { @@ -611,7 +624,7 @@ mod tests { let json = serde_jcs::to_string(&msg).unwrap(); let expect = expect![[ - r#"{"sig":"18ee190722bebfd438c82f34890540d91578b4ba9f6c0c6011cc4fd751a321e32e9442d00dad1920799c54db011694c72a9ba993b408922e9997119209aa5e09","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"# + r#"{"sig":"74ca2895ac94e741e086bae28ce8c282bf375e3e59a3408f562420d72e98d799f7e627879aa883fa0804a0799eb9b90398150b0150c2e3550635ff28b9991502","signee":{"act_key":"22fc297792f0b6ffc0bfcfdb7edb0c0aa14e025a365ec0e342e86e3829cb74b6","id_key":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559}}"# ]]; expect.assert_eq(&json); @@ -657,7 +670,7 @@ mod tests { room: Id(42), op: RoomAdminOp::AddMember { permission: MemberPermission::POST_CHAT, - user: UserKey([0x42; PUBLIC_KEY_LENGTH]), + user: PubKey([0x42; PUBLIC_KEY_LENGTH]), }, }; let raw = serde_jcs::to_string(&data).unwrap(); diff --git a/blahctl/src/main.rs b/blahctl/src/main.rs index b309f6f..18deabd 100644 --- a/blahctl/src/main.rs +++ b/blahctl/src/main.rs @@ -4,8 +4,8 @@ use std::{fs, io}; use anyhow::{Context, Result}; use blah_types::{ - bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, RichText, RoomAttrs, - ServerPermission, Signed, UserKey, + bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, PubKey, RichText, + RoomAttrs, ServerPermission, Signed, }; use ed25519_dalek::pkcs8::spki::der::pem::LineEnding; use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}; @@ -126,9 +126,9 @@ fn userkey_parser(s: &str) -> clap::error::Result { } impl User { - async fn fetch_key(&self) -> Result { + async fn fetch_key(&self) -> Result { let rawkey = if let Some(key) = &self.key { - return Ok(UserKey(key.to_bytes())); + return Ok(PubKey(key.to_bytes())); } else if let Some(path) = &self.public_key_file { fs::read_to_string(path).context("failed to read key file")? } else if let Some(url) = &self.url { @@ -140,7 +140,7 @@ impl User { let key = VerifyingKey::from_public_key_pem(&rawkey) .context("invalid key")? .to_bytes(); - Ok(UserKey(key)) + Ok(PubKey(key)) } } @@ -221,7 +221,14 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> { attrs: attrs.unwrap_or_default(), title, }); - let payload = Signed::sign(&key, get_timestamp(), &mut OsRng, payload)?; + // FIXME: Same key. + let payload = Signed::sign( + &PubKey(key.to_bytes()), + &key, + get_timestamp(), + &mut OsRng, + payload, + )?; let ret = client .post(api_url.join("/room/create")?) @@ -243,7 +250,14 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> { room: Id(room), rich_text: RichText::from(text), }; - let payload = Signed::sign(&key, get_timestamp(), &mut OsRng, payload)?; + // FIXME: Same key. + let payload = Signed::sign( + &PubKey(key.to_bytes()), + &key, + get_timestamp(), + &mut OsRng, + payload, + )?; let ret = client .post(api_url.join(&format!("/room/{room}/msg"))?) diff --git a/blahd/schema.sql b/blahd/schema.sql index 40fa8e0..1dd4098 100644 --- a/blahd/schema.sql +++ b/blahd/schema.sql @@ -2,8 +2,8 @@ -- implemented and layout can change at any time. CREATE TABLE IF NOT EXISTS `user` ( - `uid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - `userkey` BLOB NOT NULL UNIQUE, + `uid` INTEGER NOT NULL PRIMARY KEY, + `id_key` BLOB NOT NULL UNIQUE, `permission` INTEGER NOT NULL DEFAULT 0, `last_fetch_time` INTEGER NOT NULL, `id_desc` TEXT NOT NULL @@ -15,7 +15,13 @@ CREATE TABLE IF NOT EXISTS `user_act_key` ( `expire_time` INTEGER NOT NULL, PRIMARY KEY (`uid`, `act_key`) -) STRICT; +) STRICT, WITHOUT ROWID; + +CREATE VIEW IF NOT EXISTS `valid_user_act_key` AS + SELECT `act_key`, `user`.* + FROM `user_act_key` + JOIN `user` USING (`uid`) + WHERE unixepoch() < `expire_time`; -- The highest bit of `rid` will be set for peer chat room. -- So simply comparing it against 0 can filter them out. @@ -53,6 +59,8 @@ CREATE TABLE IF NOT EXISTS `msg` ( `cid` INTEGER NOT NULL PRIMARY KEY, `rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE, `uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT, + -- Optionally references `user_act_key`(`act_key`) + `act_key` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, `nonce` INTEGER NOT NULL, `sig` BLOB NOT NULL, diff --git a/blahd/src/database.rs b/blahd/src/database.rs index c4c5d07..06b6f0a 100644 --- a/blahd/src/database.rs +++ b/blahd/src/database.rs @@ -1,12 +1,17 @@ +use std::borrow::Borrow; use std::ops::DerefMut; use std::path::PathBuf; use anyhow::{ensure, Context, Result}; +use axum::http::StatusCode; +use blah_types::{ServerPermission, UserKey}; use parking_lot::Mutex; -use rusqlite::{params, Connection, OpenFlags}; +use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; use serde::Deserialize; use serde_inline_default::serde_inline_default; +use crate::ApiError; + const DEFAULT_DATABASE_PATH: &str = "/var/lib/blahd/db.sqlite"; static INIT_SQL: &str = include_str!("../schema.sql"); @@ -97,6 +102,31 @@ impl Database { } } +pub trait ConnectionExt: Borrow { + fn get_user(&self, user: &UserKey) -> Result<(i64, ServerPermission), ApiError> { + self.borrow() + .query_row( + r" + SELECT `uid`, `permission` + FROM `valid_user_act_key` + WHERE (`id_key`, `act_key`) = (?, ?) + ", + params![user.id_key, user.act_key], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()? + .ok_or_else(|| { + error_response!( + StatusCode::NOT_FOUND, + "not_found", + "the user does not exist", + ) + }) + } +} + +impl ConnectionExt for Connection {} + #[test] fn init_sql_valid() { let conn = Connection::open_in_memory().unwrap(); diff --git a/blahd/src/event.rs b/blahd/src/event.rs index d787201..ce8ca81 100644 --- a/blahd/src/event.rs +++ b/blahd/src/event.rs @@ -7,20 +7,20 @@ use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; -use anyhow::{bail, Context as _, Result}; +use anyhow::{anyhow, bail, Context as _, Result}; use axum::extract::ws::{Message, WebSocket}; use blah_types::{AuthPayload, Signed, SignedChatMsg}; use futures_util::future::Either; use futures_util::stream::SplitSink; use futures_util::{stream_select, SinkExt as _, Stream, StreamExt}; use parking_lot::Mutex; -use rusqlite::{params, OptionalExtension}; use serde::{de, Deserialize, Serialize}; use serde_inline_default::serde_inline_default; use tokio::sync::broadcast; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tokio_stream::wrappers::BroadcastStream; +use crate::database::ConnectionExt; use crate::AppState; #[derive(Debug, Deserialize)] @@ -143,19 +143,13 @@ pub async fn handle_ws(st: Arc, ws: &mut WebSocket) -> Result>(&payload)?; st.verify_signed_data(&auth)?; - st.db + let (uid, _) = st + .db .get() - .query_row( - r" - SELECT `uid` - FROM `user` - WHERE `userkey` = ? - ", - params![auth.signee.user], - |row| row.get::<_, u64>(0), - ) - .optional()? - .context("invalid user")? + .get_user(&auth.signee.user) + .map_err(|err| anyhow!("{}", err.message))?; + // FIXME: Consistency of id's sign. + uid as u64 }; tracing::debug!(%uid, "user connected"); diff --git a/blahd/src/lib.rs b/blahd/src/lib.rs index ef02738..ac8957e 100644 --- a/blahd/src/lib.rs +++ b/blahd/src/lib.rs @@ -15,6 +15,7 @@ use blah_types::{ RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signed, SignedChatMsg, Signee, UserKey, UserRegisterPayload, WithMsgId, }; +use database::ConnectionExt; use ed25519_dalek::SIGNATURE_LENGTH; use id::IdExt; use middleware::{Auth, MaybeAuth, ResultExt as _, SignedJson}; @@ -188,10 +189,10 @@ async fn user_get( .query_row( " SELECT 1 - FROM `user` - WHERE `userkey` = ? + FROM `valid_user_act_key` + WHERE (`id_key`, `act_key`) = (?, ?) ", - params![user], + params![user.id_key, user.act_key], |_| Ok(()), ) .optional()?, @@ -271,7 +272,10 @@ async fn room_list( signee: Signee { nonce: row.get("nonce")?, timestamp: row.get("timestamp")?, - user: row.get("userkey")?, + user: UserKey { + act_key: row.get("act_key")?, + id_key: row.get("id_key")?, + }, payload: ChatPayload { rich_text: row.get("rich_text")?, room: rid, @@ -290,7 +294,7 @@ async fn room_list( .filter(|cid| cid.0 != 0), unseen_cnt: row.get("unseen_cnt").ok(), member_permission: row.get("member_perm").ok(), - peer_user: row.get("peer_userkey").ok(), + peer_user: row.get("peer_id_key").ok(), }) })? .collect::, _>>()?; @@ -303,7 +307,8 @@ async fn room_list( ListRoomFilter::Public => query( r" SELECT `rid`, `title`, `attrs`, 0 AS `last_seen_cid`, - `cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text` + `cid`, `timestamp`, `nonce`, `sig`, `rich_text`, + `last_author`.`id_key`, `msg`.`act_key` FROM `room` LEFT JOIN `msg` USING (`rid`) LEFT JOIN `user` AS `last_author` USING (`uid`) @@ -325,16 +330,17 @@ async fn room_list( r" SELECT `rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`, - `cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`, - `peer_user`.`userkey` AS `peer_userkey` - FROM `user` + `cid`, `timestamp`, `nonce`, `sig`, `rich_text`, + `last_author`.`id_key`, `msg`.`act_key`, + `peer_user`.`id_key` AS `peer_id_key` + FROM `valid_user_act_key` AS `me` JOIN `room_member` USING (`uid`) JOIN `room` USING (`rid`) LEFT JOIN `msg` USING (`rid`) LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`) LEFT JOIN `user` AS `peer_user` ON - (`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`) - WHERE `user`.`userkey` = :userkey AND + (`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `me`.`uid`) + WHERE (`me`.`id_key`, `me`.`act_key`) = (:id_key, :act_key) AND `rid` > :start_rid GROUP BY `rid` HAVING `cid` IS MAX(`cid`) ORDER BY `rid` ASC @@ -343,7 +349,8 @@ async fn room_list( named_params! { ":start_rid": start_rid, ":page_len": page_len, - ":userkey": user, + ":id_key": user.id_key, + ":act_key": user.act_key, }, ) } @@ -353,20 +360,21 @@ async fn room_list( r" SELECT `rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`, - `cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`, - `peer_user`.`userkey` AS `peer_userkey`, + `cid`, `timestamp`, `nonce`, `sig`, `rich_text`, + `last_author`.`id_key`, `msg`.`act_key`, + `peer_user`.`id_key` AS `peer_id_key`, (SELECT COUNT(*) FROM `msg` AS `unseen_msg` WHERE `unseen_msg`.`rid` = `room`.`rid` AND `last_seen_cid` < `unseen_msg`.`cid`) AS `unseen_cnt` - FROM `user` + FROM `valid_user_act_key` AS `me` JOIN `room_member` USING (`uid`) JOIN `room` USING (`rid`) LEFT JOIN `msg` USING (`rid`) LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`) LEFT JOIN `user` AS `peer_user` ON - (`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`) - WHERE `user`.`userkey` = :userkey AND + (`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `me`.`uid`) + WHERE (`me`.`id_key`, `me`.`act_key`) = (:id_key, :act_key) AND `rid` > :start_rid AND `cid` > `last_seen_cid` GROUP BY `rid` HAVING `cid` IS MAX(`cid`) @@ -376,7 +384,8 @@ async fn room_list( named_params! { ":start_rid": start_rid, ":page_len": page_len, - ":userkey": user, + ":id_key": user.id_key, + ":act_key": user.act_key, }, ) } @@ -389,14 +398,16 @@ async fn room_create( SignedJson(params): SignedJson, ) -> Result, ApiError> { match params.signee.payload { - CreateRoomPayload::Group(op) => room_create_group(&st, params.signee.user, op).await, - CreateRoomPayload::PeerChat(op) => room_create_peer_chat(&st, params.signee.user, op).await, + CreateRoomPayload::Group(op) => room_create_group(&st, ¶ms.signee.user, op).await, + CreateRoomPayload::PeerChat(op) => { + room_create_peer_chat(&st, ¶ms.signee.user, op).await + } } } async fn room_create_group( st: &AppState, - user: UserKey, + user: &UserKey, op: CreateGroup, ) -> Result, ApiError> { if !RoomAttrs::GROUP_ATTRS.contains(op.attrs) { @@ -412,10 +423,10 @@ async fn room_create_group( .query_row( r" SELECT `uid`, `permission` - FROM `user` - WHERE `userkey` = ? + FROM `valid_user_act_key` + WHERE (`id_key`, `act_key`) = (?, ?) ", - params![user], + params![user.id_key, user.act_key], |row| { Ok(( row.get::<_, i64>("uid")?, @@ -462,11 +473,11 @@ async fn room_create_group( async fn room_create_peer_chat( st: &AppState, - src_user: UserKey, + src_user: &UserKey, op: CreatePeerChat, ) -> Result, ApiError> { - let tgt_user = op.peer; - if tgt_user == src_user { + let tgt_user_id_key = op.peer; + if tgt_user_id_key == src_user.id_key { return Err(error_response!( StatusCode::NOT_IMPLEMENTED, "not_implemented", @@ -478,38 +489,19 @@ async fn room_create_peer_chat( let mut conn = st.db.get(); let txn = conn.transaction()?; - let src_uid = txn + let (src_uid, _) = txn.get_user(src_user)?; + let (tgt_uid, _) = txn .query_row( r" - SELECT `uid` FROM `user` - WHERE `userkey` = ? - ", - params![src_user], - |row| row.get::<_, i64>(0), - ) - .optional()? - .ok_or_else(|| { - error_response!( - StatusCode::NOT_FOUND, - "not_found", - "the user does not exist", - ) - })?; - let tgt_uid = txn - .query_row( - r" - SELECT `uid` + SELECT `uid`, `permission` FROM `user` - WHERE `userkey` = :userkey AND - `permission` & :perm = :perm + WHERE `id_key` = ? ", - named_params! { - ":userkey": tgt_user, - ":perm": ServerPermission::ACCEPT_PEER_CHAT, - }, - |row| row.get::<_, i64>(0), + params![tgt_user_id_key], + |row| Ok((row.get::<_, i64>(0)?, row.get::<_, ServerPermission>(1)?)), ) .optional()? + .filter(|(_, perm)| perm.contains(ServerPermission::ACCEPT_PEER_CHAT)) .ok_or_else(|| { error_response!( StatusCode::NOT_FOUND, @@ -518,11 +510,8 @@ async fn room_create_peer_chat( ) })?; - let (peer1, peer2) = if src_uid <= tgt_uid { - (src_uid, tgt_uid) - } else { - (tgt_uid, src_uid) - }; + let mut peers = [src_uid, tgt_uid]; + peers.sort(); let rid = Id::gen_peer_chat_rid(); let updated = txn.execute( r" @@ -533,8 +522,8 @@ async fn room_create_peer_chat( named_params! { ":rid": rid, ":attrs": RoomAttrs::PEER_CHAT, - ":peer1": peer1, - ":peer2": peer2, + ":peer1": peers[0], + ":peer2": peers[1], }, )?; if updated == 0 { @@ -553,7 +542,7 @@ async fn room_create_peer_chat( ", )?; // TODO: Limit permission of the src user? - for uid in [peer1, peer2] { + for uid in peers { stmt.execute(named_params! { ":rid": rid, ":uid": uid, @@ -656,7 +645,8 @@ async fn room_get_feed( .map(|WithMsgId { cid, msg }| { let time = SystemTime::UNIX_EPOCH + Duration::from_secs(msg.signee.timestamp); let author = FeedAuthor { - name: msg.signee.user.to_string(), + // TODO: Retrieve id_url as name. + name: msg.signee.user.id_key.to_string(), }; FeedItem { id: cid.to_string(), @@ -747,6 +737,11 @@ fn get_room_if_readable( user: Option<&UserKey>, f: impl FnOnce(&Row<'_>) -> rusqlite::Result, ) -> Result { + let (id_key, act_key) = match user { + Some(keys) => (Some(&keys.id_key), Some(&keys.act_key)), + None => (None, None), + }; + conn.query_row( r" SELECT `title`, `attrs` @@ -755,14 +750,15 @@ fn get_room_if_readable( ((`attrs` & :perm) = :perm OR EXISTS(SELECT 1 FROM `room_member` - JOIN `user` USING (`uid`) + JOIN `valid_user_act_key` USING (`uid`) WHERE `room_member`.`rid` = `room`.`rid` AND - `userkey` = :userkey)) + (`id_key`, `act_key`) = (:id_key, :act_key))) ", named_params! { ":rid": rid, ":perm": RoomAttrs::PUBLIC_READABLE, - ":userkey": user, + ":id_key": id_key, + ":act_key": act_key, }, f, ) @@ -787,7 +783,7 @@ fn query_room_msgs( let page_len = pagination.effective_page_len(st); let mut stmt = conn.prepare( r" - SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text` + SELECT `cid`, `timestamp`, `nonce`, `sig`, `id_key`, `act_key`, `sig`, `rich_text` FROM `msg` JOIN `user` USING (`uid`) WHERE `rid` = :rid AND @@ -813,7 +809,10 @@ fn query_room_msgs( signee: Signee { nonce: row.get("nonce")?, timestamp: row.get("timestamp")?, - user: row.get("userkey")?, + user: UserKey { + id_key: row.get("id_key")?, + act_key: row.get("act_key")?, + }, payload: ChatPayload { room: rid, rich_text: row.get("rich_text")?, @@ -850,13 +849,14 @@ async fn room_msg_post( r" SELECT `uid`, `room_member`.`permission` FROM `room_member` - JOIN `user` USING (`uid`) + JOIN `valid_user_act_key` USING (`uid`) WHERE `rid` = :rid AND - `userkey` = :userkey + (`id_key`, `act_key`) = (:id_key, :act_key) ", named_params! { ":rid": rid, - ":userkey": &chat.signee.user, + ":id_key": &chat.signee.user.id_key, + ":act_key": &chat.signee.user.act_key, }, |row| { Ok(( @@ -885,13 +885,14 @@ async fn room_msg_post( let cid = Id::gen(); conn.execute( r" - INSERT INTO `msg` (`cid`, `rid`, `uid`, `timestamp`, `nonce`, `sig`, `rich_text`) - VALUES (:cid, :rid, :uid, :timestamp, :nonce, :sig, :rich_text) + INSERT INTO `msg` (`cid`, `rid`, `uid`, `act_key`, `timestamp`, `nonce`, `sig`, `rich_text`) + VALUES (:cid, :rid, :uid, :act_key, :timestamp, :nonce, :sig, :rich_text) ", named_params! { ":cid": cid, ":rid": rid, ":uid": uid, + ":act_key": chat.signee.user.act_key, ":timestamp": chat.signee.timestamp, ":nonce": chat.signee.nonce, ":rich_text": &chat.signee.payload.rich_text, @@ -954,7 +955,7 @@ async fn room_admin( match op.signee.payload.op { RoomAdminOp::AddMember { user, permission } => { - if user != op.signee.user { + if user != op.signee.user.id_key { return Err(error_response!( StatusCode::NOT_IMPLEMENTED, "not_implemented", @@ -968,17 +969,17 @@ async fn room_admin( "invalid permission", )); } - room_join(&st, rid, user, permission).await?; + room_join(&st, rid, &op.signee.user, permission).await?; } RoomAdminOp::RemoveMember { user } => { - if user != op.signee.user { + if user != op.signee.user.id_key { return Err(error_response!( StatusCode::NOT_IMPLEMENTED, "not_implemented", "only self-removing is implemented yet", )); } - room_leave(&st, rid, user).await?; + room_leave(&st, rid, &op.signee.user).await?; } } @@ -988,29 +989,12 @@ async fn room_admin( async fn room_join( st: &AppState, rid: Id, - user: UserKey, + user: &UserKey, permission: MemberPermission, ) -> Result<(), ApiError> { let mut conn = st.db.get(); let txn = conn.transaction()?; - let uid = txn - .query_row( - r" - SELECT `uid` - FROM `user` - WHERE `userkey` = ? - ", - params![user], - |row| row.get::<_, i32>(0), - ) - .optional()? - .ok_or_else(|| { - error_response!( - StatusCode::NOT_FOUND, - "not_found", - "the user does not exist", - ) - })?; + let (uid, _) = txn.get_user(user)?; txn.query_row( r" SELECT `attrs` @@ -1053,7 +1037,7 @@ async fn room_join( Ok(()) } -async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiError> { +async fn room_leave(st: &AppState, rid: Id, user: &UserKey) -> Result<(), ApiError> { let mut conn = st.db.get(); let txn = conn.transaction()?; @@ -1062,13 +1046,13 @@ async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiErro r" SELECT `uid` FROM `room_member` - JOIN `user` USING (`uid`) - WHERE `rid` = :rid AND - `userkey` = :userkey + JOIN `valid_user_act_key` USING (`uid`) + WHERE (`rid`, `id_key`, `act_key`) = (:rid, :id_key, :act_key) ", named_params! { ":rid": rid, - ":userkey": user, + ":id_key": user.id_key, + ":act_key": user.act_key, }, |row| row.get::<_, u64>("uid"), ) @@ -1108,12 +1092,15 @@ async fn room_msg_mark_seen( SET `last_seen_cid` = MAX(`last_seen_cid`, :cid) WHERE `rid` = :rid AND - `uid` = (SELECT `uid` FROM `user` WHERE `userkey` = :userkey) + `uid` = (SELECT `uid` + FROM `valid_user_act_key` + WHERE (`id_key`, `act_key`) = (:id_key, :act_key)) ", named_params! { ":cid": cid, ":rid": rid, - ":userkey": user, + ":id_key": user.id_key, + ":act_key": user.act_key, }, )?; diff --git a/blahd/src/register.rs b/blahd/src/register.rs index bada8d4..66476aa 100644 --- a/blahd/src/register.rs +++ b/blahd/src/register.rs @@ -4,7 +4,7 @@ use std::time::{Duration, Instant}; use anyhow::{anyhow, ensure, Context}; use axum::http::{HeaderMap, HeaderName, StatusCode}; use blah_types::{ - get_timestamp, Signed, UserIdentityDesc, UserKey, UserRegisterPayload, X_BLAH_DIFFICULTY, + get_timestamp, PubKey, Signed, UserIdentityDesc, UserRegisterPayload, X_BLAH_DIFFICULTY, X_BLAH_NONCE, }; use http_body_util::BodyExt; @@ -270,9 +270,9 @@ pub async fn user_register( let uid = txn .query_row( r" - INSERT INTO `user` (`userkey`, `last_fetch_time`, `id_desc`) + INSERT INTO `user` (`id_key`, `last_fetch_time`, `id_desc`) VALUES (:id_key, :last_fetch_time, :id_desc) - ON CONFLICT (`userkey`) DO UPDATE SET + ON CONFLICT (`id_key`) DO UPDATE SET `last_fetch_time` = :last_fetch_time, `id_desc` = :id_desc WHERE `last_fetch_time` < :last_fetch_time @@ -323,20 +323,31 @@ pub async fn user_register( fn validate_id_desc( id_url: &Url, - id_key: &UserKey, + id_key: &PubKey, id_desc: &UserIdentityDesc, now: u64, ) -> anyhow::Result<()> { ensure!(*id_key == id_desc.id_key, "id_key mismatch"); - let profile_signing_key = &id_desc.profile.signee.user; + ensure!( + *id_key == id_desc.profile.signee.user.id_key, + "profile id_key mismatch", + ); + let profile_signing_key = &id_desc.profile.signee.user.act_key; let mut profile_signed = false; - for (i, act_key) in id_desc.act_keys.iter().enumerate() { - let kdesc = &act_key.signee.payload; + for (i, signed_kdesc) in id_desc.act_keys.iter().enumerate() { + let kdesc = &signed_kdesc.signee.payload; (|| { - ensure!(act_key.signee.user == *id_key, "not signed by id_key"); - act_key.verify().context("signature verification failed")?; + // act_key itself is signed by id_key, so both are id_key here. + ensure!( + signed_kdesc.signee.user.id_key == *id_key + && signed_kdesc.signee.user.act_key == *id_key, + "not signed by id_key", + ); + signed_kdesc + .verify() + .context("signature verification failed")?; if now < kdesc.expire_time && *profile_signing_key == kdesc.act_key { profile_signed = true; } diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs index c79463a..3c322c7 100644 --- a/blahd/tests/webapi.rs +++ b/blahd/tests/webapi.rs @@ -11,7 +11,7 @@ use anyhow::Result; use axum::http::HeaderMap; use blah_types::{ get_timestamp, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, - MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, + MemberPermission, PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signed, SignedChatMsg, UserActKeyDesc, UserIdentityDesc, UserKey, UserProfile, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, X_BLAH_NONCE, }; @@ -52,14 +52,31 @@ unsafe_allow_id_url_custom_port = true ) }; -static ALICE_PRIV: LazyLock = LazyLock::new(|| SigningKey::from_bytes(&[b'A'; 32])); -static ALICE: LazyLock = LazyLock::new(|| UserKey(ALICE_PRIV.verifying_key().to_bytes())); -static BOB_PRIV: LazyLock = LazyLock::new(|| SigningKey::from_bytes(&[b'B'; 32])); -static BOB: LazyLock = LazyLock::new(|| UserKey(BOB_PRIV.verifying_key().to_bytes())); -static CAROL_PRIV: LazyLock = LazyLock::new(|| SigningKey::from_bytes(&[b'C'; 32])); -static CAROL: LazyLock = LazyLock::new(|| UserKey(CAROL_PRIV.verifying_key().to_bytes())); +struct User { + pubkeys: UserKey, + id_priv: SigningKey, + act_priv: SigningKey, +} -static CAROL_ACT_PRIV: LazyLock = LazyLock::new(|| SigningKey::from_bytes(&[b'c'; 32])); +impl User { + fn new(b: u8) -> Self { + assert!(b.is_ascii_uppercase()); + let id_priv = SigningKey::from_bytes(&[b; 32]); + let act_priv = SigningKey::from_bytes(&[b.to_ascii_lowercase(); 32]); + Self { + pubkeys: UserKey { + id_key: PubKey(id_priv.verifying_key().to_bytes()), + act_key: PubKey(act_priv.verifying_key().to_bytes()), + }, + id_priv, + act_priv, + } + } +} + +static ALICE: LazyLock = LazyLock::new(|| User::new(b'A')); +static BOB: LazyLock = LazyLock::new(|| User::new(b'B')); +static CAROL: LazyLock = LazyLock::new(|| User::new(b'C')); #[fixture] fn rng() -> impl RngCore { @@ -165,15 +182,25 @@ impl Server { .map_ok(|resp| resp.unwrap()) } + fn sign(&self, user: &User, msg: T) -> Signed { + Signed::sign( + &user.pubkeys.id_key, + &user.act_priv, + get_timestamp(), + &mut *self.rng.borrow_mut(), + msg, + ) + .unwrap() + } + fn create_room( &self, - key: &SigningKey, + user: &User, attrs: RoomAttrs, title: &str, ) -> impl Future> + use<'_> { - let req = sign( - key, - &mut *self.rng.borrow_mut(), + let req = self.sign( + user, CreateRoomPayload::Group(CreateGroup { attrs, title: title.to_string(), @@ -190,17 +217,16 @@ impl Server { fn join_room( &self, rid: Id, - key: &SigningKey, + user: &User, permission: MemberPermission, ) -> impl Future> + use<'_> { - let req = sign( - key, - &mut *self.rng.borrow_mut(), + let req = self.sign( + user, RoomAdminPayload { room: rid, op: RoomAdminOp::AddMember { permission, - user: UserKey(key.verifying_key().to_bytes()), + user: user.pubkeys.id_key.clone(), }, }, ); @@ -208,14 +234,13 @@ impl Server { .map_ok(|None| {}) } - fn leave_room(&self, rid: Id, key: &SigningKey) -> impl Future> + use<'_> { - let req = sign( - key, - &mut *self.rng.borrow_mut(), + fn leave_room(&self, rid: Id, user: &User) -> impl Future> + use<'_> { + let req = self.sign( + user, RoomAdminPayload { room: rid, op: RoomAdminOp::RemoveMember { - user: UserKey(key.verifying_key().to_bytes()), + user: user.pubkeys.id_key.clone(), }, }, ); @@ -226,12 +251,11 @@ impl Server { fn post_chat( &self, rid: Id, - key: &SigningKey, + user: &User, text: &str, ) -> impl Future>> + use<'_> { - let msg = sign( - key, - &mut *self.rng.borrow_mut(), + let msg = self.sign( + user, ChatPayload { room: rid, rich_text: text.into(), @@ -262,7 +286,7 @@ fn server() -> Server { let mut add_user = conn .prepare( r" - INSERT INTO `user` (`userkey`, `permission`, `last_fetch_time`, `id_desc`) + INSERT INTO `user` (`id_key`, `permission`, `last_fetch_time`, `id_desc`) VALUES (?, ?, 0, '{}') ", ) @@ -279,9 +303,13 @@ fn server() -> Server { (&*ALICE, ServerPermission::ALL), (&BOB, ServerPermission::empty()), ] { - add_user.execute(params![user, perm]).unwrap(); + add_user + .execute(params![user.pubkeys.id_key, perm]) + .unwrap(); let uid = conn.last_insert_rowid(); - add_act_key.execute(params![uid, user, i64::MAX]).unwrap(); + add_act_key + .execute(params![uid, user.pubkeys.act_key, i64::MAX]) + .unwrap(); } } let db = Database::from_raw(conn).unwrap(); @@ -314,12 +342,16 @@ async fn smoke(server: Server) { assert_eq!(got, exp); } -fn sign(key: &SigningKey, rng: &mut dyn RngCore, payload: T) -> Signed { - Signed::sign(key, get_timestamp(), rng, payload).unwrap() -} - -fn auth(key: &SigningKey, rng: &mut impl RngCore) -> String { - serde_json::to_string(&sign(key, rng, AuthPayload {})).unwrap() +fn auth(user: &User, rng: &mut impl RngCore) -> String { + let msg = Signed::sign( + &user.pubkeys.id_key, + &user.act_priv, + get_timestamp(), + rng, + AuthPayload {}, + ) + .unwrap(); + serde_json::to_string(&msg).unwrap() } #[rstest] @@ -345,26 +377,26 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ // Alice has permission. let rid = server - .create_room(&ALICE_PRIV, room_meta.attrs, title) + .create_room(&ALICE, room_meta.attrs, title) .await .unwrap(); room_meta.rid = rid; // Bob has no permission. server - .create_room(&BOB_PRIV, room_meta.attrs, title) + .create_room(&BOB, room_meta.attrs, title) .await .expect_api_err(StatusCode::FORBIDDEN, "permission_denied"); // Alice can always access it. let got_meta = server - .get::(&format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng))) + .get::(&format!("/room/{rid}"), Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!(got_meta, room_meta); // Bob or public can access it when it is public. - for auth in [None, Some(auth(&BOB_PRIV, rng))] { + for auth in [None, Some(auth(&BOB, rng))] { let resp = server .get::(&format!("/room/{rid}"), auth.as_deref()) .await; @@ -401,13 +433,13 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ .await .expect_api_err(StatusCode::UNAUTHORIZED, "unauthorized"); let got_joined = server - .get::("/room?filter=joined", Some(&auth(&ALICE_PRIV, rng))) + .get::("/room?filter=joined", Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!(got_joined, expect_list(true, Some(MemberPermission::ALL))); let got_joined = server - .get::("/room?filter=joined", Some(&auth(&BOB_PRIV, rng))) + .get::("/room?filter=joined", Some(&auth(&BOB, rng))) .await .unwrap(); assert_eq!(got_joined, expect_list(false, None)); @@ -417,41 +449,40 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ #[tokio::test] async fn room_join_leave(server: Server, ref mut rng: impl RngCore) { let rid_pub = server - .create_room(&ALICE_PRIV, RoomAttrs::PUBLIC_JOINABLE, "public room") + .create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public room") .await .unwrap(); let rid_priv = server - .create_room(&ALICE_PRIV, RoomAttrs::empty(), "private room") + .create_room(&ALICE, RoomAttrs::empty(), "private room") .await .unwrap(); - let join = - |rid: Id, key: &SigningKey| server.join_room(rid, key, MemberPermission::MAX_SELF_ADD); + let join = |rid, user| server.join_room(rid, user, MemberPermission::MAX_SELF_ADD); // Ok. - join(rid_pub, &BOB_PRIV).await.unwrap(); + join(rid_pub, &BOB).await.unwrap(); // Already joined. - join(rid_pub, &BOB_PRIV) + join(rid_pub, &BOB) .await .expect_api_err(StatusCode::CONFLICT, "exists"); // Not permitted. - join(rid_priv, &BOB_PRIV) + join(rid_priv, &BOB) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Not exists. - join(Id::INVALID, &BOB_PRIV) + join(Id::INVALID, &BOB) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Overly high permission. server - .join_room(rid_priv, &BOB_PRIV, MemberPermission::ALL) + .join_room(rid_priv, &BOB, MemberPermission::ALL) .await .expect_api_err(StatusCode::BAD_REQUEST, "deserialization"); // Bob is joined now. assert_eq!( server - .get::("/room?filter=joined", Some(&auth(&BOB_PRIV, rng))) + .get::("/room?filter=joined", Some(&auth(&BOB, rng))) .await .unwrap() .rooms @@ -459,20 +490,20 @@ async fn room_join_leave(server: Server, ref mut rng: impl RngCore) { 1, ); - let leave = |rid: Id, key: &SigningKey| server.leave_room(rid, key); + let leave = |rid, user| server.leave_room(rid, user); // Ok. - leave(rid_pub, &BOB_PRIV).await.unwrap(); + leave(rid_pub, &BOB).await.unwrap(); // Already left. - leave(rid_pub, &BOB_PRIV) + leave(rid_pub, &BOB) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Unpermitted and not inside. - leave(rid_priv, &BOB_PRIV) + leave(rid_priv, &BOB) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Invalid room. - leave(Id::INVALID, &BOB_PRIV) + leave(Id::INVALID, &BOB) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); } @@ -482,21 +513,20 @@ async fn room_join_leave(server: Server, ref mut rng: impl RngCore) { async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) { let rid_pub = server .create_room( - &ALICE_PRIV, + &ALICE, RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE, "public room", ) .await .unwrap(); let rid_priv = server - .create_room(&ALICE_PRIV, RoomAttrs::empty(), "private room") + .create_room(&ALICE, RoomAttrs::empty(), "private room") .await .unwrap(); - let mut chat = |rid: Id, key: &SigningKey, msg: &str| { - sign( - key, - rng, + let chat = |rid: Id, user: &User, msg: &str| { + server.sign( + user, ChatPayload { room: rid, rich_text: RichText::from(msg), @@ -510,8 +540,8 @@ async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) { }; // Ok. - let chat1 = chat(rid_pub, &ALICE_PRIV, "one"); - let chat2 = chat(rid_pub, &ALICE_PRIV, "two"); + let chat1 = chat(rid_pub, &ALICE, "one"); + let chat2 = chat(rid_pub, &ALICE, "two"); let cid1 = post(rid_pub, chat1.clone()).await.unwrap(); let cid2 = post(rid_pub, chat2.clone()).await.unwrap(); @@ -521,26 +551,26 @@ async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) { .expect_api_err(StatusCode::BAD_REQUEST, "duplicated_nonce"); // Wrong room. - post(rid_pub, chat(rid_priv, &ALICE_PRIV, "wrong room")) + post(rid_pub, chat(rid_priv, &ALICE, "wrong room")) .await .expect_api_err(StatusCode::BAD_REQUEST, "invalid_request"); // Not a member. - post(rid_pub, chat(rid_pub, &BOB_PRIV, "not a member")) + post(rid_pub, chat(rid_pub, &BOB, "not a member")) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Is a member but without permission. server - .join_room(rid_pub, &BOB_PRIV, MemberPermission::empty()) + .join_room(rid_pub, &BOB, MemberPermission::empty()) .await .unwrap(); - post(rid_pub, chat(rid_pub, &BOB_PRIV, "no permission")) + post(rid_pub, chat(rid_pub, &BOB, "no permission")) .await .expect_api_err(StatusCode::FORBIDDEN, "permission_denied"); // Room not exists. - post(Id::INVALID, chat(Id::INVALID, &ALICE_PRIV, "not permitted")) + post(Id::INVALID, chat(Id::INVALID, &ALICE, "not permitted")) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); @@ -605,19 +635,13 @@ async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) { // Not a member. server - .get::( - &format!("/room/{rid_priv}/msg"), - Some(&auth(&BOB_PRIV, rng)), - ) + .get::(&format!("/room/{rid_priv}/msg"), Some(&auth(&BOB, rng))) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Ok. let msgs = server - .get::( - &format!("/room/{rid_priv}/msg"), - Some(&auth(&ALICE_PRIV, rng)), - ) + .get::(&format!("/room/{rid_priv}/msg"), Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!(msgs, RoomMsgs::default()); @@ -629,18 +653,18 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) { let title = "public room"; let attrs = RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE; let member_perm = MemberPermission::ALL; - let rid = server.create_room(&ALICE_PRIV, attrs, title).await.unwrap(); + let rid = server.create_room(&ALICE, attrs, title).await.unwrap(); server - .join_room(rid, &BOB_PRIV, MemberPermission::MAX_SELF_ADD) + .join_room(rid, &BOB, MemberPermission::MAX_SELF_ADD) .await .unwrap(); - let alice_chat1 = server.post_chat(rid, &ALICE_PRIV, "alice1").await.unwrap(); - let alice_chat2 = server.post_chat(rid, &ALICE_PRIV, "alice2").await.unwrap(); + let alice_chat1 = server.post_chat(rid, &ALICE, "alice1").await.unwrap(); + let alice_chat2 = server.post_chat(rid, &ALICE, "alice2").await.unwrap(); // 2 new msgs. let rooms = server - .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .get::("/room?filter=unseen", Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!( @@ -660,21 +684,21 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) { } ); - let seen = |key: &SigningKey, cid: Id| { + let seen = |user: &User, cid: Id| { server.request::( Method::POST, &format!("/room/{rid}/msg/{cid}/seen"), - Some(&auth(key, &mut *server.rng.borrow_mut())), + Some(&auth(user, &mut *server.rng.borrow_mut())), None, ) }; // Mark the first one seen. - seen(&ALICE_PRIV, alice_chat1.cid).await.unwrap(); + seen(&ALICE, alice_chat1.cid).await.unwrap(); // 1 new msg. let rooms = server - .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .get::("/room?filter=unseen", Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!( @@ -695,17 +719,17 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) { ); // Mark the second one seen. Now there is no new messages. - seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap(); + seen(&ALICE, alice_chat2.cid).await.unwrap(); let rooms = server - .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .get::("/room?filter=unseen", Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!(rooms, RoomList::default()); // Marking a seen message seen is a no-op. - seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap(); + seen(&ALICE, alice_chat2.cid).await.unwrap(); let rooms = server - .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .get::("/room?filter=unseen", Some(&auth(&ALICE, rng))) .await .unwrap(); assert_eq!(rooms, RoomList::default()); @@ -714,11 +738,12 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) { #[rstest] #[tokio::test] async fn peer_chat(server: Server, ref mut rng: impl RngCore) { - let mut create_chat = |src: &SigningKey, tgt: &UserKey| { - let req = sign( + let create_chat = |src: &User, tgt: &User| { + let req = server.sign( src, - rng, - CreateRoomPayload::PeerChat(CreatePeerChat { peer: tgt.clone() }), + CreateRoomPayload::PeerChat(CreatePeerChat { + peer: tgt.pubkeys.id_key.clone(), + }), ); server .request::<_, Id>(Method::POST, "/room/create", None, Some(req)) @@ -726,15 +751,15 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) { }; // Bob disallows peer chat. - create_chat(&ALICE_PRIV, &BOB) + create_chat(&ALICE, &BOB) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Alice accepts bob. - let rid = create_chat(&BOB_PRIV, &ALICE).await.unwrap(); + let rid = create_chat(&BOB, &ALICE).await.unwrap(); // Room already exists. - create_chat(&BOB_PRIV, &ALICE) + create_chat(&BOB, &ALICE) .await .expect_api_err(StatusCode::CONFLICT, "exists"); @@ -750,7 +775,7 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) { .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Both alice and bob are in the room. - for (key, peer) in [(&*ALICE_PRIV, &*BOB), (&*BOB_PRIV, &*ALICE)] { + for (key, peer) in [(&*ALICE, &*BOB), (&*BOB, &*ALICE)] { let mut expect_meta = RoomMetadata { rid, title: None, @@ -769,7 +794,7 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) { assert_eq!(meta, expect_meta); expect_meta.member_permission = Some(MemberPermission::MAX_PEER_CHAT); - expect_meta.peer_user = Some(peer.clone()); + expect_meta.peer_user = Some(peer.pubkeys.id_key.clone()); let rooms = server .get::("/room?filter=joined", Some(&auth(key, rng))) .await @@ -789,14 +814,14 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) { async fn register(server: Server) { let rid = server .create_room( - &ALICE_PRIV, + &ALICE, RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE, "public room", ) .await .unwrap(); - let get_me = |user: Option<&SigningKey>| { + let get_me = |user: Option<&User>| { let auth = user.map(|user| auth(user, &mut *server.rng())); server .request::<(), ()>(Method::GET, "/user/me", auth.as_deref(), None) @@ -819,10 +844,10 @@ async fn register(server: Server) { }; // Alice is registered. - get_me(Some(&ALICE_PRIV)).await.unwrap(); + get_me(Some(&ALICE)).await.unwrap(); // Carol is not registered. - let (challenge_nonce, diff) = get_me(Some(&CAROL_PRIV)).await.unwrap_err(); + let (challenge_nonce, diff) = get_me(Some(&CAROL)).await.unwrap_err(); assert_eq!(diff, REGISTER_DIFFICULTY); // Without token. @@ -830,7 +855,7 @@ async fn register(server: Server) { assert_eq!(ret2, (challenge_nonce, diff)); let mut req = UserRegisterPayload { - id_key: CAROL.clone(), + id_key: CAROL.pubkeys.id_key.clone(), // Fake values. server_url: "http://invalid.example.com".parse().unwrap(), id_url: "file:///etc/passwd".parse().unwrap(), @@ -842,7 +867,7 @@ async fn register(server: Server) { .map_ok(|_| {}) }; let sign_with_difficulty = |req: &UserRegisterPayload, pass: bool| loop { - let signed = sign(&CAROL_PRIV, &mut *server.rng(), req.clone()); + let signed = server.sign(&CAROL, req.clone()); let mut h = Sha256::new(); h.update(signed.canonical_signee()); let h = h.finalize(); @@ -850,8 +875,7 @@ async fn register(server: Server) { return signed; } }; - let register_fast = - |req: &UserRegisterPayload| register(sign(&CAROL_PRIV, &mut *server.rng(), req.clone())); + let register_fast = |req: &UserRegisterPayload| register(server.sign(&CAROL, req.clone())); register_fast(&req) .await @@ -938,26 +962,32 @@ async fn register(server: Server) { (StatusCode::OK, desc.clone()) }} }; + let sign_profile = |url: Url| { + server.sign( + &CAROL, + UserProfile { + preferred_chat_server_urls: Vec::new(), + id_urls: vec![url], + }, + ) + }; let mut id_desc = { - let act_key = sign( - &CAROL_PRIV, + // Sign using id_key. + let act_key = Signed::sign( + &CAROL.pubkeys.id_key, + &CAROL.id_priv, + get_timestamp(), &mut *server.rng(), UserActKeyDesc { - act_key: UserKey(CAROL_ACT_PRIV.verifying_key().to_bytes()), + act_key: CAROL.pubkeys.act_key.clone(), expire_time: u64::MAX, comment: "comment".into(), }, - ); - let profile = sign( - &CAROL_ACT_PRIV, - &mut *server.rng(), - UserProfile { - preferred_chat_server_urls: Vec::new(), - id_urls: vec![req.id_url.join("/mismatch").unwrap()], - }, - ); + ) + .unwrap(); + let profile = sign_profile(req.id_url.join("/mismatch").unwrap()); UserIdentityDesc { - id_key: CAROL.clone(), + id_key: CAROL.pubkeys.id_key.clone(), act_keys: vec![act_key], profile, } @@ -970,28 +1000,21 @@ async fn register(server: Server) { .expect_api_err(StatusCode::UNAUTHORIZED, "invalid_id_description"); // Still not registered. - get_me(Some(&CAROL_PRIV)).await.unwrap_err(); + get_me(Some(&CAROL)).await.unwrap_err(); server - .join_room(rid, &CAROL_PRIV, MemberPermission::MAX_SELF_ADD) + .join_room(rid, &CAROL, MemberPermission::MAX_SELF_ADD) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Finally pass. - id_desc.profile = sign( - &CAROL_ACT_PRIV, - &mut *server.rng(), - UserProfile { - preferred_chat_server_urls: Vec::new(), - id_urls: vec![req.id_url.clone()], - }, - ); + id_desc.profile = sign_profile(req.id_url.clone()); set_id_desc(&id_desc); register(sign_with_difficulty(&req, true)).await.unwrap(); // Registered now. - get_me(Some(&CAROL_PRIV)).await.unwrap(); + get_me(Some(&CAROL)).await.unwrap(); server - .join_room(rid, &CAROL_PRIV, MemberPermission::MAX_SELF_ADD) + .join_room(rid, &CAROL, MemberPermission::MAX_SELF_ADD) .await .unwrap(); } diff --git a/docs/webapi.yaml b/docs/webapi.yaml index b5852e3..df1a44b 100644 --- a/docs/webapi.yaml +++ b/docs/webapi.yaml @@ -579,6 +579,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: type: object properties: @@ -597,6 +604,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: oneOf: @@ -625,19 +639,6 @@ components: user: type: string - - example: - sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09 - signee: - nonce: 670593955 - payload: - permission: 1 - room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd - typ: add_member - user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7 - timestamp: 1724966284 - user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7 - Signed-Chat: type: object properties: @@ -649,6 +650,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: type: object properties: @@ -659,16 +667,6 @@ components: type: string rich_text: $ref: '$/components/schemas/RichText' - example: - sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09 - signee: - nonce: 670593955 - payload: - typ: chat - room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd - rich_text: ["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"] - timestamp: 1724966284 - user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7 WithMsgId-Signed-Chat: allOf: @@ -690,6 +688,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: oneOf: - type: object @@ -707,20 +712,6 @@ components: peer: type: string - example: - sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09 - signee: - nonce: 670593955 - payload: - typ: create_room - attrs: 1 # PUBLIC_READABLE - title: 'hello room' - members: - - user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7 - permission: -1 - timestamp: 1724966284 - user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7 - Signed-UserRegister: type: object properties: @@ -732,6 +723,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: type: object properties: @@ -740,10 +738,14 @@ components: const: 'user_register' server_url: type: string - description: The server URL to register on. Must matches chat server's base_url. + description: | + The server URL to register on. Must matches chat server's base_url. + It's path segment must be normalized, eg. always contains a `/` path for top-level. id_url: type: string - description: The identity server URL. Must be in form `https://`. + description: | + The identity server URL. Must be in form `https:///`. + It's path segment must be normalized, eg. always contains a `/` path for top-level. id_key: type: string description: Hex encoded user primary key `id_key`. @@ -771,6 +773,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: type: object properties: @@ -796,6 +805,13 @@ components: nonce: type: integer format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string payload: type: object properties: