Files
quicproquo/crates/quicprochat-sdk/src/auth.rs
Christian Nennemann a710037dde chore: rename quicproquo → quicprochat in Rust workspace
Rename all crate directories, package names, binary names, proto
package/module paths, ALPN strings, env var prefixes, config filenames,
mDNS service names, and plugin ABI symbols from quicproquo/qpq to
quicprochat/qpc.
2026-03-21 19:14:06 +01:00

143 lines
5.7 KiB
Rust

//! OPAQUE authentication — register and login via the v2 RPC protocol.
//!
//! Wraps the `opaque-ke` crate to perform the OPAQUE 3-message flow against
//! the quicprochat server using prost-encoded protobuf messages over `RpcClient::call`.
use bytes::Bytes;
use opaque_ke::{
ClientLogin, ClientLoginFinishParameters, ClientRegistration,
ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse,
};
use prost::Message;
use quicprochat_core::{opaque_auth::OpaqueSuite, IdentityKeypair};
use quicprochat_proto::{method_ids, qpc::v1};
use quicprochat_rpc::client::RpcClient;
use crate::error::SdkError;
/// Register a new user account via the OPAQUE protocol.
///
/// If `identity` is `None`, a fresh Ed25519 keypair is generated.
/// Returns the identity keypair bound to this account.
pub async fn opaque_register(
rpc: &RpcClient,
username: &str,
password: &str,
identity: Option<&IdentityKeypair>,
) -> Result<IdentityKeypair, SdkError> {
let mut rng = rand::rngs::OsRng;
// Generate or use provided identity.
let keypair = match identity {
Some(kp) => IdentityKeypair::from_seed(*kp.seed_bytes()),
None => IdentityKeypair::generate(),
};
// ── Step 1: Registration Start ─────────────────────────────────────────
let reg_start = ClientRegistration::<OpaqueSuite>::start(&mut rng, password.as_bytes())
.map_err(|e| SdkError::AuthFailed(format!("OPAQUE register start: {e}")))?;
let start_req = v1::OpaqueRegisterStartRequest {
username: username.to_string(),
request: reg_start.message.serialize().to_vec(),
};
let resp_bytes = rpc
.call(method_ids::OPAQUE_REGISTER_START, Bytes::from(start_req.encode_to_vec()))
.await?;
let start_resp = v1::OpaqueRegisterStartResponse::decode(resp_bytes)
.map_err(|e| SdkError::AuthFailed(format!("decode register_start response: {e}")))?;
let reg_response = RegistrationResponse::<OpaqueSuite>::deserialize(&start_resp.response)
.map_err(|e| SdkError::AuthFailed(format!("invalid registration response: {e}")))?;
// ── Step 2: Registration Finish ────────────────────────────────────────
let reg_finish = reg_start
.state
.finish(
&mut rng,
password.as_bytes(),
reg_response,
ClientRegistrationFinishParameters::<OpaqueSuite>::default(),
)
.map_err(|e| SdkError::AuthFailed(format!("OPAQUE register finish: {e}")))?;
let finish_req = v1::OpaqueRegisterFinishRequest {
username: username.to_string(),
upload: reg_finish.message.serialize().to_vec(),
identity_key: keypair.public_key_bytes().to_vec(),
};
let resp_bytes = rpc
.call(method_ids::OPAQUE_REGISTER_FINISH, Bytes::from(finish_req.encode_to_vec()))
.await?;
let finish_resp = v1::OpaqueRegisterFinishResponse::decode(resp_bytes)
.map_err(|e| SdkError::AuthFailed(format!("decode register_finish response: {e}")))?;
if !finish_resp.success {
return Err(SdkError::AuthFailed("server rejected registration".into()));
}
Ok(keypair)
}
/// Log in via the OPAQUE protocol and receive a session token.
///
/// `identity_key` is the 32-byte Ed25519 public key to bind to this session.
/// Returns the session token bytes.
pub async fn opaque_login(
rpc: &RpcClient,
username: &str,
password: &str,
identity_key: &[u8],
) -> Result<Vec<u8>, SdkError> {
let mut rng = rand::rngs::OsRng;
// ── Step 1: Login Start ────────────────────────────────────────────────
let login_start = ClientLogin::<OpaqueSuite>::start(&mut rng, password.as_bytes())
.map_err(|e| SdkError::AuthFailed(format!("OPAQUE login start: {e}")))?;
let start_req = v1::OpaqueLoginStartRequest {
username: username.to_string(),
request: login_start.message.serialize().to_vec(),
};
let resp_bytes = rpc
.call(method_ids::OPAQUE_LOGIN_START, Bytes::from(start_req.encode_to_vec()))
.await?;
let start_resp = v1::OpaqueLoginStartResponse::decode(resp_bytes)
.map_err(|e| SdkError::AuthFailed(format!("decode login_start response: {e}")))?;
let credential_response = CredentialResponse::<OpaqueSuite>::deserialize(&start_resp.response)
.map_err(|e| SdkError::AuthFailed(format!("invalid credential response: {e}")))?;
// ── Step 2: Login Finish ───────────────────────────────────────────────
let login_finish = login_start
.state
.finish(
&mut rng,
password.as_bytes(),
credential_response,
ClientLoginFinishParameters::<OpaqueSuite>::default(),
)
.map_err(|e| SdkError::AuthFailed(format!("OPAQUE login finish (bad password?): {e}")))?;
let finish_req = v1::OpaqueLoginFinishRequest {
username: username.to_string(),
finalization: login_finish.message.serialize().to_vec(),
identity_key: identity_key.to_vec(),
};
let resp_bytes = rpc
.call(method_ids::OPAQUE_LOGIN_FINISH, Bytes::from(finish_req.encode_to_vec()))
.await?;
let finish_resp = v1::OpaqueLoginFinishResponse::decode(resp_bytes)
.map_err(|e| SdkError::AuthFailed(format!("decode login_finish response: {e}")))?;
if finish_resp.session_token.is_empty() {
return Err(SdkError::AuthFailed("server returned empty session token".into()));
}
Ok(finish_resp.session_token)
}