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.
143 lines
5.7 KiB
Rust
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)
|
|
}
|