chore: rename project quicnprotochat -> quicproquo (binaries: qpq)

Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
  quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
  *.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated

HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:11:51 +01:00
parent 553de3a2b7
commit 853ca4fec0
152 changed files with 4070 additions and 788 deletions

View File

@@ -0,0 +1,373 @@
use capnp::capability::Promise;
use opaque_ke::{
CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload,
ServerLogin, ServerRegistration,
};
use quicproquo_core::opaque_auth::OpaqueSuite;
use quicproquo_proto::node_capnp::node_service;
use crate::auth::{coded_error, current_timestamp, PendingLogin, SESSION_TTL_SECS};
use crate::error_codes::*;
use crate::metrics;
use crate::storage::StorageError;
use super::NodeServiceImpl;
// Audit events in this module must never include secrets (no session tokens, passwords, or raw keys).
fn storage_err(err: StorageError) -> capnp::Error {
coded_error(E009_STORAGE_ERROR, err)
}
/// Parse username from Cap'n Proto reader; requires valid UTF-8.
fn parse_username_param(
result: Result<capnp::text::Reader<'_>, capnp::Error>,
) -> Result<String, capnp::Error> {
let reader = result.map_err(|e| coded_error(E020_BAD_PARAMS, e))?;
reader
.to_string()
.map_err(|_| coded_error(E020_BAD_PARAMS, "username must be valid UTF-8"))
}
impl NodeServiceImpl {
pub fn handle_opaque_login_start(
&mut self,
params: node_service::OpaqueLoginStartParams,
mut results: node_service::OpaqueLoginStartResults,
) -> Promise<(), capnp::Error> {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
let username = match parse_username_param(p.get_username()) {
Ok(s) => s,
Err(e) => return Promise::err(e),
};
let request_bytes = match p.get_request() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
if username.is_empty() {
return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty"));
}
// Check for existing recent pending login before expensive OPAQUE/storage work (DoS mitigation).
if let Some(existing) = self.pending_logins.get(&username) {
let age = current_timestamp().saturating_sub(existing.created_at);
if age < 60 {
return Promise::err(coded_error(E010_OPAQUE_ERROR, "login already in progress"));
}
}
let credential_request = match CredentialRequest::<OpaqueSuite>::deserialize(&request_bytes) {
Ok(r) => r,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("invalid credential request: {e}"),
))
}
};
let password_file = match self.store.get_user_record(&username) {
Ok(Some(bytes)) => match ServerRegistration::<OpaqueSuite>::deserialize(&bytes) {
Ok(pf) => Some(pf),
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("corrupt user record: {e}"),
))
}
},
Ok(None) => None,
Err(e) => return Promise::err(storage_err(e)),
};
let mut rng = rand::rngs::OsRng;
let result = match ServerLogin::<OpaqueSuite>::start(
&mut rng,
&self.opaque_setup,
password_file,
credential_request,
username.as_bytes(),
Default::default(),
) {
Ok(r) => r,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("OPAQUE login start failed: {e}"),
))
}
};
let state_bytes = result.state.serialize().to_vec();
self.pending_logins.insert(
username.clone(),
PendingLogin {
state_bytes,
created_at: current_timestamp(),
},
);
let response_bytes = result.message.serialize();
results.get().set_response(&response_bytes);
tracing::info!(user = %username, "OPAQUE login started");
Promise::ok(())
}
pub fn handle_opaque_register_start(
&mut self,
params: node_service::OpaqueRegisterStartParams,
mut results: node_service::OpaqueRegisterStartResults,
) -> Promise<(), capnp::Error> {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
let username = match parse_username_param(p.get_username()) {
Ok(s) => s,
Err(e) => return Promise::err(e),
};
let request_bytes = match p.get_request() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
if username.is_empty() {
return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty"));
}
if let Ok(true) = self.store.has_user_record(&username) {
return Promise::err(coded_error(
E018_USER_EXISTS,
format!("user '{}' already registered", username),
));
}
let registration_request = match RegistrationRequest::<OpaqueSuite>::deserialize(&request_bytes) {
Ok(r) => r,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("invalid registration request: {e}"),
))
}
};
let result = match ServerRegistration::<OpaqueSuite>::start(
&self.opaque_setup,
registration_request,
username.as_bytes(),
) {
Ok(r) => r,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("OPAQUE registration start failed: {e}"),
))
}
};
let response_bytes = result.message.serialize();
results.get().set_response(&response_bytes);
tracing::info!(user = %username, "OPAQUE registration started");
Promise::ok(())
}
pub fn handle_opaque_login_finish(
&mut self,
params: node_service::OpaqueLoginFinishParams,
mut results: node_service::OpaqueLoginFinishResults,
) -> Promise<(), capnp::Error> {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
let username = match parse_username_param(p.get_username()) {
Ok(s) => s,
Err(e) => return Promise::err(e),
};
let finalization_bytes = match p.get_finalization() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
let identity_key = p.get_identity_key().unwrap_or_default().to_vec();
if username.is_empty() {
return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty"));
}
let pending = match self.pending_logins.remove(&username) {
Some((_, pl)) => pl,
None => {
// Audit: login failure — do not log secrets (no token, no password).
tracing::warn!(user = %username, "audit: auth login failure (no pending login)");
metrics::record_auth_login_failure_total();
return Promise::err(coded_error(E019_NO_PENDING_LOGIN, "no pending login for this username"))
}
};
let server_login = match ServerLogin::<OpaqueSuite>::deserialize(&pending.state_bytes) {
Ok(s) => s,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("corrupt login state: {e}"),
))
}
};
let finalization = match CredentialFinalization::<OpaqueSuite>::deserialize(&finalization_bytes) {
Ok(f) => f,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("invalid credential finalization: {e}"),
))
}
};
let _result = match server_login.finish(finalization, Default::default()) {
Ok(r) => r,
Err(e) => {
tracing::warn!(user = %username, "audit: auth login failure (OPAQUE finish failed)");
metrics::record_auth_login_failure_total();
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("OPAQUE login finish failed (bad password?): {e}"),
))
}
};
if identity_key.is_empty() {
metrics::record_auth_login_failure_total();
return Promise::err(coded_error(
E016_IDENTITY_MISMATCH,
"identity key required to bind session token",
));
}
if let Ok(Some(stored_ik)) = self.store.get_user_identity_key(&username) {
if stored_ik != identity_key {
tracing::warn!(user = %username, "audit: auth login failure (identity mismatch)");
metrics::record_auth_login_failure_total();
return Promise::err(coded_error(
E016_IDENTITY_MISMATCH,
"identity key does not match registered key",
));
}
}
let mut token = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token);
let token_vec = token.to_vec();
let now = current_timestamp();
self.sessions.insert(
token_vec.clone(),
crate::auth::SessionInfo {
username: username.clone(),
identity_key,
created_at: now,
expires_at: now + SESSION_TTL_SECS,
},
);
results.get().set_session_token(&token_vec);
// Audit: login success — do not log session token or any secrets.
metrics::record_auth_login_success_total();
tracing::info!(user = %username, "audit: auth login success — session token issued");
Promise::ok(())
}
pub fn handle_opaque_register_finish(
&mut self,
params: node_service::OpaqueRegisterFinishParams,
mut results: node_service::OpaqueRegisterFinishResults,
) -> Promise<(), capnp::Error> {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
let username = match parse_username_param(p.get_username()) {
Ok(s) => s,
Err(e) => return Promise::err(e),
};
let upload_bytes = match p.get_upload() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
};
let identity_key = p.get_identity_key().unwrap_or_default().to_vec();
if username.is_empty() {
return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty"));
}
let _request = match RegistrationRequest::<OpaqueSuite>::deserialize(&upload_bytes) {
Ok(r) => r,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("invalid registration upload: {e}"),
))
}
};
match self.store.has_user_record(&username) {
Ok(true) => {
return Promise::err(coded_error(
E018_USER_EXISTS,
format!("user '{}' already registered", username),
))
}
Err(e) => return Promise::err(storage_err(e)),
_ => {}
}
let upload = match RegistrationUpload::<OpaqueSuite>::deserialize(&upload_bytes) {
Ok(u) => u,
Err(e) => {
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("invalid registration upload: {e}"),
))
}
};
let password_file = ServerRegistration::<OpaqueSuite>::finish(upload);
let record_bytes = password_file.serialize().to_vec();
match self
.store
.store_user_record(&username, record_bytes)
{
Ok(()) => {}
Err(crate::storage::StorageError::DuplicateUser(_)) => {
return Promise::err(coded_error(
E018_USER_EXISTS,
format!("user '{}' already registered", username),
))
}
Err(e) => return Promise::err(storage_err(e)),
}
if !identity_key.is_empty() {
if let Err(e) = self
.store
.store_user_identity_key(&username, identity_key)
.map_err(storage_err)
{
return Promise::err(e);
}
}
results.get().set_success(true);
tracing::info!(user = %username, "OPAQUE registration complete");
Promise::ok(())
}
}