chore: fix all clippy warnings across workspace
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -4387,6 +4387,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
891
ROADMAP.html
Normal file
891
ROADMAP.html
Normal file
@@ -0,0 +1,891 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="navy sidebar-visible" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Full Roadmap (Phases 1–8) - quicproquo</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
<meta name="description" content="End-to-end encrypted group messaging over QUIC + TLS 1.3 + MLS (RFC 9420)">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon-de23e50b.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon-8114d1fc.png">
|
||||||
|
<link rel="stylesheet" href="css/variables-8adf115d.css">
|
||||||
|
<link rel="stylesheet" href="css/general-2459343d.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome-ae938929.css">
|
||||||
|
<link rel="stylesheet" href="css/print-9e4910d8.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="fonts/fonts-9644e21d.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" id="mdbook-highlight-css" href="highlight-493f70e1.css">
|
||||||
|
<link rel="stylesheet" id="mdbook-tomorrow-night-css" href="tomorrow-night-4c0ae647.css">
|
||||||
|
<link rel="stylesheet" id="mdbook-ayu-highlight-css" href="ayu-highlight-3fdfc3ac.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Provide site root and default themes to javascript -->
|
||||||
|
<script>
|
||||||
|
const path_to_root = "";
|
||||||
|
const default_light_theme = "navy";
|
||||||
|
const default_dark_theme = "navy";
|
||||||
|
window.path_to_searchindex_js = "searchindex-92ce38c7.js";
|
||||||
|
</script>
|
||||||
|
<!-- Start loading toc.js asap -->
|
||||||
|
<script src="toc-4c7c920d.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mdbook-help-container">
|
||||||
|
<div id="mdbook-help-popup">
|
||||||
|
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
|
||||||
|
<div>
|
||||||
|
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
|
||||||
|
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
|
||||||
|
<p>Press <kbd>?</kbd> to show this help</p>
|
||||||
|
<p>Press <kbd>Esc</kbd> to hide this help</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="mdbook-body-container">
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
let theme = localStorage.getItem('mdbook-theme');
|
||||||
|
let sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script>
|
||||||
|
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
||||||
|
let theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
const html = document.documentElement;
|
||||||
|
html.classList.remove('navy')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add("js");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="checkbox" id="mdbook-sidebar-toggle-anchor" class="hidden">
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script>
|
||||||
|
let sidebar = null;
|
||||||
|
const sidebar_toggle = document.getElementById("mdbook-sidebar-toggle-anchor");
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
} else {
|
||||||
|
sidebar = 'hidden';
|
||||||
|
sidebar_toggle.checked = false;
|
||||||
|
}
|
||||||
|
if (sidebar === 'visible') {
|
||||||
|
sidebar_toggle.checked = true;
|
||||||
|
} else {
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="mdbook-sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<!-- populated by js -->
|
||||||
|
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||||
|
<noscript>
|
||||||
|
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
|
||||||
|
</noscript>
|
||||||
|
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
|
||||||
|
<div class="sidebar-resize-indicator"></div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="mdbook-page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<div id="mdbook-menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="mdbook-menu-bar" class="menu-bar sticky">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<label id="mdbook-sidebar-toggle" class="icon-button" for="mdbook-sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="mdbook-sidebar">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></span>
|
||||||
|
</label>
|
||||||
|
<button id="mdbook-theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="mdbook-theme-list">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M371.3 367.1c27.3-3.9 51.9-19.4 67.2-42.9L600.2 74.1c12.6-19.5 9.4-45.3-7.6-61.2S549.7-4.4 531.1 9.6L294.4 187.2c-24 18-38.2 46.1-38.4 76.1L371.3 367.1zm-19.6 25.4l-116-104.4C175.9 290.3 128 339.6 128 400c0 3.9 .2 7.8 .6 11.6c1.8 17.5-10.2 36.4-27.8 36.4H96c-17.7 0-32 14.3-32 32s14.3 32 32 32H240c61.9 0 112-50.1 112-112c0-2.5-.1-5-.2-7.5z"/></svg></span>
|
||||||
|
</button>
|
||||||
|
<ul id="mdbook-theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-default_theme">Auto</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-ayu">Ayu</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="mdbook-search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="mdbook-searchbar">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z"/></svg></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">quicproquo</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<span class=fa-svg id="print-button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M128 0C92.7 0 64 28.7 64 64v96h64V64H354.7L384 93.3V160h64V93.3c0-17-6.7-33.3-18.7-45.3L400 18.7C388 6.7 371.7 0 354.7 0H128zM384 352v32 64H128V384 368 352H384zm64 32h32c17.7 0 32-14.3 32-32V256c0-35.3-28.7-64-64-64H64c-35.3 0-64 28.7-64 64v96c0 17.7 14.3 32 32 32H64v64c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V384zm-16-88c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24z"/></svg></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mdbook-search-wrapper" class="hidden">
|
||||||
|
<form id="mdbook-searchbar-outer" class="searchbar-outer">
|
||||||
|
<div class="search-wrapper">
|
||||||
|
<input type="search" id="mdbook-searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="mdbook-searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
<div class="spinner-wrapper">
|
||||||
|
<span class=fa-svg id="fa-spin"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M304 48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zm0 416c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM48 304c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48zm464-48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM142.9 437c18.7-18.7 18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zm0-294.2c18.7-18.7 18.7-49.1 0-67.9S93.7 56.2 75 75s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zM369.1 437c18.7 18.7 49.1 18.7 67.9 0s18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9z"/></svg></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="mdbook-searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="mdbook-searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="mdbook-searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script>
|
||||||
|
document.getElementById('mdbook-sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('mdbook-sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#mdbook-sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="mdbook-content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="roadmap--quicproquo"><a class="header" href="#roadmap--quicproquo">Roadmap — quicproquo</a></h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>From proof-of-concept to production-grade E2E encrypted messaging.</p>
|
||||||
|
<p>Each phase is designed to be tackled sequentially. Items within a phase
|
||||||
|
can be parallelised. Check the box when done.</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-1--production-hardening-critical"><a class="header" href="#phase-1--production-hardening-critical">Phase 1 — Production Hardening (Critical)</a></h2>
|
||||||
|
<p>Eliminate all crash paths, enforce secure defaults, fix deployment blockers.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>1.1 Remove <code>.unwrap()</code> / <code>.expect()</code> from production paths</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Replace <code>AUTH_CONTEXT.read().expect()</code> in client RPC with proper <code>Result</code></li>
|
||||||
|
<li>Replace <code>"0.0.0.0:0".parse().unwrap()</code> in client with fallible parse</li>
|
||||||
|
<li>Replace <code>Mutex::lock().unwrap()</code> in server storage with <code>.map_err()</code></li>
|
||||||
|
<li>Audit: <code>grep -rn 'unwrap()\|expect(' crates/</code> outside <code>#[cfg(test)]</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>1.2 Enforce secure defaults in production mode</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Reject startup if <code>QPQ_PRODUCTION=true</code> and <code>auth_token</code> is empty or <code>"devtoken"</code></li>
|
||||||
|
<li>Require non-empty <code>db_key</code> when using SQL backend in production</li>
|
||||||
|
<li>Refuse to auto-generate TLS certs in production mode (require existing cert+key)</li>
|
||||||
|
<li>Already partially implemented — verify and harden the validation in <code>config.rs</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>1.3 Fix <code>.gitignore</code></strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Add <code>data/</code>, <code>*.der</code>, <code>*.pem</code>, <code>*.db</code>, <code>*.bin</code> (state files), <code>*.ks</code> (keystores)</li>
|
||||||
|
<li>Verify no secrets are already tracked: <code>git ls-files data/ *.der *.db</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>1.4 Fix Dockerfile</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Sync workspace members (handle excluded <code>p2p</code> crate)</li>
|
||||||
|
<li>Create dedicated user/group instead of <code>nobody</code></li>
|
||||||
|
<li>Set writable <code>QPQ_DATA_DIR</code> with correct permissions</li>
|
||||||
|
<li>Test: <code>docker build . && docker run --rm -it qpq-server --help</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>1.5 TLS certificate lifecycle</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Document CA-signed cert setup (Let’s Encrypt / custom CA)</li>
|
||||||
|
<li>Add <code>--tls-required</code> flag that refuses to start without valid cert</li>
|
||||||
|
<li>Log clear warning when using self-signed certs</li>
|
||||||
|
<li>Document certificate rotation procedure</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-2--test--ci-maturity"><a class="header" href="#phase-2--test--ci-maturity">Phase 2 — Test & CI Maturity</a></h2>
|
||||||
|
<p>Build confidence before adding features.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>2.1 Expand E2E test coverage</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Auth failure scenarios (wrong password, expired token, invalid token)</li>
|
||||||
|
<li>Message ordering verification (send N messages, verify seq numbers)</li>
|
||||||
|
<li>Concurrent clients (3+ members in group, simultaneous send/recv)</li>
|
||||||
|
<li>OPAQUE registration + login full flow</li>
|
||||||
|
<li>Queue full behavior (>1000 messages)</li>
|
||||||
|
<li>Rate limiting behavior (>100 enqueues/minute)</li>
|
||||||
|
<li>Reconnection after server restart</li>
|
||||||
|
<li>KeyPackage exhaustion (fetch when none available)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>2.2 Add unit tests for untested paths</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Client retry logic (exponential backoff, jitter, retriable classification)</li>
|
||||||
|
<li>REPL input parsing edge cases (empty input, special characters, <code>/</code> commands)</li>
|
||||||
|
<li>State file encryption/decryption round-trip with bad password</li>
|
||||||
|
<li>Token cache expiry</li>
|
||||||
|
<li>Conversation store migrations</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>2.3 CI hardening</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Add <code>.github/CODEOWNERS</code> (crypto, auth, wire-format require 2 reviewers)</li>
|
||||||
|
<li>Ensure <code>cargo deny check</code> runs on every PR (already in CI — verify)</li>
|
||||||
|
<li>Add <code>cargo audit</code> as blocking check (already in CI — verify)</li>
|
||||||
|
<li>Add coverage reporting (tarpaulin or llvm-cov)</li>
|
||||||
|
<li>Add CI job for Docker build validation</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>2.4 Clean up build warnings</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Fix Cap’n Proto generated <code>unused_parens</code> warnings</li>
|
||||||
|
<li>Remove dead code / unused imports</li>
|
||||||
|
<li>Address <code>openmls</code> future-incompat warnings</li>
|
||||||
|
<li>Target: <code>cargo clippy --workspace -- -D warnings</code> passes clean</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-3--client-sdks-native-quic--capn-proto-everywhere"><a class="header" href="#phase-3--client-sdks-native-quic--capn-proto-everywhere">Phase 3 — Client SDKs: Native QUIC + Cap’n Proto Everywhere</a></h2>
|
||||||
|
<p><strong>No REST gateway. No protocol dilution.</strong> The <code>.capnp</code> schemas are the
|
||||||
|
interface definition. Every SDK speaks native QUIC + Cap’n Proto. The
|
||||||
|
project name stays honest.</p>
|
||||||
|
<h3 id="why-this-matters"><a class="header" href="#why-this-matters">Why this matters</a></h3>
|
||||||
|
<p>The name is <strong>quic</strong>n<strong>proto</strong>chat — the protocol IS the product. Instead
|
||||||
|
of adding an HTTP translation layer that loses zero-copy performance and
|
||||||
|
adds base64 overhead, we invest in making the native protocol accessible
|
||||||
|
from every language that has QUIC + Cap’n Proto support, and provide
|
||||||
|
WASM/FFI for the crypto layer.</p>
|
||||||
|
<h3 id="architecture"><a class="header" href="#architecture">Architecture</a></h3>
|
||||||
|
<pre><code> Server: QUIC + Cap'n Proto (single protocol, no gateway)
|
||||||
|
|
||||||
|
Client SDKs:
|
||||||
|
┌─── Rust quinn + capnp-rpc (existing, reference impl)
|
||||||
|
├─── Go quic-go + go-capnp (native, high confidence)
|
||||||
|
├─── Python aioquic + pycapnp (native QUIC, manual framing)
|
||||||
|
├─── C/C++ msquic/ngtcp2 + capnproto (reference impl, full RPC)
|
||||||
|
└─── Browser WebTransport + capnp (WASM) (QUIC transport, no HTTP needed)
|
||||||
|
|
||||||
|
Crypto layer (client-side MLS, shared across all SDKs):
|
||||||
|
┌─── Rust crate (native, existing)
|
||||||
|
├─── WASM module (browsers, Node.js, Deno)
|
||||||
|
└─── C FFI (Swift, Kotlin, Python, Go via cgo)
|
||||||
|
</code></pre>
|
||||||
|
<h3 id="language-support-reality-check"><a class="header" href="#language-support-reality-check">Language support reality check</a></h3>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Language</th><th>QUIC</th><th>Cap’n Proto</th><th>RPC</th><th>Confidence</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><strong>Rust</strong></td><td>quinn ✅</td><td>capnp-rpc ✅</td><td>Full ✅</td><td>Existing</td></tr>
|
||||||
|
<tr><td><strong>Go</strong></td><td>quic-go ✅</td><td>go-capnp ✅</td><td>Level 1 ✅</td><td>High</td></tr>
|
||||||
|
<tr><td><strong>Python</strong></td><td>aioquic ✅</td><td>pycapnp ⚠️</td><td>Manual framing</td><td>Medium</td></tr>
|
||||||
|
<tr><td><strong>C/C++</strong></td><td>msquic/ngtcp2 ✅</td><td>capnproto ✅</td><td>Full ✅</td><td>High</td></tr>
|
||||||
|
<tr><td><strong>Browser</strong></td><td>WebTransport ✅</td><td>WASM ✅</td><td>Via WASM bridge</td><td>Medium</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h3 id="implementation"><a class="header" href="#implementation">Implementation</a></h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>3.1 Go SDK (<code>quicproquo-go</code>)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Generated Go types from <code>node.capnp</code> (6487-line codegen, all 24 RPC methods)</li>
|
||||||
|
<li>QUIC transport via <code>quic-go</code> with TLS 1.3 + ALPN <code>"capnp"</code></li>
|
||||||
|
<li>High-level <code>qpq</code> package: Connect, Health, ResolveUser, CreateChannel, Send/SendWithTTL, Receive/ReceiveWait, DeleteAccount, OPAQUE auth</li>
|
||||||
|
<li>Example CLI in <code>sdks/go/cmd/example/</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>3.2 Python SDK (<code>quicproquo-py</code>)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>QUIC transport: <code>aioquic</code> with custom Cap’n Proto stream handler</li>
|
||||||
|
<li>Cap’n Proto serialization: <code>pycapnp</code> for message types</li>
|
||||||
|
<li>Manual RPC framing: length-prefixed request/response over QUIC stream</li>
|
||||||
|
<li>Async/await API matching the Rust client patterns</li>
|
||||||
|
<li>Crypto: PyO3 bindings to <code>quicproquo-core</code> for MLS operations</li>
|
||||||
|
<li>Publish: PyPI <code>quicproquo</code></li>
|
||||||
|
<li>Example: async bot client</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>3.3 C FFI layer (<code>quicproquo-ffi</code>)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>crates/quicproquo-ffi</code> with 7 extern “C” functions: connect, login, send, receive, disconnect, last_error, free_string</li>
|
||||||
|
<li>Builds as <code>libquicproquo_ffi.so</code> / <code>.dylib</code> / <code>.dll</code></li>
|
||||||
|
<li>Python ctypes wrapper in <code>examples/python/qpq_client.py</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>3.4 WASM compilation of <code>quicproquo-core</code></strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>wasm-pack build</code> target producing 175 KB WASM bundle (LTO + opt-level=s)</li>
|
||||||
|
<li>13 <code>wasm_bindgen</code> functions: Ed25519 identity, hybrid KEM, safety numbers, sealed sender, padding</li>
|
||||||
|
<li>Browser-ready with <code>crypto.getRandomValues()</code> RNG</li>
|
||||||
|
<li>Published as <code>sdks/typescript/wasm-crypto/</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>3.5 WebTransport server endpoint</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Add HTTP/3 + WebTransport listener to server (same QUIC stack via quinn)</li>
|
||||||
|
<li>Cap’n Proto RPC framed over WebTransport bidirectional streams</li>
|
||||||
|
<li>Same auth, same storage, same RPC handlers — just a different stream source</li>
|
||||||
|
<li>Browsers connect via <code>new WebTransport("https://server:7443")</code></li>
|
||||||
|
<li>ALPN negotiation: <code>"h3"</code> for WebTransport, <code>"capnp"</code> for native QUIC</li>
|
||||||
|
<li>Configurable port: <code>--webtransport-listen 0.0.0.0:7443</code></li>
|
||||||
|
<li>Feature-flagged: <code>--features webtransport</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>3.6 TypeScript/JavaScript SDK (<code>@quicproquo/client</code>)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>QpqClient</code> class: connect, offline, health, resolveUser, createChannel, send/sendWithTTL, receive, deleteAccount</li>
|
||||||
|
<li>WASM crypto wrapper: generateIdentity, sign/verify, hybridEncrypt/Decrypt, computeSafetyNumber, sealedSend, pad</li>
|
||||||
|
<li>WebSocket transport with request/response correlation and reconnection</li>
|
||||||
|
<li>Browser demo: interactive crypto playground + chat UI (<code>sdks/typescript/demo/index.html</code>)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>3.7 SDK documentation and schema publishing</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Publish <code>.capnp</code> schemas as the canonical API contract</li>
|
||||||
|
<li>Document the QUIC + Cap’n Proto connection pattern for each language</li>
|
||||||
|
<li>Provide a “build your own SDK” guide (QUIC stream → Cap’n Proto RPC bootstrap)</li>
|
||||||
|
<li>Reference implementation checklist: connect, auth, upload key, enqueue, fetch</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-4--trust--security-infrastructure"><a class="header" href="#phase-4--trust--security-infrastructure">Phase 4 — Trust & Security Infrastructure</a></h2>
|
||||||
|
<p>Address the security gaps required for real-world deployment.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>4.1 Third-party cryptographic audit</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Scope: MLS integration, OPAQUE flow, hybrid KEM, key lifecycle, zeroization</li>
|
||||||
|
<li>Firms: NCC Group, Trail of Bits, Cure53</li>
|
||||||
|
<li>Budget and timeline: typically 4-6 weeks, $50K–$150K</li>
|
||||||
|
<li>Publish report publicly (builds trust)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>4.2 Key Transparency / revocation</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Replace <code>BasicCredential</code> with X.509-based MLS credentials</li>
|
||||||
|
<li>Or: verifiable key directory (Merkle tree, auditable log)</li>
|
||||||
|
<li>Users can verify peer keys haven’t been substituted (MITM detection)</li>
|
||||||
|
<li>Revocation mechanism for compromised keys</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>4.3 Client authentication on Delivery Service</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>DS sender identity binding with explicit audit logging</li>
|
||||||
|
<li><code>sender_prefix</code> tracking in enqueue/batch_enqueue RPCs</li>
|
||||||
|
<li>Sender identity derived from authenticated session</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>4.4 M7 — Post-quantum MLS integration</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Integrate hybrid KEM (X25519 + ML-KEM-768) into the OpenMLS crypto provider</li>
|
||||||
|
<li>Group key material gets post-quantum confidentiality</li>
|
||||||
|
<li>Full test suite with PQ ciphersuite</li>
|
||||||
|
<li>Ref: existing <code>hybrid_kem.rs</code> and <code>hybrid_crypto.rs</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>4.5 Username enumeration mitigation</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>5 ms timing floor on <code>resolveUser</code> responses</li>
|
||||||
|
<li>Rate limiting to prevent bulk enumeration attacks</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-5--features--ux"><a class="header" href="#phase-5--features--ux">Phase 5 — Features & UX</a></h2>
|
||||||
|
<p>Make it a product people want to use.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>5.1 Multi-device support</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Account → multiple devices, each with own Ed25519 key + MLS KeyPackages</li>
|
||||||
|
<li>Device graph management (add device, remove device, list devices)</li>
|
||||||
|
<li>Messages delivered to all devices of a user</li>
|
||||||
|
<li><code>device_id</code> field already in Auth struct — wire it through</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>5.2 Account recovery</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Recovery codes or backup key (encrypted, stored by user)</li>
|
||||||
|
<li>Option: server-assisted recovery with security questions (lower security)</li>
|
||||||
|
<li>MLS state re-establishment after device loss</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>5.3 Full MLS lifecycle</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Member removal (Remove proposal → Commit → fan-out)</li>
|
||||||
|
<li>Credential update (Update proposal for key rotation)</li>
|
||||||
|
<li>Explicit proposal handling (queue proposals, batch commit)</li>
|
||||||
|
<li>Group metadata (name, description, avatar hash)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>5.4 Message editing and deletion</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Edit</code> (0x06) and <code>Delete</code> (0x07) message types in <code>AppMessage</code></li>
|
||||||
|
<li><code>/edit <index> <text></code> and <code>/delete <index></code> REPL commands (own messages only)</li>
|
||||||
|
<li>Database update/removal on incoming edit/delete</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>5.5 File and media transfer</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>uploadBlob</code> / <code>downloadBlob</code> RPCs with 256 KB chunked streaming</li>
|
||||||
|
<li>SHA-256 content-addressable storage with hash verification</li>
|
||||||
|
<li><code>FileRef</code> (0x08) message type with blob_id, filename, file_size, mime_type</li>
|
||||||
|
<li><code>/send-file <path></code> and <code>/download <index></code> REPL commands with progress bars</li>
|
||||||
|
<li>50 MB max file size, automatic MIME detection via <code>mime_guess</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>5.6 Abuse prevention and moderation</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Block user (client-side, suppress display)</li>
|
||||||
|
<li>Report message (encrypted report to admin key)</li>
|
||||||
|
<li>Admin tools: ban user, delete account, audit log</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>5.7 Offline message queue (client-side)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Queue messages when disconnected, send on reconnect</li>
|
||||||
|
<li>Idempotent message IDs to prevent duplicates</li>
|
||||||
|
<li>Gap detection: compare local seq with server seq</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-6--scale--operations"><a class="header" href="#phase-6--scale--operations">Phase 6 — Scale & Operations</a></h2>
|
||||||
|
<p>Prepare for real traffic.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>6.1 Distributed rate limiting</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Current: in-memory per-process, lost on restart</li>
|
||||||
|
<li>Move to Redis or shared state for multi-node deployments</li>
|
||||||
|
<li>Sliding window with configurable thresholds</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>6.2 Multi-node / horizontal scaling</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Stateless server design (already mostly there — state is in storage backend)</li>
|
||||||
|
<li>Shared PostgreSQL or CockroachDB backend (replace SQLite)</li>
|
||||||
|
<li>Message queue fan-out (Redis pub/sub or NATS for cross-node notification)</li>
|
||||||
|
<li>Load balancer health check via QUIC RPC <code>health()</code> or Prometheus <code>/metrics</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>6.3 Operational runbook</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Backup / restore procedures (SQLCipher, file backend)</li>
|
||||||
|
<li>Key rotation (auth token, TLS cert, DB encryption key)</li>
|
||||||
|
<li>Incident response playbook</li>
|
||||||
|
<li>Scaling guide (when to add nodes, resource sizing)</li>
|
||||||
|
<li>Monitoring dashboard templates (Grafana + Prometheus)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>6.4 Connection draining and graceful shutdown</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Stop accepting new connections on SIGTERM</li>
|
||||||
|
<li>Wait for in-flight RPCs (configurable timeout, default 30s)</li>
|
||||||
|
<li>Drain WebTransport sessions with close frame</li>
|
||||||
|
<li>Document expected behavior for load balancers (health → unhealthy first)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>6.5 Request-level timeouts</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Per-RPC timeout (prevent slow clients from holding resources)</li>
|
||||||
|
<li>Database query timeout</li>
|
||||||
|
<li>Overall request deadline propagation</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>6.6 Observability enhancements</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Request correlation IDs (trace across RPC → storage)</li>
|
||||||
|
<li>Storage operation latency metrics</li>
|
||||||
|
<li>Per-endpoint latency histograms</li>
|
||||||
|
<li>Structured audit log to persistent storage (not just stdout)</li>
|
||||||
|
<li>OpenTelemetry integration</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-7--platform-expansion--research"><a class="header" href="#phase-7--platform-expansion--research">Phase 7 — Platform Expansion & Research</a></h2>
|
||||||
|
<p>Long-term vision for wide adoption.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>7.1 Mobile clients (iOS + Android)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Use C FFI (Phase 3.3) for crypto + transport (single library)</li>
|
||||||
|
<li>Push notifications via APNs / FCM (server sends notification on enqueue)</li>
|
||||||
|
<li>Background QUIC connection for message polling</li>
|
||||||
|
<li>Biometric auth for local key storage (Keychain / Android Keystore)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>7.2 Web client (browser)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Use WASM (Phase 3.4) for crypto</li>
|
||||||
|
<li>Use WebTransport (Phase 3.5) for native QUIC transport</li>
|
||||||
|
<li>Cap’n Proto via WASM bridge (Phase 3.6)</li>
|
||||||
|
<li>IndexedDB for local state persistence</li>
|
||||||
|
<li>Service Worker for background notifications</li>
|
||||||
|
<li>Progressive Web App (PWA) support</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>7.3 Federation</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Server-to-server protocol via Cap’n Proto RPC over QUIC (see <code>federation.capnp</code>)</li>
|
||||||
|
<li><code>relayEnqueue</code>, <code>proxyFetchKeyPackage</code>, <code>federationHealth</code> methods</li>
|
||||||
|
<li>Identity resolution across federated servers</li>
|
||||||
|
<li>MLS group spanning multiple servers</li>
|
||||||
|
<li>Trust model for federated deployments</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>7.4 Sealed Sender</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Sender identity inside MLS ciphertext only (server can’t see who sent)</li>
|
||||||
|
<li><code>sealed_sender</code> module in quicproquo-core with seal/unseal API</li>
|
||||||
|
<li>WASM-accessible via <code>wasm_bindgen</code> for browser use</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>7.5 Additional language SDKs</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Java/Kotlin: JNI bindings to C FFI (Phase 3.3) + native QUIC (netty-quic)</li>
|
||||||
|
<li>Swift: Swift wrapper over C FFI + Network.framework QUIC</li>
|
||||||
|
<li>Ruby: FFI bindings via <code>quicproquo-ffi</code></li>
|
||||||
|
<li>Evaluate demand-driven — only build SDKs people request</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>7.6 P2P / NAT traversal</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Direct peer-to-peer via iroh (foundation exists in <code>quicproquo-p2p</code>)</li>
|
||||||
|
<li>Server as fallback relay only</li>
|
||||||
|
<li>Reduces latency and single-point-of-failure</li>
|
||||||
|
<li>Ref: <code>FUTURE-IMPROVEMENTS.md § 6.1</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>7.7 Traffic analysis resistance</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Padding messages to uniform size</li>
|
||||||
|
<li>Decoy traffic to mask timing patterns</li>
|
||||||
|
<li>Optional Tor/I2P routing for IP privacy</li>
|
||||||
|
<li>Ref: <code>FUTURE-IMPROVEMENTS.md § 5.4, 6.3</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-8--freifunk--community-mesh-networking"><a class="header" href="#phase-8--freifunk--community-mesh-networking">Phase 8 — Freifunk / Community Mesh Networking</a></h2>
|
||||||
|
<p>Make qpq a first-class citizen on decentralised, community-operated wireless
|
||||||
|
networks (Freifunk, BATMAN-adv/Babel routing, OpenWrt). Multiple qpq nodes form
|
||||||
|
a federated mesh; clients auto-discover nearby nodes via mDNS; the network
|
||||||
|
functions without any central infrastructure or internet uplink.</p>
|
||||||
|
<h3 id="architecture-1"><a class="header" href="#architecture-1">Architecture</a></h3>
|
||||||
|
<pre><code> Client A ─── mDNS discovery ──► nearby qpq node (LAN / mesh)
|
||||||
|
│
|
||||||
|
Cap'n Proto federation
|
||||||
|
│
|
||||||
|
remote qpq node (across mesh)
|
||||||
|
</code></pre>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F0 — Re-include <code>quicproquo-p2p</code> in workspace; fix ALPN strings</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Moved <code>crates/quicproquo-p2p</code> from <code>exclude</code> back into <code>[workspace] members</code></li>
|
||||||
|
<li>Fixed ALPN <code>b"quicnprotochat/p2p/1"</code> → <code>b"quicproquo/p2p/1"</code> (breaking wire change)</li>
|
||||||
|
<li>Fixed federation ALPN <code>b"qnpc-fed"</code> → <code>b"quicproquo/federation/1"</code></li>
|
||||||
|
<li>Feature-gated behind <code>--features mesh</code> on client (keeps iroh out of default builds)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F1 — Federation routing in message delivery</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>handle_enqueue</code> and <code>handle_batch_enqueue</code> call <code>federation::routing::resolve_destination()</code></li>
|
||||||
|
<li>Recipients with a remote home server are relayed via <code>FederationClient::relay_enqueue()</code></li>
|
||||||
|
<li>mTLS mutual authentication between nodes (both present client certs, validated against shared CA)</li>
|
||||||
|
<li>Config: <code>QPQ_FEDERATION_LISTEN</code>, <code>QPQ_LOCAL_DOMAIN</code>, <code>QPQ_FEDERATION_CERT/KEY/CA</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F2 — mDNS local peer discovery</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Server announces <code>_quicproquo._udp.local.</code> on startup via <code>mdns-sd</code></li>
|
||||||
|
<li>Client: <code>MeshDiscovery::start()</code> browses for nearby nodes (feature-gated)</li>
|
||||||
|
<li>REPL commands: <code>/mesh peers</code> (scan + list), <code>/mesh server <host:port></code> (note address)</li>
|
||||||
|
<li>Nodes announce: <code>ver=1</code>, <code>server=<host:port></code>, <code>domain=<local_domain></code> TXT records</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F3 — Self-sovereign mesh identity</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Ed25519 keypair-based identity independent of AS registration</li>
|
||||||
|
<li>JSON-persisted seed + known peers directory</li>
|
||||||
|
<li>Sign/verify operations for mesh authenticity (<code>crates/quicproquo-p2p/src/identity.rs</code>)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F4 — Store-and-forward with TTL</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>MeshEnvelope</code> with TTL-based expiry, hop_count tracking, max_hops routing limit</li>
|
||||||
|
<li>SHA-256 deduplication ID prevents relay loops</li>
|
||||||
|
<li>Ed25519 signature verification on envelopes</li>
|
||||||
|
<li><code>MeshStore</code> in-memory queue with per-recipient capacity limits and TTL-based GC</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F5 — Lightweight broadcast channels</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Symmetric ChaCha20-Poly1305 encrypted channels (no MLS overhead)</li>
|
||||||
|
<li>Topic-based pub/sub via <code>BroadcastChannel</code> and <code>BroadcastManager</code></li>
|
||||||
|
<li>Subscribe/unsubscribe, create, publish API on <code>P2pNode</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>F6 — Extended <code>/mesh</code> REPL commands</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>/mesh send <peer_id> <msg></code> — direct P2P message via iroh</li>
|
||||||
|
<li><code>/mesh broadcast <topic> <msg></code> — publish to broadcast channel</li>
|
||||||
|
<li><code>/mesh subscribe <topic></code> — join broadcast channel</li>
|
||||||
|
<li><code>/mesh route</code> — show routing table</li>
|
||||||
|
<li><code>/mesh identity</code> — show mesh identity info</li>
|
||||||
|
<li><code>/mesh store</code> — show store-and-forward statistics</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>F7 — OpenWrt cross-compilation guide</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Musl static builds: <code>x86_64-unknown-linux-musl</code>, <code>armv7-unknown-linux-musleabihf</code>, <code>mips-unknown-linux-musl</code></li>
|
||||||
|
<li>Strip binary: <code>--release</code> + <code>strip</code> → target size < 5 MB for flash storage</li>
|
||||||
|
<li><code>opkg</code> package manifest for OpenWrt feed</li>
|
||||||
|
<li><code>procd</code> init script + <code>uci</code> config file for OpenWrt integration</li>
|
||||||
|
<li>CI job: cross-compile and size-check on every release tag</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>F8 — Traffic analysis resistance for mesh</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Uniform message padding to nearest 256-byte boundary (hides message size)</li>
|
||||||
|
<li>Configurable decoy traffic rate (fake messages to mask send timing)</li>
|
||||||
|
<li>Optional onion routing: 3-hop relay through other mesh nodes (no Tor dependency)</li>
|
||||||
|
<li>Ref: Phase 7.7 for server-side traffic analysis resistance</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="phase-9--developer-experience--community-growth"><a class="header" href="#phase-9--developer-experience--community-growth">Phase 9 — Developer Experience & Community Growth</a></h2>
|
||||||
|
<p>Features designed to attract contributors, create demo/showcase potential,
|
||||||
|
and lower the barrier to entry for non-crypto developers.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>9.1 Criterion Benchmark Suite (<code>qpq-bench</code>)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Criterion benchmarks for all crypto primitives: hybrid KEM encap/decap,
|
||||||
|
MLS group-add at 10/100/1000 members, epoch rotation, Noise_XX handshake</li>
|
||||||
|
<li>CI publishes HTML benchmark reports as GitHub Actions artifacts</li>
|
||||||
|
<li>Citable numbers — no other project benchmarks MLS + PQ-KEM in Rust</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>9.2 Safety Numbers (key verification)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>60-digit numeric code derived from two identity keys (Signal-style)</li>
|
||||||
|
<li><code>/verify <username></code> REPL command for out-of-band verification</li>
|
||||||
|
<li>Available in WASM via <code>compute_safety_number</code> binding</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>9.3 Full-Screen TUI (Ratatui + Crossterm)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>qpq tui</code> launches a full-screen terminal UI: message pane, input bar,
|
||||||
|
channel sidebar with unread counts, MLS epoch indicator</li>
|
||||||
|
<li>Feature-gated <code>--features tui</code> to keep ratatui/crossterm out of default builds</li>
|
||||||
|
<li>Existing REPL and CLI subcommands are unaffected</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>9.4 Delivery Proof Canary Tokens</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Server signs <code>Ed25519(SHA-256(message_id || recipient || timestamp))</code> on enqueue</li>
|
||||||
|
<li>Sender stores proof locally — cryptographic evidence the server queued the message</li>
|
||||||
|
<li>Cap’n Proto schema gains optional <code>deliveryProof: Data</code> on enqueue response</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>9.5 Verifiable Transcript Archive</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>GroupMember::export_transcript(path, password)</code> writes encrypted, tamper-evident
|
||||||
|
message archive (CBOR records, Argon2id + ChaCha20-Poly1305, Merkle chain)</li>
|
||||||
|
<li><code>qpq export verify</code> CLI command independently verifies chain integrity</li>
|
||||||
|
<li>Useful for legal discovery, audit, or personal backup</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>9.6 Key Transparency (Merkle-Log Identity Binding)</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Append-only Merkle log of (username, identity_key) bindings in the AS</li>
|
||||||
|
<li>Clients receive inclusion proofs alongside key fetches</li>
|
||||||
|
<li>Any client can independently audit the full identity history</li>
|
||||||
|
<li>Lightweight subset of RFC 9162 adapted for identity keys</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox" checked=""> <strong>9.7 Dynamic Server Plugin System</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Server loads <code>.so</code>/<code>.dylib</code> plugins at runtime via <code>--plugin-dir</code></li>
|
||||||
|
<li>C-compatible <code>HookVTable</code> via <code>extern "C"</code> — plugins in any language</li>
|
||||||
|
<li>6 hook points: on_message_enqueue, on_batch_enqueue, on_auth, on_channel_created, on_fetch, on_user_registered</li>
|
||||||
|
<li>Example plugins: logging plugin, rate limit plugin (512 KiB payload enforcement)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><input disabled="" type="checkbox"> <strong>9.8 PQ Noise Transport Layer</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Hybrid <code>Noise_XX + ML-KEM-768</code> handshake for post-quantum transport security</li>
|
||||||
|
<li>Closes the harvest-now-decrypt-later gap on handshake metadata (ADR-006)</li>
|
||||||
|
<li>Feature-gated <code>--features pq-noise</code>; classical Noise_XX default preserved</li>
|
||||||
|
<li>May require extending or forking <code>snow</code> crate’s <code>CryptoResolver</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h2 id="summary-timeline"><a class="header" href="#summary-timeline">Summary Timeline</a></h2>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Phase</th><th>Focus</th><th>Estimated Effort</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><strong>1</strong></td><td>Production Hardening</td><td>1–2 days</td></tr>
|
||||||
|
<tr><td><strong>2</strong></td><td>Test & CI Maturity</td><td>2–3 days</td></tr>
|
||||||
|
<tr><td><strong>3</strong></td><td>Client SDKs (Go, Python, WASM, FFI, WebTransport)</td><td>5–8 days</td></tr>
|
||||||
|
<tr><td><strong>4</strong></td><td>Trust & Security Infrastructure</td><td>2–4 days (excl. audit)</td></tr>
|
||||||
|
<tr><td><strong>5</strong></td><td>Features & UX</td><td>5–7 days</td></tr>
|
||||||
|
<tr><td><strong>6</strong></td><td>Scale & Operations</td><td>3–5 days</td></tr>
|
||||||
|
<tr><td><strong>7</strong></td><td>Platform Expansion & Research</td><td>ongoing</td></tr>
|
||||||
|
<tr><td><strong>8</strong></td><td>Freifunk / Community Mesh</td><td>ongoing</td></tr>
|
||||||
|
<tr><td><strong>9</strong></td><td>Developer Experience & Community Growth</td><td>3–5 days</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h2 id="related-documents"><a class="header" href="#related-documents">Related Documents</a></h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="docs/FUTURE-IMPROVEMENTS.html">Future Improvements</a> — consolidated improvement list</li>
|
||||||
|
<li><a href="docs/PRODUCTION-READINESS-AUDIT.html">Production Readiness Audit</a> — specific blockers</li>
|
||||||
|
<li><a href="docs/SECURITY-AUDIT.html">Security Audit</a> — findings and recommendations</li>
|
||||||
|
<li><a href="docs/src/roadmap/milestones.html">Milestone Tracker</a> — M1–M7 status</li>
|
||||||
|
<li><a href="docs/src/roadmap/authz-plan.html">Auth, Devices, and Tokens</a> — authorization design</li>
|
||||||
|
<li><a href="docs/src/roadmap/dm-channels.html">DM Channel Design</a> — 1:1 channel spec</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="roadmap/future-research.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next prefetch" href="contributing/coding-standards.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="roadmap/future-research.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next prefetch" href="contributing/coding-standards.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg></span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template id=fa-eye><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"/></svg></span></template>
|
||||||
|
<template id=fa-eye-slash><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"/></svg></span></template>
|
||||||
|
<template id=fa-copy><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 70.63l-61.25-61.25C435.4 3.371 427.2 0 418.7 0H255.1c-35.35 0-64 28.66-64 64l.0195 256C192 355.4 220.7 384 256 384h192c35.2 0 64-28.8 64-64V93.25C512 84.77 508.6 76.63 502.6 70.63zM464 320c0 8.836-7.164 16-16 16H255.1c-8.838 0-16-7.164-16-16L239.1 64.13c0-8.836 7.164-16 16-16h128L384 96c0 17.67 14.33 32 32 32h47.1V320zM272 448c0 8.836-7.164 16-16 16H63.1c-8.838 0-16-7.164-16-16L47.98 192.1c0-8.836 7.164-16 16-16H160V128H63.99c-35.35 0-64 28.65-64 64l.0098 256C.002 483.3 28.66 512 64 512h192c35.2 0 64-28.8 64-64v-32h-47.1L272 448z"/></svg></span></template>
|
||||||
|
<template id=fa-play><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg></span></template>
|
||||||
|
<template id=fa-clock-rotate-left><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24H134.1c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24V256c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65V152c0-13.3-10.7-24-24-24z"/></svg></span></template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr-ef4e11c1.min.js"></script>
|
||||||
|
<script src="mark-09e88c2c.min.js"></script>
|
||||||
|
<script src="searcher-c2a407aa.js"></script>
|
||||||
|
|
||||||
|
<script src="clipboard-1626706a.min.js"></script>
|
||||||
|
<script src="highlight-abc7f01d.js"></script>
|
||||||
|
<script src="book-a0b12cfe.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
assets/left.ansi
Normal file
26
assets/left.ansi
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
registering 'alice'...
|
||||||
|
user 'alice' registered
|
||||||
|
logging in as 'alice'...
|
||||||
|
logged in, session cached
|
||||||
|
[2midentity: c1e1f6df17eeb6..2816[0m
|
||||||
|
KeyPackage uploaded
|
||||||
|
hybrid key uploaded
|
||||||
|
type /help for commands, Ctrl+D to exit
|
||||||
|
|
||||||
|
[2m[[0m[1mno conversation[0m[2m][0m > /dm bob
|
||||||
|
resolving bob...
|
||||||
|
creating channel...
|
||||||
|
fetching peer's key package...
|
||||||
|
DM with @bob created. Start typing!
|
||||||
|
[2m[[0m[1m@bob[0m[2m][0m > [32mHey Bob, testing our E2E encrypted channel![0m
|
||||||
|
[36m[1m[bob][0m Works great -- the server never sees plaintext?
|
||||||
|
[2m[[0m[1m@bob[0m[2m][0m > [32mRight. MLS forward secrecy + post-quantum KEM.[0m
|
||||||
|
[36m[1m[bob][0m Impressive. How do I verify your identity?
|
||||||
|
[2m[[0m[1m@bob[0m[2m][0m > [32mRun /verify alice -- compare the safety number out-of-band.[0m
|
||||||
|
[2m[[0m[1m@bob[0m[2m][0m > /group-info
|
||||||
|
[2m Conversation: @bob[0m
|
||||||
|
[2m Type: DM[0m
|
||||||
|
[2m Members: 2[0m
|
||||||
|
[2m alice (you), bob[0m
|
||||||
|
[2m MLS epoch: 3[0m
|
||||||
|
[2m[[0m[1m@bob[0m[2m][0m >
|
||||||
24
assets/right.ansi
Normal file
24
assets/right.ansi
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
registering 'bob'...
|
||||||
|
user 'bob' registered
|
||||||
|
logging in as 'bob'...
|
||||||
|
logged in, session cached
|
||||||
|
[2midentity: a8c2f19f1b0806..c73f[0m
|
||||||
|
KeyPackage uploaded
|
||||||
|
hybrid key uploaded
|
||||||
|
type /help for commands, Ctrl+D to exit
|
||||||
|
|
||||||
|
[36m[1m[system][0m new conversation: @alice
|
||||||
|
[2m[[0m[1m@alice[0m[2m][0m > [36m[1m[alice][0m Hey Bob, testing our E2E encrypted channel!
|
||||||
|
[2m[[0m[1m@alice[0m[2m][0m > [32mWorks great -- the server never sees plaintext?[0m
|
||||||
|
[36m[1m[alice][0m Right. MLS forward secrecy + post-quantum KEM.
|
||||||
|
[2m[[0m[1m@alice[0m[2m][0m > [32mImpressive. How do I verify your identity?[0m
|
||||||
|
[36m[1m[alice][0m Run /verify alice -- compare the safety number out-of-band.
|
||||||
|
[2m[[0m[1m@alice[0m[2m][0m > /verify alice
|
||||||
|
[2m Safety number for @alice:[0m
|
||||||
|
[2m 096482 731945 208376[0m
|
||||||
|
[2m 571039 284617 950283[0m
|
||||||
|
[2m[[0m[1m@alice[0m[2m][0m > /whoami
|
||||||
|
[2m identity: a8c2f19f1b0806..c73f[0m
|
||||||
|
[2m hybrid key: yes[0m
|
||||||
|
[2m conversations: 1[0m
|
||||||
|
[2m[[0m[1m@alice[0m[2m][0m >
|
||||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
59
assets/screenshot.txt
Normal file
59
assets/screenshot.txt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
=== Alice (left) ===
|
||||||
|
❯ ./target/debug/qpq repl --username alice --password de
|
||||||
|
opass1 --server 127.0.0.1:17123 --ca-cert /tmp/tmp.adbXG
|
||||||
|
OrPY/server-cert.der --state /tmp/tmp.adbXGLOrPY/alice.b
|
||||||
|
n
|
||||||
|
registering 'alice'...
|
||||||
|
user 'alice' registered
|
||||||
|
logging in as 'alice'...
|
||||||
|
logged in, session cached
|
||||||
|
identity: c1e1f6df17eeb6f539d7fbea94129fa32fc02ca40e5c
|
||||||
|
7a7c95cfc94161d5f628
|
||||||
|
KeyPackage uploaded
|
||||||
|
hybrid key uploaded
|
||||||
|
type /help for commands, Ctrl+D to exit
|
||||||
|
|
||||||
|
[no conversation] > /dm bob
|
||||||
|
resolving bob...
|
||||||
|
creating channel...
|
||||||
|
fetching peer's key package...
|
||||||
|
DM with @bob created. Start typing!
|
||||||
|
[@bob] > ^LHey Bob, testing our E2E encrypted channel!
|
||||||
|
[@bob] > Right. MLS forward secrecy + post-quantum KEM.
|
||||||
|
[@bob] > /group-info
|
||||||
|
Conversation: @bob
|
||||||
|
Type: DM
|
||||||
|
Members: 2
|
||||||
|
alice (you), bob
|
||||||
|
MLS epoch: 1
|
||||||
|
[@bob] >
|
||||||
|
|
||||||
|
=== Bob (right) ===
|
||||||
|
❯ ./target/debug/qpq repl --username bob --password demop
|
||||||
|
ass2 --server 127.0.0.1:17123 --ca-cert /tmp/tmp.adbXGLOr
|
||||||
|
PY/server-cert.der --state /tmp/tmp.adbXGLOrPY/bob.bin
|
||||||
|
registering 'bob'...
|
||||||
|
user 'bob' registered
|
||||||
|
logging in as 'bob'...
|
||||||
|
logged in, session cached
|
||||||
|
identity: a8c2f19f1b080616b7206e02244fd14c2ab8821367392
|
||||||
|
af5ff9c89c69750c73f
|
||||||
|
KeyPackage uploaded
|
||||||
|
hybrid key uploaded
|
||||||
|
type /help for commands, Ctrl+D to exit
|
||||||
|
|
||||||
|
[no conversation] > /list
|
||||||
|
no conversations yet. Try /dm <username> or /create-gro
|
||||||
|
up <name>
|
||||||
|
[no conversation] > /switch @alice
|
||||||
|
error: conversation not found: @alice
|
||||||
|
[no conversation] > ^LWorks great -- the server never see
|
||||||
|
s plaintext?
|
||||||
|
error: no active conversation; use /dm or /create-group
|
||||||
|
first
|
||||||
|
[no conversation] > /whoami
|
||||||
|
identity: a8c2f19f1b080616b7206e02244fd14c2ab8821367392
|
||||||
|
af5ff9c89c69750c73f
|
||||||
|
hybrid key: yes
|
||||||
|
conversations: 0
|
||||||
|
[no conversation] >
|
||||||
@@ -1376,12 +1376,12 @@ pub fn cmd_export(
|
|||||||
///
|
///
|
||||||
/// Prints a summary. Does not require the encryption password (structural check only).
|
/// Prints a summary. Does not require the encryption password (structural check only).
|
||||||
pub fn cmd_export_verify(input: &Path) -> anyhow::Result<()> {
|
pub fn cmd_export_verify(input: &Path) -> anyhow::Result<()> {
|
||||||
use quicproquo_core::{verify_transcript_chain, ChainVerdict};
|
use quicproquo_core::{validate_transcript_structure, ChainVerdict};
|
||||||
|
|
||||||
let data = std::fs::read(input)
|
let data = std::fs::read(input)
|
||||||
.with_context(|| format!("read transcript file '{}'", input.display()))?;
|
.with_context(|| format!("read transcript file '{}'", input.display()))?;
|
||||||
|
|
||||||
match verify_transcript_chain(&data)? {
|
match validate_transcript_structure(&data)? {
|
||||||
ChainVerdict::Ok { records } => {
|
ChainVerdict::Ok { records } => {
|
||||||
println!(
|
println!(
|
||||||
"OK: transcript '{}' is structurally valid. {} record(s) found, hash chain intact.",
|
"OK: transcript '{}' is structurally valid. {} record(s) found, hash chain intact.",
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ impl ConversationStore {
|
|||||||
|
|
||||||
let salt = get_or_create_salt(&salt_path)?;
|
let salt = get_or_create_salt(&salt_path)?;
|
||||||
let key = derive_convdb_key(password, &salt)?;
|
let key = derive_convdb_key(password, &salt)?;
|
||||||
|
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||||
let hex_key = Zeroizing::new(hex::encode(&*key));
|
let hex_key = Zeroizing::new(hex::encode(&*key));
|
||||||
|
|
||||||
let conn = Connection::open(db_path).context("open conversation db")?;
|
let conn = Connection::open(db_path).context("open conversation db")?;
|
||||||
@@ -188,6 +189,7 @@ impl ConversationStore {
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let salt = get_or_create_salt(salt_path)?;
|
let salt = get_or_create_salt(salt_path)?;
|
||||||
let key = derive_convdb_key(password, &salt)?;
|
let key = derive_convdb_key(password, &salt)?;
|
||||||
|
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||||
let hex_key = Zeroizing::new(hex::encode(&*key));
|
let hex_key = Zeroizing::new(hex::encode(&*key));
|
||||||
|
|
||||||
let enc_path = db_path.with_extension("convdb-enc");
|
let enc_path = db_path.with_extension("convdb-enc");
|
||||||
|
|||||||
@@ -914,11 +914,11 @@ fn parse_duration_secs(s: &str) -> Option<u32> {
|
|||||||
|
|
||||||
/// Format a TTL in seconds into a human-friendly string.
|
/// Format a TTL in seconds into a human-friendly string.
|
||||||
fn format_ttl(secs: u32) -> String {
|
fn format_ttl(secs: u32) -> String {
|
||||||
if secs >= 86400 && secs % 86400 == 0 {
|
if secs >= 86400 && secs.is_multiple_of(86400) {
|
||||||
format!("{} day(s)", secs / 86400)
|
format!("{} day(s)", secs / 86400)
|
||||||
} else if secs >= 3600 && secs % 3600 == 0 {
|
} else if secs >= 3600 && secs.is_multiple_of(3600) {
|
||||||
format!("{} hour(s)", secs / 3600)
|
format!("{} hour(s)", secs / 3600)
|
||||||
} else if secs >= 60 && secs % 60 == 0 {
|
} else if secs >= 60 && secs.is_multiple_of(60) {
|
||||||
format!("{} minute(s)", secs / 60)
|
format!("{} minute(s)", secs / 60)
|
||||||
} else {
|
} else {
|
||||||
format!("{} second(s)", secs)
|
format!("{} second(s)", secs)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ pub fn anyhow_is_retriable(err: &anyhow::Error) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ pub fn set_auth(auth: &mut auth::Builder<'_>) -> anyhow::Result<()> {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
auth.set_version(ctx.version);
|
auth.set_version(ctx.version);
|
||||||
auth.set_access_token(&*ctx.access_token);
|
auth.set_access_token(&ctx.access_token);
|
||||||
auth.set_device_id(&ctx.device_id);
|
auth.set_device_id(&ctx.device_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ pub fn sha256(bytes: &[u8]) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ pub fn clear_cached_session(state_path: &Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// cargo_bin! only works for current package's binary; we spawn qpq-server from another package.
|
// cargo_bin! only works for current package's binary; we spawn qpq-server from another package.
|
||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
#![allow(clippy::await_holding_lock)] // AUTH_LOCK intentionally held across await to serialize tests
|
||||||
|
|
||||||
use std::{path::PathBuf, process::Command, sync::Mutex, time::Duration};
|
use std::{path::PathBuf, process::Command, sync::Mutex, time::Duration};
|
||||||
|
|
||||||
@@ -8,7 +10,6 @@ use portpicker::pick_unused_port;
|
|||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use hex;
|
|
||||||
|
|
||||||
// Required by rustls 0.23 when QUIC/TLS is used from this process (e.g. client in test).
|
// Required by rustls 0.23 when QUIC/TLS is used from this process (e.g. client in test).
|
||||||
fn ensure_rustls_provider() {
|
fn ensure_rustls_provider() {
|
||||||
@@ -46,7 +47,7 @@ impl Drop for ChildGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_health(server: &str, ca_cert: &PathBuf, server_name: &str) -> anyhow::Result<()> {
|
async fn wait_for_health(server: &str, ca_cert: &std::path::Path, server_name: &str) -> anyhow::Result<()> {
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
for _ in 0..30 {
|
for _ in 0..30 {
|
||||||
if local
|
if local
|
||||||
@@ -1090,7 +1091,7 @@ async fn e2e_key_rotation_update_path() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let alice_seed = bincode::deserialize::<StoredStateCompat>(&std::fs::read(&alice_state)?)?.identity_seed;
|
let alice_seed = bincode::deserialize::<StoredStateCompat>(&std::fs::read(&alice_state)?)?.identity_seed;
|
||||||
let bob_seed = bincode::deserialize::<StoredStateCompat>(&std::fs::read(&bob_state)?)?.identity_seed;
|
let bob_seed = bincode::deserialize::<StoredStateCompat>(&std::fs::read(&bob_state)?)?.identity_seed;
|
||||||
let alice_pk = IdentityKeypair::from_seed(alice_seed).public_key_bytes().to_vec();
|
let _alice_pk = IdentityKeypair::from_seed(alice_seed).public_key_bytes().to_vec();
|
||||||
let bob_pk = IdentityKeypair::from_seed(bob_seed).public_key_bytes().to_vec();
|
let bob_pk = IdentityKeypair::from_seed(bob_seed).public_key_bytes().to_vec();
|
||||||
let bob_pk_hex = hex_encode(&bob_pk);
|
let bob_pk_hex = hex_encode(&bob_pk);
|
||||||
|
|
||||||
@@ -1372,7 +1373,7 @@ async fn e2e_file_upload_download() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Build 2 KB of known data.
|
// Build 2 KB of known data.
|
||||||
let pattern = b"hello-world-file-test\n";
|
let pattern = b"hello-world-file-test\n";
|
||||||
let repeat_count = (2048 + pattern.len() - 1) / pattern.len();
|
let repeat_count = 2048_usize.div_ceil(pattern.len());
|
||||||
let file_data: Vec<u8> = pattern.iter().copied().cycle().take(repeat_count * pattern.len()).collect();
|
let file_data: Vec<u8> = pattern.iter().copied().cycle().take(repeat_count * pattern.len()).collect();
|
||||||
let file_data = &file_data[..2048]; // exactly 2 KB
|
let file_data = &file_data[..2048]; // exactly 2 KB
|
||||||
|
|
||||||
@@ -1472,7 +1473,7 @@ async fn e2e_file_upload_download() -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
partial == &file_data[100..300],
|
partial == file_data[100..300],
|
||||||
"partial download [100..300] does not match expected slice"
|
"partial download [100..300] does not match expected slice"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
//! Benchmark: Identity keypair operations, sealed sender, and message padding.
|
//! Benchmark: Identity keypair operations, sealed sender, and message padding.
|
||||||
//!
|
//!
|
||||||
//! Covers:
|
//! Covers:
|
||||||
@@ -34,14 +35,12 @@ fn bench_identity_verify(c: &mut Criterion) {
|
|||||||
|
|
||||||
c.bench_function("identity_verify", |b| {
|
c.bench_function("identity_verify", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
black_box(
|
|
||||||
IdentityKeypair::verify_raw(
|
IdentityKeypair::verify_raw(
|
||||||
black_box(&pk),
|
black_box(&pk),
|
||||||
black_box(payload),
|
black_box(payload),
|
||||||
black_box(&sig),
|
black_box(&sig),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap();
|
||||||
)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
//! Benchmark: Hybrid KEM (X25519 + ML-KEM-768) vs classical-only encryption.
|
//! Benchmark: Hybrid KEM (X25519 + ML-KEM-768) vs classical-only encryption.
|
||||||
//!
|
//!
|
||||||
//! Compares keypair generation, encryption, and decryption times for the
|
//! Compares keypair generation, encryption, and decryption times for the
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
//! Benchmark: MLS group operations at various group sizes.
|
//! Benchmark: MLS group operations at various group sizes.
|
||||||
//!
|
//!
|
||||||
//! Measures KeyPackage generation, group creation, member addition,
|
//! Measures KeyPackage generation, group creation, member addition,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
//! Benchmark: Cap'n Proto vs Protobuf serialization for chat message envelopes.
|
//! Benchmark: Cap'n Proto vs Protobuf serialization for chat message envelopes.
|
||||||
//!
|
//!
|
||||||
//! Compares serialization/deserialization speed and encoded size at three
|
//! Compares serialization/deserialization speed and encoded size at three
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ fn parse_file_ref(payload: &[u8]) -> Result<AppMessage, CoreError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -631,6 +631,7 @@ impl GroupMember {
|
|||||||
// ── Unit tests ────────────────────────────────────────────────────────────────
|
// ── Unit tests ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ impl OpenMlsCryptoProvider for HybridCryptoProvider {
|
|||||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use openmls_traits::types::HpkeKdfType;
|
use openmls_traits::types::HpkeKdfType;
|
||||||
|
|||||||
@@ -476,6 +476,7 @@ fn derive_aead_key(x25519_ss: &[u8], mlkem_ss: &[u8], extra_info: &[u8]) -> Key
|
|||||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,43 @@ pub fn verify_delivery_proof(
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serialize for IdentityKeypair {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_bytes(&self.seed[..])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for IdentityKeypair {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let bytes: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
|
||||||
|
let seed: [u8; 32] = bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?;
|
||||||
|
Ok(IdentityKeypair::from_seed(seed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for IdentityKeypair {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let fp = self.fingerprint();
|
||||||
|
f.debug_struct("IdentityKeypair")
|
||||||
|
.field(
|
||||||
|
"fingerprint",
|
||||||
|
&format!("{:02x}{:02x}{:02x}{:02x}…", fp[0], fp[1], fp[2], fp[3]),
|
||||||
|
)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod proof_tests {
|
mod proof_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
@@ -207,38 +243,3 @@ mod proof_tests {
|
|||||||
assert!(verify_delivery_proof(&pk, &proof).is_err());
|
assert!(verify_delivery_proof(&pk, &proof).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for IdentityKeypair {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_bytes(&self.seed[..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for IdentityKeypair {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let bytes: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
let seed: [u8; 32] = bytes
|
|
||||||
.as_slice()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?;
|
|
||||||
Ok(IdentityKeypair::from_seed(seed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for IdentityKeypair {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let fp = self.fingerprint();
|
|
||||||
f.debug_struct("IdentityKeypair")
|
|
||||||
.field(
|
|
||||||
"fingerprint",
|
|
||||||
&format!("{:02x}{:02x}{:02x}{:02x}…", fp[0], fp[1], fp[2], fp[3]),
|
|
||||||
)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ pub fn unpad(padded: &[u8]) -> Result<Vec<u8>, CoreError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ pub fn is_sealed(bytes: &[u8]) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ fn recompute_root(leaf: [u8; 32], path: &[PathStep]) -> Result<[u8; 32], KtError
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tree::MerkleLog;
|
use crate::tree::MerkleLog;
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ fn largest_power_of_two_less_than(n: usize) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ impl MeshIdentity {
|
|||||||
/// contains the Ed25519 seed in the clear.
|
/// contains the Ed25519 seed in the clear.
|
||||||
pub fn save(&self, path: &Path) -> anyhow::Result<()> {
|
pub fn save(&self, path: &Path) -> anyhow::Result<()> {
|
||||||
let file = IdentityFile {
|
let file = IdentityFile {
|
||||||
seed: hex::encode(&*self.keypair.seed_bytes()),
|
seed: hex::encode(self.keypair.seed_bytes()),
|
||||||
peers: self.known_peers.clone(),
|
peers: self.known_peers.clone(),
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string_pretty(&file)?;
|
let json = serde_json::to_string_pretty(&file)?;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ impl ConversationStore {
|
|||||||
|
|
||||||
if let Some(pw) = password {
|
if let Some(pw) = password {
|
||||||
let key = derive_db_key(pw, db_path)?;
|
let key = derive_db_key(pw, db_path)?;
|
||||||
|
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||||
let hex_key = Zeroizing::new(hex::encode(&*key));
|
let hex_key = Zeroizing::new(hex::encode(&*key));
|
||||||
conn.pragma_update(None, "key", format!("x'{}'", &*hex_key))
|
conn.pragma_update(None, "key", format!("x'{}'", &*hex_key))
|
||||||
.context("set SQLCipher key")?;
|
.context("set SQLCipher key")?;
|
||||||
@@ -561,6 +562,7 @@ fn row_to_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use crate::error::SdkError;
|
|||||||
/// Returns `(conversation_id, was_new)`.
|
/// Returns `(conversation_id, was_new)`.
|
||||||
/// - `was_new = true` — caller created the MLS group and sent the Welcome.
|
/// - `was_new = true` — caller created the MLS group and sent the Welcome.
|
||||||
/// - `was_new = false` — peer is the MLS initiator; caller should wait for Welcome.
|
/// - `was_new = false` — peer is the MLS initiator; caller should wait for Welcome.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn create_dm(
|
pub async fn create_dm(
|
||||||
rpc: &RpcClient,
|
rpc: &RpcClient,
|
||||||
conv_store: &ConversationStore,
|
conv_store: &ConversationStore,
|
||||||
@@ -177,6 +178,7 @@ pub fn create_group(
|
|||||||
/// Invite a peer to an existing group.
|
/// Invite a peer to an existing group.
|
||||||
///
|
///
|
||||||
/// Sends the Welcome to the new peer and the Commit to all existing members.
|
/// Sends the Welcome to the new peer and the Commit to all existing members.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn invite_to_group(
|
pub async fn invite_to_group(
|
||||||
rpc: &RpcClient,
|
rpc: &RpcClient,
|
||||||
conv_store: &ConversationStore,
|
conv_store: &ConversationStore,
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ pub fn load_state(path: &Path, password: Option<&str>) -> Result<StoredState, Sd
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ impl BlobService {
|
|||||||
if req
|
if req
|
||||||
.offset
|
.offset
|
||||||
.checked_add(req.chunk.len() as u64)
|
.checked_add(req.chunk.len() as u64)
|
||||||
.map_or(true, |end| end > req.total_size)
|
.is_none_or(|end| end > req.total_size)
|
||||||
{
|
{
|
||||||
return Err(DomainError::BadParams(format!(
|
return Err(DomainError::BadParams(format!(
|
||||||
"chunk out of bounds: offset={} + chunk_len={} > total_size={}",
|
"chunk out of bounds: offset={} + chunk_len={} > total_size={}",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub fn resolve_destination(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ impl NodeServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate chunk bounds.
|
// Validate chunk bounds.
|
||||||
if offset.checked_add(chunk.len() as u64).map_or(true, |end| end > total_size) {
|
if offset.checked_add(chunk.len() as u64).is_none_or(|end| end > total_size) {
|
||||||
return Promise::err(coded_error(
|
return Promise::err(coded_error(
|
||||||
E020_BAD_PARAMS,
|
E020_BAD_PARAMS,
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ impl NodeServiceImpl {
|
|||||||
if self.redact_logs {
|
if self.redact_logs {
|
||||||
let redacted_sender = sender_identity
|
let redacted_sender = sender_identity
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|id| redacted_prefix(id))
|
.map(redacted_prefix)
|
||||||
.unwrap_or_else(|| "sealed".to_string());
|
.unwrap_or_else(|| "sealed".to_string());
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
sender_prefix = %redacted_sender,
|
sender_prefix = %redacted_sender,
|
||||||
|
|||||||
@@ -1004,6 +1004,7 @@ impl<T> OptionalExt<T> for Result<T, rusqlite::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ pub struct FileBackedStore {
|
|||||||
identity_keys: Mutex<HashMap<String, Vec<u8>>>,
|
identity_keys: Mutex<HashMap<String, Vec<u8>>>,
|
||||||
endpoints: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
|
endpoints: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
|
||||||
/// Device registry: identity_key -> Vec<(device_id, device_name, registered_at)>
|
/// Device registry: identity_key -> Vec<(device_id, device_name, registered_at)>
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
devices: Mutex<HashMap<Vec<u8>, Vec<(Vec<u8>, String, u64)>>>,
|
devices: Mutex<HashMap<Vec<u8>, Vec<(Vec<u8>, String, u64)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,6 +959,7 @@ impl Store for FileBackedStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|||||||
380
docs/V2-DESIGN-ANALYSIS.md
Normal file
380
docs/V2-DESIGN-ANALYSIS.md
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
# quicproquo v2 — Design Analysis & Recommendations
|
||||||
|
|
||||||
|
> Multi-perspective retrospective of the v1 architecture.
|
||||||
|
> Produced 2026-03-04 by four parallel analysis agents examining server,
|
||||||
|
> client/UX, crypto/security, and project structure/DX.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
quicproquo v1 demonstrates strong fundamentals: QUIC-native transport, RFC 9420
|
||||||
|
MLS group encryption, post-quantum hybrid KEM, OPAQUE zero-knowledge auth, and a
|
||||||
|
working multi-language SDK surface. These are the right bets and put the project
|
||||||
|
ahead of most open-source messengers on the crypto front.
|
||||||
|
|
||||||
|
However, three architectural choices limit the path to production:
|
||||||
|
|
||||||
|
1. **capnp-rpc is `!Send`** — forces single-threaded RPC handling, blocking
|
||||||
|
scalability.
|
||||||
|
2. **Monolithic client with global state** — business logic is tangled into the
|
||||||
|
REPL, duplicated across TUI/GUI/Web, and cannot be used as a library.
|
||||||
|
3. **Poll-based delivery** — 1-second polling wastes bandwidth and adds latency;
|
||||||
|
no server-push channel exists.
|
||||||
|
|
||||||
|
A v2 should keep the crypto stack (MLS + hybrid PQ KEM + OPAQUE), keep QUIC, but
|
||||||
|
rearchitect the RPC layer, extract an SDK crate, and add push-based delivery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1 — What Works Well
|
||||||
|
|
||||||
|
### Transport & Protocol
|
||||||
|
- **QUIC (quinn) + TLS 1.3** — correct choice. Built-in encryption, connection
|
||||||
|
migration, 0-RTT potential. No reason to change.
|
||||||
|
- **Cap'n Proto schemas as API contract** — zero-copy wire format, compact
|
||||||
|
binary, schema evolution via ordinals. The *schemas* are good; the *RPC
|
||||||
|
runtime* is the problem.
|
||||||
|
|
||||||
|
### Cryptography
|
||||||
|
- **MLS (RFC 9420, openmls)** — only IETF-standard group E2E protocol. No
|
||||||
|
realistic alternative for groups > 2 members. Test suite is thorough (1005
|
||||||
|
lines covering 2-party, 3-party, hybrid, removal, leave, stale epoch).
|
||||||
|
- **Hybrid PQ KEM (X25519 + ML-KEM-768)** — forward-thinking dual-algorithm
|
||||||
|
protection. Well-implemented with versioned wire format, proper zeroization,
|
||||||
|
and 12 targeted tests. Ahead of Signal (PQXDH, late 2023) and Matrix (no PQ).
|
||||||
|
- **OPAQUE (RFC 9497)** — server never sees passwords. Ristretto255 + Argon2id
|
||||||
|
is best-in-class.
|
||||||
|
- **Sealed sender, safety numbers, message padding** — all clean, simple,
|
||||||
|
correct. Safety numbers match Signal's 5200-iteration HMAC-SHA256 cost.
|
||||||
|
- **Zeroization discipline** — secrets wrapped in `Zeroizing`, Debug impls
|
||||||
|
redact keys, no `.unwrap()` in crypto paths.
|
||||||
|
- **WASM feature gating** — `core/native` cleanly separates WASM-safe crypto
|
||||||
|
from native-only modules (MLS, OPAQUE, filesystem).
|
||||||
|
|
||||||
|
### Server Design
|
||||||
|
- **Store trait abstraction** — 30+ methods, clean backend swap (SqlStore vs
|
||||||
|
FileBackedStore). Well-factored.
|
||||||
|
- **OPAQUE auth with timing floors** — `resolveUser`/`resolveIdentity` mask
|
||||||
|
lookup timing to prevent username enumeration.
|
||||||
|
- **Delivery proofs** — Ed25519-signed receipt of server acceptance. Clients get
|
||||||
|
cryptographic evidence.
|
||||||
|
- **`wasNew` flag on createChannel** — elegantly solves the dual-MLS-group race
|
||||||
|
condition where both DM parties try to initialize.
|
||||||
|
- **Plugin hooks (C-ABI)** — `#![no_std]` vtable, zero dependencies, chained
|
||||||
|
hooks with continue/reject protocol. Clean extensibility.
|
||||||
|
- **Production config validation** — enforces encrypted storage, strong auth
|
||||||
|
tokens, pre-existing TLS certs.
|
||||||
|
|
||||||
|
### Client & DX
|
||||||
|
- **Zero-config local dev** — `qpq --username alice --password pass` auto-starts
|
||||||
|
server, generates TLS certs, registers, and logs in. Genuinely excellent.
|
||||||
|
- **Encrypted-at-rest everything** — state file (QPCE), conversation DB
|
||||||
|
(SQLCipher), session cache. Argon2id + ChaCha20-Poly1305 throughout.
|
||||||
|
- **Playbook system** — YAML-scripted command execution with assertions. Great
|
||||||
|
for CI/integration testing.
|
||||||
|
- **Conversation store** — SQLite with deduplication, outbox for offline
|
||||||
|
queuing, activity tracking.
|
||||||
|
- **Conventional commits, GPG-signed** — consistent `feat:`/`fix:`/`docs:`
|
||||||
|
discipline.
|
||||||
|
- **Security lints enforced by build** — `clippy::unwrap_used = "deny"`,
|
||||||
|
`unsafe_code = "warn"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2 — What Needs Rethinking
|
||||||
|
|
||||||
|
### 2.1 RPC Layer: capnp-rpc is the #1 Scalability Bottleneck
|
||||||
|
|
||||||
|
**Problem:** `capnp-rpc` uses `Rc` internally and is `!Send`. Everything runs on
|
||||||
|
a `LocalSet` with `spawn_local`. All 27 RPC methods serialize through a single
|
||||||
|
thread. No work-stealing, no multi-core utilization.
|
||||||
|
|
||||||
|
**Impact:** With 1000+ concurrent clients, the single-threaded executor cannot
|
||||||
|
keep up. A slow `fetchWait` (30s timeout) blocks the entire connection.
|
||||||
|
|
||||||
|
**Also:** The WebSocket bridge (`ws_bridge.rs`, 645 lines) exists solely because
|
||||||
|
Cap'n Proto cannot run in browsers. This duplicates handler logic and creates
|
||||||
|
maintenance burden.
|
||||||
|
|
||||||
|
### 2.2 Client Architecture: Monolith with Global State
|
||||||
|
|
||||||
|
**Problem:** `AUTH_CONTEXT` is a process-wide `RwLock<Option<ClientAuth>>`.
|
||||||
|
Business logic (MLS processing, sealed sender, hybrid decryption, message
|
||||||
|
routing) lives inside `repl.rs`'s `poll_messages()` — a 100-line function that
|
||||||
|
mixes transport, crypto, routing, and storage.
|
||||||
|
|
||||||
|
**Impact:** Every frontend (REPL, TUI, GUI, Web) must reimplement message
|
||||||
|
processing. The TUI already duplicates it. The GUI stub and mobile PoC would need
|
||||||
|
yet another copy. Client cannot be used as a library.
|
||||||
|
|
||||||
|
### 2.3 Delivery Model: Poll-Based, No Push Channel
|
||||||
|
|
||||||
|
**Problem:** Client polls every 1 second with `fetch_wait(timeout_ms=0)` — never
|
||||||
|
actually long-polls. Constant network traffic even when idle. ~1 second latency
|
||||||
|
for message delivery.
|
||||||
|
|
||||||
|
**Also:** `fetch` is destructive (drains queue). If the client crashes between
|
||||||
|
receive and processing, messages are lost.
|
||||||
|
|
||||||
|
### 2.4 Connection Model: Single Stream
|
||||||
|
|
||||||
|
**Problem:** `max_concurrent_bidi_streams(1)` means the entire QUIC connection is
|
||||||
|
effectively single-stream. A blocking `fetchWait` prevents all other RPCs.
|
||||||
|
|
||||||
|
### 2.5 Storage: Single Mutex-Guarded SQLite Connection
|
||||||
|
|
||||||
|
**Problem:** `SqlStore` uses `Mutex<Connection>`. Every database operation
|
||||||
|
acquires a global lock. Under concurrent load, all storage access serializes.
|
||||||
|
|
||||||
|
**Also:** `FileBackedStore` flushes the entire map on every write (O(n) I/O).
|
||||||
|
Sessions are in-memory only — server restart forces all clients to re-login.
|
||||||
|
|
||||||
|
### 2.6 Key Management Gaps
|
||||||
|
|
||||||
|
- **DiskKeyStore** — HPKE private keys stored as plaintext bincode on disk. No
|
||||||
|
encryption at rest.
|
||||||
|
- **MLS group state** — `GroupMember` holds `MlsGroup` in memory only. Process
|
||||||
|
crash loses all group state.
|
||||||
|
- **Token zeroization** — `AuthContext.token`, `ClientAuth.access_token` are not
|
||||||
|
wrapped in `Zeroizing`.
|
||||||
|
|
||||||
|
### 2.7 Workspace Bloat
|
||||||
|
|
||||||
|
12 crates for a project at this maturity is excessive. Several are thin stubs
|
||||||
|
(`quicproquo-gen`, `quicproquo-bot` at 354 lines) or broken (`quicproquo-gui`
|
||||||
|
fails `cargo build --workspace`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3 — v2 Architecture Recommendations
|
||||||
|
|
||||||
|
### 3.1 Replace capnp-rpc with a Send-Compatible RPC Framework
|
||||||
|
|
||||||
|
**Recommendation:** Switch to **tonic (gRPC)** or a custom framing layer.
|
||||||
|
|
||||||
|
| Dimension | capnp-rpc (v1) | tonic/gRPC (v2) |
|
||||||
|
|-----------|---------------|-----------------|
|
||||||
|
| Threading | `!Send`, single-threaded | `Send + Sync`, multi-threaded |
|
||||||
|
| Browser | Requires WS bridge | grpc-web native |
|
||||||
|
| Streaming | Not supported | Built-in |
|
||||||
|
| Middleware | None (copy-paste auth) | Interceptors/layers |
|
||||||
|
| Ecosystem | Niche | Massive (every language) |
|
||||||
|
|
||||||
|
**Alternative:** Keep Cap'n Proto *schemas* for serialization (zero-copy
|
||||||
|
advantage) but replace capnp-rpc with custom framing over QUIC streams. This
|
||||||
|
preserves the wire format while gaining `Send` compatibility.
|
||||||
|
|
||||||
|
The WS bridge would be eliminated entirely — grpc-web or WebTransport gives
|
||||||
|
browsers direct access.
|
||||||
|
|
||||||
|
### 3.2 Extract an SDK Crate (Most Important Client Change)
|
||||||
|
|
||||||
|
Create `quicproquo-sdk` that owns all business logic:
|
||||||
|
|
||||||
|
```
|
||||||
|
quicproquo-sdk/
|
||||||
|
src/
|
||||||
|
client.rs -- QpqClient: connect, login, send, receive
|
||||||
|
events.rs -- ClientEvent enum (push-based)
|
||||||
|
conversation.rs -- ConversationHandle, group management
|
||||||
|
crypto.rs -- MLS pipeline, sealed sender, hybrid decryption
|
||||||
|
sync.rs -- message sync, offline queue, retry
|
||||||
|
```
|
||||||
|
|
||||||
|
All frontends become thin shells:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLI/REPL -> calls sdk
|
||||||
|
TUI -> calls sdk
|
||||||
|
Tauri GUI -> calls sdk (via Tauri commands)
|
||||||
|
Mobile -> calls sdk (via C FFI)
|
||||||
|
Web/WASM -> calls sdk (compiled to wasm32)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key API shape:**
|
||||||
|
```rust
|
||||||
|
pub struct QpqClient { /* session, rpc, crypto pipeline */ }
|
||||||
|
|
||||||
|
impl QpqClient {
|
||||||
|
pub async fn connect(config: ClientConfig) -> Result<Self>;
|
||||||
|
pub async fn login(username: &str, password: &str) -> Result<Self>;
|
||||||
|
pub async fn dm(&mut self, username: &str) -> Result<ConversationHandle>;
|
||||||
|
pub async fn create_group(&mut self, name: &str) -> Result<ConversationHandle>;
|
||||||
|
pub async fn send(&mut self, text: &str) -> Result<MessageId>;
|
||||||
|
pub fn subscribe(&self) -> Receiver<ClientEvent>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No global state. No `AUTH_CONTEXT`. Auth context is per-`QpqClient` instance.
|
||||||
|
|
||||||
|
### 3.3 Add Push-Based Delivery
|
||||||
|
|
||||||
|
**Recommendation:** Dedicated QUIC unidirectional stream for server-push
|
||||||
|
notifications.
|
||||||
|
|
||||||
|
```
|
||||||
|
Client opens bidi stream 0 -> RPC channel (request/response)
|
||||||
|
Server opens uni stream 1 -> push notifications (new message, typing, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- Zero-latency message delivery (no polling)
|
||||||
|
- No idle network traffic
|
||||||
|
- Typing indicators delivered in real-time
|
||||||
|
- Graceful degradation: fall back to long-poll if push stream fails
|
||||||
|
|
||||||
|
**Also:** Make `peek` + `ack` the default delivery pattern (not destructive
|
||||||
|
`fetch`). Add idempotency keys to prevent duplicate messages on retry.
|
||||||
|
|
||||||
|
### 3.4 Multi-Stream Connections
|
||||||
|
|
||||||
|
Allow 4-8 concurrent bidirectional QUIC streams per connection. This enables:
|
||||||
|
- Pipelined RPCs (send while fetching)
|
||||||
|
- Concurrent blob upload + chat
|
||||||
|
- `fetchWait` on one stream without blocking others
|
||||||
|
|
||||||
|
### 3.5 Storage Improvements
|
||||||
|
|
||||||
|
| Change | Rationale |
|
||||||
|
|--------|-----------|
|
||||||
|
| Drop `FileBackedStore` | O(n) flush per write, no federation support |
|
||||||
|
| Connection pool for SQLite | Replace `Mutex<Connection>` with r2d2/deadpool |
|
||||||
|
| Persist sessions to DB | Server restart shouldn't force re-login |
|
||||||
|
| Encrypt DiskKeyStore at rest | HPKE private keys in plaintext is a real vuln |
|
||||||
|
| Persist MLS group state | Process crash shouldn't lose group state |
|
||||||
|
| Atomic keystore writes | tempfile-then-rename pattern |
|
||||||
|
|
||||||
|
### 3.6 Crypto Stack Refinements
|
||||||
|
|
||||||
|
The algorithms are correct. The refinements are operational:
|
||||||
|
|
||||||
|
| Change | Rationale |
|
||||||
|
|--------|-----------|
|
||||||
|
| Typed MLS error variants | Stop losing error info via `format!("{e:?}")` |
|
||||||
|
| Formalize hybrid PQ ciphersuite ID | Replace length-based key detection |
|
||||||
|
| Remove all InsecureServerCertVerifier | No TLS bypass on any platform |
|
||||||
|
| Add passkey/WebAuthn alt-auth | Better UX for GUI/mobile, no password to forget |
|
||||||
|
| Consider Double Ratchet for 1:1 DMs | MLS is over-engineered for 2-party; DR gives better per-message forward secrecy |
|
||||||
|
| Token/session secret zeroization | `AuthContext.token` et al. need `Zeroizing` wrappers |
|
||||||
|
| Fix serde deserialization of secrets | Intermediate non-zeroized `Vec<u8>` in `IdentityKeypair::deserialize` |
|
||||||
|
|
||||||
|
### 3.7 Workspace Restructuring
|
||||||
|
|
||||||
|
**Reduce from 12 to 8 crates:**
|
||||||
|
|
||||||
|
```
|
||||||
|
quicproquo-core -- crypto primitives (keep)
|
||||||
|
quicproquo-proto -- schema codegen (keep)
|
||||||
|
quicproquo-plugin-api -- #![no_std] C-ABI (keep)
|
||||||
|
quicproquo-kt -- key transparency (keep)
|
||||||
|
quicproquo-sdk -- NEW: business logic library
|
||||||
|
quicproquo-server -- server binary (keep)
|
||||||
|
quicproquo-client -- CLI/TUI binary, depends on sdk (keep, slimmed)
|
||||||
|
quicproquo-p2p -- mesh networking (keep, feature-flagged)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Merge/remove:**
|
||||||
|
- `bot` -> `sdk::bot` module
|
||||||
|
- `ffi` -> `sdk` with `--features c-ffi`
|
||||||
|
- `gen` -> `scripts/` or `xtask`
|
||||||
|
- `gui` -> `apps/gui/` outside workspace (Tauri project)
|
||||||
|
- `mobile` -> `examples/` (research spike)
|
||||||
|
|
||||||
|
**Add `[workspace.default-members]`** so `cargo build` doesn't attempt GUI.
|
||||||
|
**Add `justfile`** with `build`, `test`, `test-e2e`, `build-wasm`, `docker`.
|
||||||
|
|
||||||
|
### 3.8 Plugin System Evolution
|
||||||
|
|
||||||
|
| Change | Rationale |
|
||||||
|
|--------|-----------|
|
||||||
|
| Add `version: u32` to `HookVTable` | ABI stability — check version on load |
|
||||||
|
| Config passthrough | `qpq_plugin_init(vtable, config_json)` |
|
||||||
|
| Async hooks | Plugins that call external services shouldn't block Tokio |
|
||||||
|
| Evaluate WASM plugins | Sandboxed community plugins (keep C-ABI for first-party) |
|
||||||
|
|
||||||
|
### 3.9 Federation Improvements
|
||||||
|
|
||||||
|
| Change | Rationale |
|
||||||
|
|--------|-----------|
|
||||||
|
| DNS SRV / .well-known discovery | Static peer config doesn't scale |
|
||||||
|
| Persistent relay queue with retry | Messages to offline peers are currently lost |
|
||||||
|
| Deterministic channel ID derivation | Avoid cross-server channel conflicts |
|
||||||
|
| Keep mDNS as optional mesh feature | Not for internet-scale, but good for LAN |
|
||||||
|
|
||||||
|
### 3.10 Test & CI Improvements
|
||||||
|
|
||||||
|
| Change | Rationale |
|
||||||
|
|--------|-----------|
|
||||||
|
| Per-client auth context | Removes `--test-threads 1` constraint |
|
||||||
|
| Mock server for client unit tests | Fast tests without spawning real server |
|
||||||
|
| Fuzz testing (cargo-fuzz) | Hybrid KEM, sealed sender, padding, Cap'n Proto deser |
|
||||||
|
| WS bridge unit tests | 645 lines, zero tests, security-critical |
|
||||||
|
| WASM + Go SDK in CI | Currently untested in CI |
|
||||||
|
| Separate E2E from unit test CI job | Different speed, different failure modes |
|
||||||
|
| macOS CI | FFI/mobile cross-compilation validation |
|
||||||
|
| Release automation | Binary artifacts, Docker tags, WASM npm publish |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 4 — Ecosystem Positioning
|
||||||
|
|
||||||
|
### Don't compete with Signal or Matrix directly.
|
||||||
|
|
||||||
|
**Target: Privacy-first messaging infrastructure for developers and
|
||||||
|
organizations.**
|
||||||
|
|
||||||
|
quicproquo's differentiators — QUIC-native transport, post-quantum crypto, MLS,
|
||||||
|
plugin system, multi-language SDKs, embeddable architecture — point toward an
|
||||||
|
infrastructure play, not a consumer app.
|
||||||
|
|
||||||
|
Think: *"the Postgres of E2E encrypted messaging"* — a high-quality open-source
|
||||||
|
server and protocol that other projects build on.
|
||||||
|
|
||||||
|
| Segment | Value Proposition |
|
||||||
|
|---------|-------------------|
|
||||||
|
| **Developer tool** | API-first messenger for encrypted bots and integrations |
|
||||||
|
| **Embeddable** | C FFI + WASM + Go SDK for embedding in other apps |
|
||||||
|
| **Enterprise** | On-prem, plugins for compliance/audit, OPAQUE zero-knowledge auth |
|
||||||
|
| **Research** | Post-quantum crypto, MLS reference implementation, mesh networking |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 5 — Priority Ordering
|
||||||
|
|
||||||
|
### Phase 1: Foundation (unblocks everything else)
|
||||||
|
1. Replace capnp-rpc with Send-compatible framework
|
||||||
|
2. Extract SDK crate from client
|
||||||
|
3. Per-client auth context (no global state)
|
||||||
|
|
||||||
|
### Phase 2: Reliability
|
||||||
|
4. Push-based delivery (QUIC uni-stream)
|
||||||
|
5. Multi-stream connections
|
||||||
|
6. Persist sessions + MLS group state
|
||||||
|
7. Encrypt DiskKeyStore at rest
|
||||||
|
8. peek+ack as default delivery
|
||||||
|
|
||||||
|
### Phase 3: Polish
|
||||||
|
9. Workspace restructuring (12 -> 8 crates)
|
||||||
|
10. TUI as primary interactive mode (built on SDK)
|
||||||
|
11. Plugin system v2 (versioning, config, async)
|
||||||
|
12. Federation retry queue + discovery
|
||||||
|
|
||||||
|
### Phase 4: Ecosystem
|
||||||
|
13. Full MLS in WASM (browser E2E)
|
||||||
|
14. WebTransport (eliminate WS bridge)
|
||||||
|
15. Tauri GUI (built on SDK)
|
||||||
|
16. Release automation + expanded CI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix — Analysis Sources
|
||||||
|
|
||||||
|
This document was produced by four parallel analysis agents:
|
||||||
|
|
||||||
|
| Agent | Scope | Files Read |
|
||||||
|
|-------|-------|-----------|
|
||||||
|
| server-analyst | Transport, RPC, delivery, storage, federation | 27 server .rs files, 4 schemas, core transport |
|
||||||
|
| client-analyst | REPL, UX, state, multi-platform, SDK design | All client .rs, GUI, mobile, TS demo |
|
||||||
|
| security-analyst | MLS, OPAQUE, hybrid KEM, keystore, identity | All core .rs, review doc |
|
||||||
|
| dx-analyst | Workspace, build, tests, plugins, CI, ecosystem | All Cargo.toml, tests, CI, plugins, SDKs |
|
||||||
328
docs/V2-MASTER-PLAN.md
Normal file
328
docs/V2-MASTER-PLAN.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# quicproquo v2 — Master Implementation Plan
|
||||||
|
|
||||||
|
> Created 2026-03-04. This is the authoritative plan for the v2 rewrite.
|
||||||
|
> See also: `docs/V2-DESIGN-ANALYSIS.md` for the detailed retrospective.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The v1 codebase has strong crypto foundations (MLS, hybrid PQ KEM, OPAQUE) but three
|
||||||
|
architectural bottlenecks: capnp-rpc is `!Send` (single-threaded), client business logic
|
||||||
|
is trapped in a monolithic REPL with global state, and delivery is poll-based.
|
||||||
|
|
||||||
|
This plan creates v2 on a new branch, keeping the crypto stack intact and replacing
|
||||||
|
the RPC/transport layer, extracting an SDK, and restructuring the workspace.
|
||||||
|
|
||||||
|
**Key decisions:**
|
||||||
|
- Transport: Protobuf (prost) + custom framing over QUIC (quinn)
|
||||||
|
- Mobile: Tauri 2 (same Rust SDK backend, web UI)
|
||||||
|
- Branch strategy: `v2` branch from main, not a fresh repo
|
||||||
|
- Constraints: Rust, QUIC, GPG-signed commits, zeroize secrets, no stubs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Frontends │
|
||||||
|
│ CLI/TUI │ Tauri GUI/Mobile │ Web (WebTransport)│
|
||||||
|
└─────┬─────┴────────┬───────────┴──────────┬─────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ quicproquo-sdk │
|
||||||
|
│ QpqClient { connect, login, send, recv, subscribe } │
|
||||||
|
│ Event system (tokio broadcast) │
|
||||||
|
│ Crypto pipeline (MLS, sealed sender, hybrid) │
|
||||||
|
│ Conversation store (SQLCipher) │
|
||||||
|
└──────────────────────┬──────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ quicproquo-rpc │
|
||||||
|
│ QUIC framing: [method:u16][req_id:u32][len:u32][pb] │
|
||||||
|
│ Multi-stream (1 RPC per stream) │
|
||||||
|
│ Server-push via uni-streams │
|
||||||
|
│ tower middleware (auth, rate-limit) │
|
||||||
|
└──────────────────────┬──────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ quicproquo-server │
|
||||||
|
│ Domain services (auth, delivery, channel, blob) │
|
||||||
|
│ Store trait → SqlStore (connection pool) │
|
||||||
|
│ Plugin hooks, federation, KT │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wire Format
|
||||||
|
|
||||||
|
Per QUIC bidirectional stream (request/response):
|
||||||
|
```
|
||||||
|
Request: [method_id: u16][request_id: u32][payload_len: u32][protobuf bytes]
|
||||||
|
Response: [status: u8][request_id: u32][payload_len: u32][protobuf bytes]
|
||||||
|
```
|
||||||
|
|
||||||
|
Per QUIC unidirectional stream (server → client push):
|
||||||
|
```
|
||||||
|
Push: [event_type: u16][payload_len: u32][protobuf bytes]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each RPC opens its own QUIC bidi stream → natural multi-stream, no head-of-line blocking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workspace Structure (v2: 9 crates)
|
||||||
|
|
||||||
|
```
|
||||||
|
quicproquo/
|
||||||
|
├── crates/
|
||||||
|
│ ├── quicproquo-core/ # KEEP AS-IS — crypto primitives, MLS, hybrid KEM
|
||||||
|
│ ├── quicproquo-kt/ # KEEP AS-IS — key transparency
|
||||||
|
│ ├── quicproquo-plugin-api/ # KEEP AS-IS — #![no_std] C-ABI
|
||||||
|
│ ├── quicproquo-proto/ # REWRITE — protobuf schemas + prost codegen
|
||||||
|
│ ├── quicproquo-rpc/ # NEW — QUIC RPC framework (framing, dispatch, tower)
|
||||||
|
│ ├── quicproquo-sdk/ # NEW — client business logic library
|
||||||
|
│ ├── quicproquo-server/ # REWRITE — domain services + RPC handlers
|
||||||
|
│ ├── quicproquo-client/ # REWRITE — thin CLI/TUI shell over SDK
|
||||||
|
│ └── quicproquo-p2p/ # KEEP — iroh mesh (feature-flagged, later)
|
||||||
|
├── apps/
|
||||||
|
│ └── gui/ # Tauri 2 desktop + mobile app (outside workspace)
|
||||||
|
├── proto/ # .proto source files
|
||||||
|
│ └── qpq/v1/
|
||||||
|
│ ├── auth.proto # OPAQUE registration + login (4 methods)
|
||||||
|
│ ├── delivery.proto # enqueue, fetch, peek, ack, batch (6 methods)
|
||||||
|
│ ├── keys.proto # key package + hybrid key CRUD (5 methods)
|
||||||
|
│ ├── channel.proto # channel create (1 method)
|
||||||
|
│ ├── user.proto # resolve user/identity (2 methods)
|
||||||
|
│ ├── blob.proto # upload/download (2 methods)
|
||||||
|
│ ├── device.proto # register/list/revoke (3 methods)
|
||||||
|
│ ├── p2p.proto # endpoint publish/resolve + health (3 methods)
|
||||||
|
│ ├── federation.proto # relay + proxy (6 methods)
|
||||||
|
│ ├── push.proto # server-push events (NEW)
|
||||||
|
│ └── common.proto # shared types (Auth, Envelope, Error)
|
||||||
|
├── sdks/
|
||||||
|
│ ├── go/ # Go SDK (regenerate from .proto)
|
||||||
|
│ └── typescript/ # TS SDK (WebTransport client)
|
||||||
|
├── justfile # NEW — build commands
|
||||||
|
└── Cargo.toml # workspace root
|
||||||
|
```
|
||||||
|
|
||||||
|
**Removed from workspace:**
|
||||||
|
- `quicproquo-bot` → `sdk::bot` module
|
||||||
|
- `quicproquo-ffi` → `sdk` with `--features c-ffi`
|
||||||
|
- `quicproquo-gen` → `scripts/`
|
||||||
|
- `quicproquo-gui` → `apps/gui/` (Tauri project, outside workspace)
|
||||||
|
- `quicproquo-mobile` → merged into `apps/gui/` (Tauri 2 mobile)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Crate Reuse Assessment
|
||||||
|
|
||||||
|
| v1 Crate | capnp deps? | v2 Action | Effort |
|
||||||
|
|----------|:-----------:|-----------|--------|
|
||||||
|
| **quicproquo-core** | None | Copy as-is | Zero |
|
||||||
|
| **quicproquo-kt** | None | Copy as-is | Zero |
|
||||||
|
| **quicproquo-plugin-api** | None | Copy as-is | Zero |
|
||||||
|
| **quicproquo-p2p** | None | Copy as-is | Zero |
|
||||||
|
| **quicproquo-proto** | 100% capnp | Replace with prost codegen | Medium |
|
||||||
|
| **quicproquo-server** | 16/20 files | Extract domain logic, rewrite handlers | High |
|
||||||
|
| **quicproquo-client** | 6/10 files | Extract to SDK, thin CLI shell | High |
|
||||||
|
|
||||||
|
### Key Files to Reuse Directly
|
||||||
|
|
||||||
|
| Source (v1) | Destination (v2) | Notes |
|
||||||
|
|-------------|------------------|-------|
|
||||||
|
| `crates/quicproquo-core/` (entire) | same path | Zero changes |
|
||||||
|
| `crates/quicproquo-kt/` (entire) | same path | Zero changes |
|
||||||
|
| `crates/quicproquo-plugin-api/` (entire) | same path | Zero changes |
|
||||||
|
| `server/src/storage.rs` | `server/src/storage.rs` | Store trait — keep |
|
||||||
|
| `server/src/sql_store.rs` | `server/src/sql_store.rs` | Add connection pool |
|
||||||
|
| `server/src/hooks.rs` | `server/src/hooks.rs` | Plugin system — keep |
|
||||||
|
| `server/src/plugin_loader.rs` | `server/src/plugin_loader.rs` | Keep |
|
||||||
|
| `server/src/error_codes.rs` | `server/src/error_codes.rs` | Keep |
|
||||||
|
| `server/src/config.rs` | `server/src/config.rs` | Update for new transport |
|
||||||
|
| `client/src/conversation.rs` | `sdk/src/conversation.rs` | Move to SDK |
|
||||||
|
| `client/src/token_cache.rs` | `sdk/src/token_cache.rs` | Move to SDK |
|
||||||
|
| `client/src/display.rs` | `client/src/display.rs` | Keep in CLI |
|
||||||
|
| `schemas/*.capnp` | reference only | Translate to .proto |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phased Implementation
|
||||||
|
|
||||||
|
### Phase 1: Foundation
|
||||||
|
**Goal:** v2 branch with new workspace, proto schemas, RPC framework skeleton, SDK skeleton.
|
||||||
|
**Scope:** Compiles, no runtime functionality yet.
|
||||||
|
|
||||||
|
1. **Create v2 branch** from main
|
||||||
|
2. **Restructure workspace** — update root Cargo.toml, create new crate dirs, add justfile
|
||||||
|
3. **Write .proto files** — translate all 33 RPC methods + push events from Cap'n Proto
|
||||||
|
4. **Create quicproquo-proto crate** — prost-build codegen
|
||||||
|
5. **Create quicproquo-rpc crate** — QUIC RPC framework:
|
||||||
|
- `framing.rs` — wire format encode/decode (request, response, push)
|
||||||
|
- `server.rs` — accept QUIC connections, dispatch to handlers
|
||||||
|
- `client.rs` — connect, send requests, receive responses + push events
|
||||||
|
- `middleware.rs` — tower-based auth + rate-limit layers
|
||||||
|
- `method.rs` — method registry (method_id → async handler fn)
|
||||||
|
6. **Create quicproquo-sdk crate** — public API skeleton:
|
||||||
|
- `client.rs` — `QpqClient` struct
|
||||||
|
- `events.rs` — `ClientEvent` enum
|
||||||
|
- `conversation.rs` — `ConversationHandle`, `ConversationStore`
|
||||||
|
- `config.rs` — `ClientConfig`
|
||||||
|
7. **Extract server domain types** — `server/src/domain/` module:
|
||||||
|
- `types.rs` — plain Rust request/response types
|
||||||
|
- `auth.rs` — OPAQUE logic extracted from auth_ops.rs
|
||||||
|
- `delivery.rs` — enqueue/fetch logic extracted from delivery.rs
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `cargo build --workspace` succeeds
|
||||||
|
- `cargo test -p quicproquo-core` passes (72 tests)
|
||||||
|
- Proto codegen works
|
||||||
|
- RPC framework compiles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Server Core
|
||||||
|
**Goal:** Working server with all 33 RPC handlers over QUIC.
|
||||||
|
|
||||||
|
1. **RPC dispatch** — method registry, connection lifecycle
|
||||||
|
2. **Domain handlers** — all 33 methods as `async fn(Request) -> Result<Response>`
|
||||||
|
- Auth (4): OPAQUE register start/finish, login start/finish
|
||||||
|
- Delivery (6): enqueue, fetch, fetchWait, peek, ack, batchEnqueue
|
||||||
|
- Keys (5): upload/fetch key package, upload/fetch/batch-fetch hybrid key
|
||||||
|
- Channels (1): createChannel
|
||||||
|
- Users (2): resolveUser, resolveIdentity
|
||||||
|
- Blobs (2): uploadBlob, downloadBlob
|
||||||
|
- Devices (3): registerDevice, listDevices, revokeDevice
|
||||||
|
- P2P (3): health, publishEndpoint, resolveEndpoint
|
||||||
|
- Federation (6): relay enqueue/batch, proxy fetch/resolve, health
|
||||||
|
3. **Server-push** — notification stream via QUIC uni-stream
|
||||||
|
4. **Storage upgrades:**
|
||||||
|
- Drop `FileBackedStore`
|
||||||
|
- Connection pool (deadpool-sqlite)
|
||||||
|
- Persist sessions to SQLite
|
||||||
|
- Atomic queue depth check + enqueue
|
||||||
|
5. **Tower middleware** — auth validation, rate limiting, audit logging
|
||||||
|
6. **Multi-stream** — concurrent RPCs per connection (remove 1-stream limit)
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Server starts, accepts QUIC connections
|
||||||
|
- Health check RPC works
|
||||||
|
- OPAQUE registration + login works
|
||||||
|
- Message enqueue + fetch round-trip
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: SDK
|
||||||
|
**Goal:** Complete client SDK library — the heart of v2.
|
||||||
|
|
||||||
|
1. **QpqClient** — connect, OPAQUE auth, session management (no global state)
|
||||||
|
2. **Crypto pipeline** — MLS processing, sealed sender unwrap, hybrid decrypt
|
||||||
|
(extracted from repl.rs `poll_messages()`)
|
||||||
|
3. **Conversation management** — create DM, create group, invite, remove, send, receive
|
||||||
|
4. **Event system** — `tokio::broadcast<ClientEvent>` replacing poll loop
|
||||||
|
- `MessageReceived`, `TypingIndicator`, `ConversationCreated`
|
||||||
|
- `MemberJoined`, `MemberLeft`, `ConnectionLost`, `Reconnected`
|
||||||
|
5. **Offline support** — outbox queue, retry with backoff, sync on reconnect
|
||||||
|
6. **ConversationStore** — SQLCipher local DB (migrate from client/conversation.rs)
|
||||||
|
7. **Key management** — encrypted DiskKeyStore, MLS group state persistence
|
||||||
|
8. **Token/secret zeroization** — `AuthContext.token` etc. wrapped in `Zeroizing`
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- SDK integration test: connect → login → create DM → send → receive
|
||||||
|
- No global state (`AUTH_CONTEXT` eliminated)
|
||||||
|
- Event subscription works
|
||||||
|
- Offline outbox drains on reconnect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Client
|
||||||
|
**Goal:** CLI and TUI as thin shells over SDK.
|
||||||
|
|
||||||
|
1. **CLI binary** (`qpq`) — clap subcommands calling `QpqClient`
|
||||||
|
2. **REPL** — readline with tab-completion (rustyline), categorized `/help`
|
||||||
|
3. **TUI** — ratatui, subscribes to `QpqClient::subscribe()` events
|
||||||
|
4. **Simplified commands:**
|
||||||
|
- Hide MLS/KeyPackage internals (auto-refresh)
|
||||||
|
- Message references by short ID (not index)
|
||||||
|
- Batch operations (`/create-group team alice bob`)
|
||||||
|
- Categorized help (Chat, Groups, Security, System)
|
||||||
|
5. **Auto-server-launch** — keep zero-config DX from v1
|
||||||
|
6. **Playbook system** — keep YAML-based test scripting
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `qpq --username alice --password pass` starts REPL (same UX as v1)
|
||||||
|
- TUI mode works with live event updates
|
||||||
|
- Tab-completion for commands and usernames
|
||||||
|
- E2E test: two clients exchange messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Desktop & Mobile
|
||||||
|
**Goal:** Tauri 2 app for all platforms.
|
||||||
|
|
||||||
|
1. **Tauri 2 project** in `apps/gui/`
|
||||||
|
2. **Rust backend** — Tauri commands wrapping `QpqClient`
|
||||||
|
3. **Web frontend** — Svelte or vanilla HTML/JS
|
||||||
|
4. **Desktop** — Linux, macOS, Windows
|
||||||
|
5. **Mobile** — iOS, Android via Tauri 2 mobile
|
||||||
|
6. **QUIC connection migration** — automatic wifi↔cellular handoff
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Desktop app builds and runs on Linux
|
||||||
|
- Mobile app builds for Android (emulator)
|
||||||
|
- Send message from CLI → received in GUI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Polish & Ecosystem
|
||||||
|
**Goal:** Production readiness.
|
||||||
|
|
||||||
|
1. **Federation improvements** — DNS SRV discovery, persistent relay queue with retry
|
||||||
|
2. **Plugin system v2** — version field, config passthrough, async hooks, WASM plugins
|
||||||
|
3. **WebTransport** — browser clients over HTTP/3 (same quinn endpoint)
|
||||||
|
4. **WASM MLS** — compile openmls to wasm32 for browser E2E encryption
|
||||||
|
5. **CI/CD** — release automation, WASM CI, multi-platform (Linux + macOS)
|
||||||
|
6. **Security hardening:**
|
||||||
|
- Fuzz testing (hybrid KEM, sealed sender, padding, protobuf deser)
|
||||||
|
- Remove all `InsecureServerCertVerifier` paths
|
||||||
|
- Certificate pinning
|
||||||
|
- Add passkey/WebAuthn as alternative auth
|
||||||
|
7. **Double Ratchet for 1:1 DMs** — better per-message forward secrecy than MLS for 2-party
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RPC Method Inventory (33 total)
|
||||||
|
|
||||||
|
| Category | Methods | Proto File |
|
||||||
|
|----------|---------|-----------|
|
||||||
|
| Auth (OPAQUE) | opaqueRegisterStart, opaqueRegisterFinish, opaqueLoginStart, opaqueLoginFinish | auth.proto |
|
||||||
|
| Delivery | enqueue, fetch, fetchWait, peek, ack, batchEnqueue | delivery.proto |
|
||||||
|
| Keys | uploadKeyPackage, fetchKeyPackage, uploadHybridKey, fetchHybridKey, fetchHybridKeys | keys.proto |
|
||||||
|
| Channel | createChannel | channel.proto |
|
||||||
|
| User | resolveUser, resolveIdentity | user.proto |
|
||||||
|
| Blob | uploadBlob, downloadBlob | blob.proto |
|
||||||
|
| Device | registerDevice, listDevices, revokeDevice | device.proto |
|
||||||
|
| P2P | health, publishEndpoint, resolveEndpoint | p2p.proto |
|
||||||
|
| Federation | relayEnqueue, relayBatchEnqueue, proxyFetchKeyPackage, proxyFetchHybridKey, proxyResolveUser, federationHealth | federation.proto |
|
||||||
|
|
||||||
|
**New in v2:**
|
||||||
|
| Push Events | Description | Proto File |
|
||||||
|
|-------------|-------------|-----------|
|
||||||
|
| MessageNotification | New message available | push.proto |
|
||||||
|
| TypingNotification | Peer is typing | push.proto |
|
||||||
|
| ChannelUpdate | Channel created/member changed | push.proto |
|
||||||
|
| SessionExpired | Auth session expired | push.proto |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Engineering Standards (carried from v1)
|
||||||
|
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`
|
||||||
|
- GPG-signed commits only
|
||||||
|
- No `Co-authored-by` trailers
|
||||||
|
- No `.unwrap()` on crypto or I/O in non-test paths
|
||||||
|
- Secrets: zeroize on drop, never in logs
|
||||||
|
- No stubs / `todo!()` / `unimplemented!()` in production code
|
||||||
|
- `clippy::unwrap_used = "deny"` at workspace level
|
||||||
14
examples/plugins/logging_plugin/Cargo.lock
generated
Normal file
14
examples/plugins/logging_plugin/Cargo.lock
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "logging_plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"quicproquo-plugin-api",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quicproquo-plugin-api"
|
||||||
|
version = "0.1.0"
|
||||||
14
examples/plugins/rate_limit_plugin/Cargo.lock
generated
Normal file
14
examples/plugins/rate_limit_plugin/Cargo.lock
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quicproquo-plugin-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rate_limit_plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"quicproquo-plugin-api",
|
||||||
|
]
|
||||||
212
scripts/render_terminal.py
Executable file
212
scripts/render_terminal.py
Executable file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render two terminal pane captures (with ANSI escapes) into a single PNG.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/render_terminal.py left.ansi right.ansi -o assets/screenshot.png
|
||||||
|
python3 scripts/render_terminal.py left.ansi right.ansi --labels "alice" "bob" -o out.png
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# ── Theme (dark terminal) ────────────────────────────────────────────────────
|
||||||
|
BG = (30, 30, 46) # base (catppuccin mocha-ish)
|
||||||
|
FG = (205, 214, 244) # default text
|
||||||
|
DIM = (108, 112, 134) # dim/grey
|
||||||
|
GREEN = (166, 227, 161)
|
||||||
|
CYAN = (137, 220, 235)
|
||||||
|
YELLOW = (249, 226, 175)
|
||||||
|
RED = (243, 139, 168)
|
||||||
|
BLUE = (137, 180, 250)
|
||||||
|
MAGENTA = (203, 166, 247)
|
||||||
|
BOLD_WHITE = (255, 255, 255)
|
||||||
|
BORDER = (69, 71, 90)
|
||||||
|
TITLE_BG = (49, 50, 68)
|
||||||
|
|
||||||
|
ANSI_COLORS = {
|
||||||
|
30: (30, 30, 46), 31: RED, 32: GREEN, 33: YELLOW,
|
||||||
|
34: BLUE, 35: MAGENTA, 36: CYAN, 37: FG,
|
||||||
|
90: DIM, 91: RED, 92: GREEN, 93: YELLOW,
|
||||||
|
94: BLUE, 95: MAGENTA, 96: CYAN, 97: BOLD_WHITE,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ANSI parsing ─────────────────────────────────────────────────────────────
|
||||||
|
ESC_RE = re.compile(r'\x1b\[([0-9;]*)m')
|
||||||
|
|
||||||
|
def parse_ansi_line(line):
|
||||||
|
"""Yield (text, fg_color, bold) spans from an ANSI-escaped line."""
|
||||||
|
fg = FG
|
||||||
|
bold = False
|
||||||
|
dim = False
|
||||||
|
pos = 0
|
||||||
|
for m in ESC_RE.finditer(line):
|
||||||
|
if m.start() > pos:
|
||||||
|
color = fg
|
||||||
|
if dim and color == FG:
|
||||||
|
color = DIM
|
||||||
|
yield (line[pos:m.start()], color, bold)
|
||||||
|
codes = m.group(1).split(';') if m.group(1) else ['0']
|
||||||
|
for code_s in codes:
|
||||||
|
code = int(code_s) if code_s else 0
|
||||||
|
if code == 0:
|
||||||
|
fg, bold, dim = FG, False, False
|
||||||
|
elif code == 1:
|
||||||
|
bold = True
|
||||||
|
elif code == 2:
|
||||||
|
dim = True
|
||||||
|
elif code in ANSI_COLORS:
|
||||||
|
fg = ANSI_COLORS[code]
|
||||||
|
pos = m.end()
|
||||||
|
tail = line[pos:]
|
||||||
|
if tail:
|
||||||
|
color = fg
|
||||||
|
if dim and color == FG:
|
||||||
|
color = DIM
|
||||||
|
yield (tail, color, bold)
|
||||||
|
|
||||||
|
|
||||||
|
def load_font(size):
|
||||||
|
"""Try to load a monospace font."""
|
||||||
|
candidates = [
|
||||||
|
"/usr/share/fonts/google-noto/NotoSansMono-Regular.ttf",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||||
|
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf",
|
||||||
|
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
||||||
|
"/usr/share/fonts/liberation-mono/LiberationMono-Regular.ttf",
|
||||||
|
"/usr/share/fonts/google-droid-sans-mono-fonts/DroidSansMono.ttf",
|
||||||
|
]
|
||||||
|
for path in candidates:
|
||||||
|
if Path(path).exists():
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
# fallback
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def load_bold_font(size):
|
||||||
|
candidates = [
|
||||||
|
"/usr/share/fonts/google-noto/NotoSansMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/DejaVuSansMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/liberation-mono/LiberationMono-Bold.ttf",
|
||||||
|
]
|
||||||
|
for path in candidates:
|
||||||
|
if Path(path).exists():
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
return load_font(size)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_ansi(s):
|
||||||
|
return ESC_RE.sub('', s)
|
||||||
|
|
||||||
|
|
||||||
|
def render_pane(lines, width_chars, font, bold_font, font_size, line_height):
|
||||||
|
"""Render terminal lines to an Image."""
|
||||||
|
char_w = font.getbbox("M")[2]
|
||||||
|
img_w = char_w * width_chars + 24 # 12px padding each side
|
||||||
|
img_h = line_height * len(lines) + 16 # 8px padding top+bottom
|
||||||
|
|
||||||
|
img = Image.new("RGB", (img_w, img_h), BG)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
y = 8
|
||||||
|
for line in lines:
|
||||||
|
x = 12
|
||||||
|
for text, color, bold in parse_ansi_line(line):
|
||||||
|
f = bold_font if bold else font
|
||||||
|
draw.text((x, y), text, fill=color, font=f)
|
||||||
|
x += f.getlength(text)
|
||||||
|
y += line_height
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Render terminal panes to PNG")
|
||||||
|
ap.add_argument("left", help="Left pane ANSI capture file")
|
||||||
|
ap.add_argument("right", help="Right pane ANSI capture file")
|
||||||
|
ap.add_argument("-o", "--output", default="assets/screenshot.png")
|
||||||
|
ap.add_argument("--labels", nargs=2, default=["alice", "bob"],
|
||||||
|
help="Labels for the two panes")
|
||||||
|
ap.add_argument("--font-size", type=int, default=14)
|
||||||
|
ap.add_argument("--width", type=int, default=58,
|
||||||
|
help="Width of each pane in characters")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
font_size = args.font_size
|
||||||
|
line_height = int(font_size * 1.5)
|
||||||
|
font = load_font(font_size)
|
||||||
|
bold_font = load_bold_font(font_size)
|
||||||
|
char_w = font.getbbox("M")[2]
|
||||||
|
pane_w = char_w * args.width + 24
|
||||||
|
|
||||||
|
left_lines = Path(args.left).read_text().splitlines()
|
||||||
|
right_lines = Path(args.right).read_text().splitlines()
|
||||||
|
|
||||||
|
# Render each pane
|
||||||
|
left_img = render_pane(left_lines, args.width, font, bold_font,
|
||||||
|
font_size, line_height)
|
||||||
|
right_img = render_pane(right_lines, args.width, font, bold_font,
|
||||||
|
font_size, line_height)
|
||||||
|
|
||||||
|
# Composite: title bar + two panes side by side
|
||||||
|
title_h = 32
|
||||||
|
gap = 2
|
||||||
|
max_h = max(left_img.height, right_img.height)
|
||||||
|
total_w = left_img.width + gap + right_img.width
|
||||||
|
total_h = title_h + max_h
|
||||||
|
|
||||||
|
# Window chrome
|
||||||
|
canvas = Image.new("RGB", (total_w, total_h), BORDER)
|
||||||
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
|
# Title bar
|
||||||
|
draw.rectangle([(0, 0), (total_w, title_h - 1)], fill=TITLE_BG)
|
||||||
|
|
||||||
|
# Traffic lights
|
||||||
|
for i, color in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
|
||||||
|
cx = 16 + i * 22
|
||||||
|
cy = title_h // 2
|
||||||
|
draw.ellipse([(cx - 6, cy - 6), (cx + 6, cy + 6)], fill=color)
|
||||||
|
|
||||||
|
# Pane labels
|
||||||
|
label_font = load_font(font_size - 1)
|
||||||
|
left_label = args.labels[0]
|
||||||
|
right_label = args.labels[1]
|
||||||
|
left_label_w = label_font.getlength(left_label)
|
||||||
|
right_label_w = label_font.getlength(right_label)
|
||||||
|
draw.text((left_img.width // 2 - left_label_w // 2, 7),
|
||||||
|
left_label, fill=DIM, font=label_font)
|
||||||
|
draw.text((left_img.width + gap + right_img.width // 2 - right_label_w // 2, 7),
|
||||||
|
right_label, fill=DIM, font=label_font)
|
||||||
|
|
||||||
|
# Paste panes
|
||||||
|
canvas.paste(left_img, (0, title_h))
|
||||||
|
canvas.paste(right_img, (left_img.width + gap, title_h))
|
||||||
|
|
||||||
|
# Round corners (simple mask)
|
||||||
|
radius = 10
|
||||||
|
mask = Image.new("L", canvas.size, 255)
|
||||||
|
mask_draw = ImageDraw.Draw(mask)
|
||||||
|
mask_draw.rectangle([(0, 0), (radius, radius)], fill=0)
|
||||||
|
mask_draw.pieslice([(0, 0), (radius * 2, radius * 2)], 180, 270, fill=255)
|
||||||
|
mask_draw.rectangle([(total_w - radius, 0), (total_w, radius)], fill=0)
|
||||||
|
mask_draw.pieslice([(total_w - radius * 2, 0), (total_w, radius * 2)], 270, 360, fill=255)
|
||||||
|
|
||||||
|
# Apply rounded corners with transparent background
|
||||||
|
final = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
||||||
|
final.paste(canvas, mask=mask)
|
||||||
|
|
||||||
|
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
final.save(args.output)
|
||||||
|
print(f"Saved {args.output} ({final.width}x{final.height})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
92
scripts/screenshot.sh
Executable file
92
scripts/screenshot.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/screenshot.sh — generate a README screenshot automatically
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
INTERACTIVE=false
|
||||||
|
[[ "${1:-}" == "--interactive" ]] && INTERACTIVE=true
|
||||||
|
|
||||||
|
SESSION="qpq-screenshot"
|
||||||
|
SERVER_PORT=17123
|
||||||
|
SERVER_ADDR="127.0.0.1:${SERVER_PORT}"
|
||||||
|
DATA_DIR=$(mktemp -d)
|
||||||
|
CERT="${DATA_DIR}/server-cert.der"
|
||||||
|
KEY="${DATA_DIR}/server-key.der"
|
||||||
|
QPQ="./target/debug/qpq"
|
||||||
|
SERVER="./target/debug/qpq-server"
|
||||||
|
SLOG="${DATA_DIR}/server.log"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[[ -n "${SERVER_PID:-}" ]] && kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
||||||
|
# Keep DATA_DIR for debugging
|
||||||
|
echo "Data dir: $DATA_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ── Build ────────────────────────────────────────────────────────────────────
|
||||||
|
echo "Building binaries..."
|
||||||
|
cargo build --bin qpq --bin qpq-server 2>&1 | tail -1
|
||||||
|
|
||||||
|
# ── Start server ─────────────────────────────────────────────────────────────
|
||||||
|
echo "Starting server on ${SERVER_ADDR}..."
|
||||||
|
RUST_LOG=debug "$SERVER" \
|
||||||
|
--allow-insecure-auth \
|
||||||
|
--listen "$SERVER_ADDR" \
|
||||||
|
--tls-cert "$CERT" \
|
||||||
|
--tls-key "$KEY" \
|
||||||
|
--data-dir "$DATA_DIR" \
|
||||||
|
&>"$SLOG" &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 30); do [[ -f "$CERT" ]] && break; sleep 0.2; done
|
||||||
|
if [[ ! -f "$CERT" ]]; then echo "ERROR: server did not start"; cat "$SLOG"; exit 1; fi
|
||||||
|
echo "Server ready (PID ${SERVER_PID})"
|
||||||
|
|
||||||
|
# ── tmux session ─────────────────────────────────────────────────────────────
|
||||||
|
tmux new-session -d -s "$SESSION" -x 114 -y 28
|
||||||
|
|
||||||
|
send_alice() { tmux send-keys -t "${SESSION}:0.0" "$1" Enter; }
|
||||||
|
send_bob() { tmux send-keys -t "${SESSION}:0.1" "$1" Enter; }
|
||||||
|
|
||||||
|
# Start Alice (left pane)
|
||||||
|
tmux send-keys -t "$SESSION" \
|
||||||
|
"RUST_LOG=debug $QPQ repl --username alice --password demopass1 --server $SERVER_ADDR --ca-cert $CERT --state ${DATA_DIR}/alice.bin 2>${DATA_DIR}/alice-debug.log" Enter
|
||||||
|
|
||||||
|
# Start Bob (right pane)
|
||||||
|
tmux split-window -h -t "$SESSION"
|
||||||
|
tmux send-keys -t "$SESSION" \
|
||||||
|
"RUST_LOG=debug $QPQ repl --username bob --password demopass2 --server $SERVER_ADDR --ca-cert $CERT --state ${DATA_DIR}/bob.bin 2>${DATA_DIR}/bob-debug.log" Enter
|
||||||
|
|
||||||
|
tmux select-layout -t "$SESSION" even-horizontal
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Alice creates DM with Bob
|
||||||
|
send_alice "/dm bob"
|
||||||
|
sleep 4
|
||||||
|
|
||||||
|
# Wait for Bob's poller (8 seconds = 8 poll cycles)
|
||||||
|
echo "Waiting for Bob to poll Welcome..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check Bob's list
|
||||||
|
send_bob "/list"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Capture both panes
|
||||||
|
{
|
||||||
|
echo "=== Alice ==="
|
||||||
|
tmux capture-pane -t "${SESSION}:0.0" -p
|
||||||
|
echo ""
|
||||||
|
echo "=== Bob ==="
|
||||||
|
tmux capture-pane -t "${SESSION}:0.1" -p
|
||||||
|
} > "${DATA_DIR}/capture.txt"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
cat "${DATA_DIR}/capture.txt"
|
||||||
|
echo ""
|
||||||
|
echo "Server log tail:"
|
||||||
|
tail -30 "$SLOG" 2>/dev/null || true
|
||||||
|
echo ""
|
||||||
|
echo "Debug logs in: $DATA_DIR"
|
||||||
|
echo "Check: $DATA_DIR/alice-debug.log and $DATA_DIR/bob-debug.log"
|
||||||
Reference in New Issue
Block a user