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)
This commit is contained in:
2026-03-04 12:37:01 +01:00
parent 6b757f8d65
commit 918da0c23d
4 changed files with 235 additions and 0 deletions

View File

@@ -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<u8>,
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<bool, SdkError> {
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<Vec<DeviceInfo>, 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<bool, SdkError> {
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)
}

View File

@@ -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<Vec<u8>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Vec<u8>>` per requested identity, in the same order.
pub async fn fetch_hybrid_keys(
rpc: &RpcClient,
identity_keys: &[Vec<u8>],
) -> Result<Vec<Option<Vec<u8>>>, 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)
}

View File

@@ -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;

View File

@@ -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<Option<Vec<u8>>, 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<Option<String>, 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))
}
}