//! 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 { 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::::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::::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::::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, SdkError> { let mut rng = rand::rngs::OsRng; // ── Step 1: Login Start ──────────────────────────────────────────────── let login_start = ClientLogin::::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::::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::::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) }