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:
73
crates/quicproquo-sdk/src/devices.rs
Normal file
73
crates/quicproquo-sdk/src/devices.rs
Normal 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)
|
||||
}
|
||||
109
crates/quicproquo-sdk/src/keys.rs
Normal file
109
crates/quicproquo-sdk/src/keys.rs
Normal 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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
50
crates/quicproquo-sdk/src/users.rs
Normal file
50
crates/quicproquo-sdk/src/users.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user