Skip to main content

client_core/
key_exchange.rs

1//! Shared key-exchange responder logic. The desktop's sync writer and
2//! `cinch pull --watch` both invoke `handle_event` when the relay
3//! broadcasts `key_exchange_requested` for a peer device that has
4//! registered a public key but lacks an encrypted bundle.
5
6use crate::crypto;
7use crate::http::{HttpError, RestClient};
8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
9
10#[derive(Debug, thiserror::Error)]
11pub enum RespondError {
12    #[error("derive shared key: {0}")]
13    DeriveShared(String),
14    #[error("encrypt user key: {0}")]
15    Encrypt(String),
16    #[error("post bundle: {0}")]
17    Post(#[from] HttpError),
18}
19
20#[derive(Debug, thiserror::Error)]
21pub enum HandleEventError {
22    #[error("list devices: {0}")]
23    ListDevices(HttpError),
24    #[error("peer device {0} not found in list_devices response")]
25    PeerNotFound(String),
26    #[error("peer device {0} has no public key registered")]
27    PeerPubkeyMissing(String),
28    #[error("respond: {0}")]
29    Respond(#[from] RespondError),
30}
31
32/// Build and post an encrypted key bundle for `target_device_id`.
33///
34/// `user_master_key_b64` is the local device's stored encryption key
35/// (`base64url(32-byte AES-256 secret)`). `peer_pub_b64` comes from
36/// the WS event payload; the relay vouches for its origin.
37pub async fn respond(
38    client: &RestClient,
39    target_device_id: &str,
40    peer_pub_b64: &str,
41    user_master_key_b64: &str,
42) -> Result<(), RespondError> {
43    let (eph_priv_b64, eph_pub_b64) = crypto::generate_ephemeral_keypair();
44
45    let shared = crypto::derive_shared_key(&eph_priv_b64, peer_pub_b64)
46        .map_err(RespondError::DeriveShared)?;
47
48    let raw_master = URL_SAFE_NO_PAD
49        .decode(user_master_key_b64)
50        .map_err(|e| RespondError::Encrypt(format!("master key decode: {}", e)))?;
51
52    let encrypted = crypto::encrypt(&shared, &raw_master).map_err(RespondError::Encrypt)?;
53
54    client
55        .post_key_bundle(target_device_id, &eph_pub_b64, &encrypted)
56        .await?;
57    Ok(())
58}
59
60/// End-to-end handler for a `KeyExchangeRequested` WS event: look up
61/// the peer device's public key via `list_devices`, then call
62/// `respond` to post the encrypted bundle.
63///
64/// Callers (`Writer`, `cinch pull --watch`) get the 32-byte master
65/// key from the local credstore or directly from `WsConfig`. Pass it
66/// in raw — base64url encoding happens here.
67pub async fn handle_event(
68    client: &RestClient,
69    target_device_id: &str,
70    user_master_key: &[u8; 32],
71) -> Result<(), HandleEventError> {
72    let devices = client
73        .list_devices()
74        .await
75        .map_err(HandleEventError::ListDevices)?;
76    let peer = devices
77        .iter()
78        .find(|d| d.id == target_device_id)
79        .ok_or_else(|| HandleEventError::PeerNotFound(target_device_id.to_string()))?;
80    if peer.public_key.is_empty() {
81        return Err(HandleEventError::PeerPubkeyMissing(
82            target_device_id.to_string(),
83        ));
84    }
85    let key_b64 = URL_SAFE_NO_PAD.encode(user_master_key);
86    respond(client, target_device_id, &peer.public_key, &key_b64).await?;
87    Ok(())
88}