feat: Phase 9 — developer experience, extensibility, and community growth
New crates: - quicproquo-bot: Bot SDK with polling API + JSON pipe mode - quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset) - quicproquo-plugin-api: no_std C-compatible plugin vtable API - quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook) Server features: - ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth, channel, registration) with plugin rejection support - Dynamic plugin loader (libloading) with --plugin-dir config - Delivery proof canary tokens (Ed25519 server signatures on enqueue) - Key Transparency Merkle log with inclusion proofs on resolveUser Core library: - Safety numbers (60-digit HMAC-SHA256 key verification codes) - Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain) - Delivery proof verification utility - Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding) Client: - /verify REPL command for out-of-band key verification - Full-screen TUI via Ratatui (feature-gated --features tui) - qpq export / qpq export-verify CLI subcommands - KT inclusion proof verification on user resolution Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs, crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
@@ -11,7 +11,7 @@ use std::time::Duration;
|
||||
use anyhow::Context;
|
||||
use quicproquo_core::{
|
||||
AppMessage, DiskKeyStore, GroupMember, IdentityKeypair, ReceivedMessage,
|
||||
hybrid_encrypt, parse as parse_app_msg, serialize_chat,
|
||||
compute_safety_number, hybrid_encrypt, parse as parse_app_msg, serialize_chat,
|
||||
};
|
||||
use quicproquo_proto::node_capnp::node_service;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -57,6 +57,8 @@ enum SlashCommand {
|
||||
/// Mesh subcommands: /mesh peers, /mesh server <addr>
|
||||
MeshPeers,
|
||||
MeshServer { addr: String },
|
||||
/// Display safety number for out-of-band key verification with a contact.
|
||||
Verify { username: String },
|
||||
}
|
||||
|
||||
fn parse_input(line: &str) -> Input {
|
||||
@@ -135,6 +137,13 @@ fn parse_input(line: &str) -> Input {
|
||||
Input::Empty
|
||||
}
|
||||
},
|
||||
"/verify" => match arg {
|
||||
Some(username) => Input::Slash(SlashCommand::Verify { username }),
|
||||
None => {
|
||||
display::print_error("usage: /verify <username>");
|
||||
Input::Empty
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
display::print_error(&format!("unknown command: {cmd}. Try /help"));
|
||||
Input::Empty
|
||||
@@ -601,6 +610,7 @@ async fn handle_slash(
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
SlashCommand::Verify { username } => cmd_verify(session, client, &username).await,
|
||||
};
|
||||
if let Err(e) = result {
|
||||
display::print_error(&format!("{e:#}"));
|
||||
@@ -622,6 +632,7 @@ fn print_help() {
|
||||
display::print_status(" /whoami - Show your identity");
|
||||
display::print_status(" /mesh peers - Discover nearby qpq nodes via mDNS");
|
||||
display::print_status(" /mesh server <host:port> - Show how to reconnect to a mesh node");
|
||||
display::print_status(" /verify <username> - Show safety number for key verification");
|
||||
display::print_status(" /quit - Exit");
|
||||
}
|
||||
|
||||
@@ -1200,6 +1211,43 @@ fn cmd_history(session: &SessionState, count: usize) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_verify(
|
||||
session: &SessionState,
|
||||
client: &node_service::Client,
|
||||
username: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
// Resolve contact's identity key from the server.
|
||||
display::print_status(&format!("resolving {username}..."));
|
||||
let peer_key_vec = resolve_user(client, username)
|
||||
.await?
|
||||
.with_context(|| format!("user '{username}' not found"))?;
|
||||
|
||||
anyhow::ensure!(
|
||||
peer_key_vec.len() == 32,
|
||||
"server returned an identity key with unexpected length ({}); expected 32 bytes",
|
||||
peer_key_vec.len()
|
||||
);
|
||||
|
||||
let peer_key: [u8; 32] = peer_key_vec
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("length checked above");
|
||||
|
||||
let my_key: [u8; 32] = session.identity.public_key_bytes();
|
||||
|
||||
let safety_number = compute_safety_number(&my_key, &peer_key);
|
||||
|
||||
display::print_status(&format!("Safety number with @{username}:"));
|
||||
display::print_status("");
|
||||
display::print_status(&format!(" {safety_number}"));
|
||||
display::print_status("");
|
||||
display::print_status("Compare this number with your contact via a separate channel");
|
||||
display::print_status("(voice call, in person, or any out-of-band means).");
|
||||
display::print_status("If the numbers match, the connection has not been tampered with.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Sending ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn handle_send(
|
||||
|
||||
Reference in New Issue
Block a user