From 918da0c23d748dba14a38265bfd94ea1b350c298 Mon Sep 17 00:00:00 2001 From: Christian Nennemann Date: Wed, 4 Mar 2026 12:37:01 +0100 Subject: [PATCH] feat(sdk): key management, user resolution, device management Add three new SDK modules wrapping v2 protobuf RPC calls: - keys.rs: upload/fetch KeyPackages and hybrid public keys (5 methods) - users.rs: resolve username <-> identity key (2 methods) - devices.rs: register/list/revoke devices (3 methods) --- crates/quicproquo-sdk/src/devices.rs | 73 ++++++++++++++++++ crates/quicproquo-sdk/src/keys.rs | 109 +++++++++++++++++++++++++++ crates/quicproquo-sdk/src/lib.rs | 3 + crates/quicproquo-sdk/src/users.rs | 50 ++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 crates/quicproquo-sdk/src/devices.rs create mode 100644 crates/quicproquo-sdk/src/keys.rs create mode 100644 crates/quicproquo-sdk/src/users.rs diff --git a/crates/quicproquo-sdk/src/devices.rs b/crates/quicproquo-sdk/src/devices.rs new file mode 100644 index 0000000..a483254 --- /dev/null +++ b/crates/quicproquo-sdk/src/devices.rs @@ -0,0 +1,73 @@ +//! Device management — register, list, and revoke devices. + +use quicproquo_proto::bytes::Bytes; +use quicproquo_proto::prost::Message; +use quicproquo_proto::{method_ids, qpq::v1}; +use quicproquo_rpc::client::RpcClient; + +use crate::error::SdkError; + +/// Info about a registered device. +pub struct DeviceInfo { + pub device_id: Vec, + pub device_name: String, + pub registered_at: u64, +} + +/// Register a device for multi-device support. +/// Returns `true` if the device was newly registered, `false` if it already existed. +pub async fn register_device( + rpc: &RpcClient, + device_id: &[u8], + device_name: &str, +) -> Result { + let req = v1::RegisterDeviceRequest { + device_id: device_id.to_vec(), + device_name: device_name.to_string(), + }; + let resp_bytes = rpc + .call(method_ids::REGISTER_DEVICE, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::RegisterDeviceResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode RegisterDeviceResponse: {e}")))?; + Ok(resp.success) +} + +/// List all registered devices for the authenticated identity. +pub async fn list_devices( + rpc: &RpcClient, +) -> Result, SdkError> { + let req = v1::ListDevicesRequest {}; + let resp_bytes = rpc + .call(method_ids::LIST_DEVICES, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::ListDevicesResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode ListDevicesResponse: {e}")))?; + let devices = resp + .devices + .into_iter() + .map(|d| DeviceInfo { + device_id: d.device_id, + device_name: d.device_name, + registered_at: d.registered_at, + }) + .collect(); + Ok(devices) +} + +/// Revoke (remove) a registered device. +/// Returns `true` if the device was found and revoked. +pub async fn revoke_device( + rpc: &RpcClient, + device_id: &[u8], +) -> Result { + let req = v1::RevokeDeviceRequest { + device_id: device_id.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::REVOKE_DEVICE, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::RevokeDeviceResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode RevokeDeviceResponse: {e}")))?; + Ok(resp.success) +} diff --git a/crates/quicproquo-sdk/src/keys.rs b/crates/quicproquo-sdk/src/keys.rs new file mode 100644 index 0000000..d33be90 --- /dev/null +++ b/crates/quicproquo-sdk/src/keys.rs @@ -0,0 +1,109 @@ +//! Key management — upload/fetch KeyPackages and hybrid public keys. + +use quicproquo_proto::bytes::Bytes; +use quicproquo_proto::prost::Message; +use quicproquo_proto::{method_ids, qpq::v1}; +use quicproquo_rpc::client::RpcClient; + +use crate::error::SdkError; + +/// Upload a KeyPackage for pre-key distribution. +/// Returns the SHA-256 fingerprint echoed by the server. +pub async fn upload_key_package( + rpc: &RpcClient, + identity_key: &[u8], + package: &[u8], +) -> Result, SdkError> { + let req = v1::UploadKeyPackageRequest { + identity_key: identity_key.to_vec(), + package: package.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::UPLOAD_KEY_PACKAGE, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::UploadKeyPackageResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode UploadKeyPackageResponse: {e}")))?; + Ok(resp.fingerprint) +} + +/// Fetch a KeyPackage for a peer (consumed: single-use). +/// Returns `None` if the peer has no available key packages. +pub async fn fetch_key_package( + rpc: &RpcClient, + identity_key: &[u8], +) -> Result>, SdkError> { + let req = v1::FetchKeyPackageRequest { + identity_key: identity_key.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::FETCH_KEY_PACKAGE, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::FetchKeyPackageResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode FetchKeyPackageResponse: {e}")))?; + if resp.package.is_empty() { + Ok(None) + } else { + Ok(Some(resp.package)) + } +} + +/// Upload hybrid public key (X25519 + ML-KEM-768). +pub async fn upload_hybrid_key( + rpc: &RpcClient, + identity_key: &[u8], + hybrid_public_key: &[u8], +) -> Result<(), SdkError> { + let req = v1::UploadHybridKeyRequest { + identity_key: identity_key.to_vec(), + hybrid_public_key: hybrid_public_key.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::UPLOAD_HYBRID_KEY, Bytes::from(req.encode_to_vec())) + .await?; + let _resp = v1::UploadHybridKeyResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode UploadHybridKeyResponse: {e}")))?; + Ok(()) +} + +/// Fetch a peer's hybrid public key. +/// Returns `None` if the peer has not uploaded a hybrid key. +pub async fn fetch_hybrid_key( + rpc: &RpcClient, + identity_key: &[u8], +) -> Result>, SdkError> { + let req = v1::FetchHybridKeyRequest { + identity_key: identity_key.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::FETCH_HYBRID_KEY, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::FetchHybridKeyResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode FetchHybridKeyResponse: {e}")))?; + if resp.hybrid_public_key.is_empty() { + Ok(None) + } else { + Ok(Some(resp.hybrid_public_key)) + } +} + +/// Batch fetch hybrid keys for multiple identities. +/// Returns one `Option>` per requested identity, in the same order. +pub async fn fetch_hybrid_keys( + rpc: &RpcClient, + identity_keys: &[Vec], +) -> Result>>, SdkError> { + let req = v1::FetchHybridKeysRequest { + identity_keys: identity_keys.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::FETCH_HYBRID_KEYS, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::FetchHybridKeysResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode FetchHybridKeysResponse: {e}")))?; + let result = resp + .keys + .into_iter() + .map(|k| if k.is_empty() { None } else { Some(k) }) + .collect(); + Ok(result) +} diff --git a/crates/quicproquo-sdk/src/lib.rs b/crates/quicproquo-sdk/src/lib.rs index 88105ee..87d36d6 100644 --- a/crates/quicproquo-sdk/src/lib.rs +++ b/crates/quicproquo-sdk/src/lib.rs @@ -6,5 +6,8 @@ pub mod client; pub mod config; pub mod conversation; +pub mod devices; pub mod events; pub mod error; +pub mod keys; +pub mod users; diff --git a/crates/quicproquo-sdk/src/users.rs b/crates/quicproquo-sdk/src/users.rs new file mode 100644 index 0000000..0318124 --- /dev/null +++ b/crates/quicproquo-sdk/src/users.rs @@ -0,0 +1,50 @@ +//! User resolution — username <-> identity key lookups. + +use quicproquo_proto::bytes::Bytes; +use quicproquo_proto::prost::Message; +use quicproquo_proto::{method_ids, qpq::v1}; +use quicproquo_rpc::client::RpcClient; + +use crate::error::SdkError; + +/// Resolve a username to its identity key. +/// Returns `None` if the username is not registered. +pub async fn resolve_user( + rpc: &RpcClient, + username: &str, +) -> Result>, SdkError> { + let req = v1::ResolveUserRequest { + username: username.to_string(), + }; + let resp_bytes = rpc + .call(method_ids::RESOLVE_USER, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::ResolveUserResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode ResolveUserResponse: {e}")))?; + if resp.identity_key.is_empty() { + Ok(None) + } else { + Ok(Some(resp.identity_key)) + } +} + +/// Reverse lookup: identity key to username. +/// Returns `None` if no username is associated with the key. +pub async fn resolve_identity( + rpc: &RpcClient, + identity_key: &[u8], +) -> Result, SdkError> { + let req = v1::ResolveIdentityRequest { + identity_key: identity_key.to_vec(), + }; + let resp_bytes = rpc + .call(method_ids::RESOLVE_IDENTITY, Bytes::from(req.encode_to_vec())) + .await?; + let resp = v1::ResolveIdentityResponse::decode(resp_bytes) + .map_err(|e| SdkError::Other(anyhow::anyhow!("decode ResolveIdentityResponse: {e}")))?; + if resp.username.is_empty() { + Ok(None) + } else { + Ok(Some(resp.username)) + } +}