//! Account recovery — setup, upload, and restore via recovery codes. //! //! Wraps `quicproquo_core::recovery` and the v2 RPC recovery service. use bytes::Bytes; use prost::Message; use quicproquo_core::recovery::{ generate_recovery_codes, recover_from_bundle, recovery_token_hash, RecoveryBundle, }; use quicproquo_proto::{method_ids, qpq::v1}; use quicproquo_rpc::client::RpcClient; use crate::error::SdkError; /// Set up account recovery: generate codes, encrypt bundles, upload to server. /// /// Returns the recovery codes (display to user once, never store). pub async fn setup_recovery( rpc: &RpcClient, identity_seed: &[u8; 32], conversation_ids: &[Vec], ) -> Result, SdkError> { let setup = generate_recovery_codes(identity_seed, conversation_ids) .map_err(|e| SdkError::Crypto(format!("recovery code generation: {e}")))?; // Upload each encrypted bundle to the server. for bundle in &setup.bundles { let bundle_bytes = bincode::serialize(bundle) .map_err(|e| SdkError::Crypto(format!("serialize recovery bundle: {e}")))?; let req = v1::StoreRecoveryBundleRequest { token_hash: bundle.token_hash.clone(), bundle: bundle_bytes, ttl_secs: 0, // Use server default (90 days). }; let resp_bytes = rpc .call( method_ids::STORE_RECOVERY_BUNDLE, Bytes::from(req.encode_to_vec()), ) .await?; let resp = v1::StoreRecoveryBundleResponse::decode(resp_bytes) .map_err(|e| SdkError::Crypto(format!("decode store_recovery response: {e}")))?; if !resp.success { return Err(SdkError::Crypto( "server rejected recovery bundle upload".into(), )); } } Ok(setup.codes) } /// Recover an account from a recovery code. /// /// Fetches the encrypted bundle from the server, decrypts it with the code, /// and returns the identity seed and conversation IDs. pub async fn recover_account( rpc: &RpcClient, code: &str, ) -> Result<(/* identity_seed */ [u8; 32], /* conversation_ids */ Vec>), SdkError> { // Compute the token hash for server-side lookup. let token_hash = recovery_token_hash(code); let req = v1::FetchRecoveryBundleRequest { token_hash: token_hash.clone(), }; let resp_bytes = rpc .call( method_ids::FETCH_RECOVERY_BUNDLE, Bytes::from(req.encode_to_vec()), ) .await?; let resp = v1::FetchRecoveryBundleResponse::decode(resp_bytes) .map_err(|e| SdkError::Crypto(format!("decode fetch_recovery response: {e}")))?; if resp.bundle.is_empty() { return Err(SdkError::Crypto( "no recovery bundle found for this code".into(), )); } // Deserialize the bundle. let bundle: RecoveryBundle = bincode::deserialize(&resp.bundle) .map_err(|e| SdkError::Crypto(format!("deserialize recovery bundle: {e}")))?; // Decrypt with the code. let payload = recover_from_bundle(code, &bundle) .map_err(|e| SdkError::Crypto(format!("recovery decryption failed: {e}")))?; Ok((payload.identity_seed, payload.conversation_ids)) } /// Delete all recovery bundles for the given codes (e.g. after refresh). pub async fn delete_recovery_bundles( rpc: &RpcClient, codes: &[String], ) -> Result<(), SdkError> { for code in codes { let token_hash = recovery_token_hash(code); let req = v1::DeleteRecoveryBundleRequest { token_hash }; let resp_bytes = rpc .call( method_ids::DELETE_RECOVERY_BUNDLE, Bytes::from(req.encode_to_vec()), ) .await?; let _resp = v1::DeleteRecoveryBundleResponse::decode(resp_bytes) .map_err(|e| SdkError::Crypto(format!("decode delete_recovery response: {e}")))?; } Ok(()) }