Skip to main content

client_core/
key_exchange.rs

1//! Shared key-exchange responder logic. Both desktop (`ws.rs`) and
2//! CLI (`pull --watch`, `pair`) implement key-bearer behavior by
3//! invoking `respond` when the relay broadcasts
4//! `key_exchange_requested` for a peer device that has registered a
5//! public key but lacks an encrypted bundle.
6
7use crate::crypto;
8use crate::http::RestClient;
9use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
10
11#[derive(Debug, thiserror::Error)]
12pub enum RespondError {
13    #[error("derive shared key: {0}")]
14    DeriveShared(String),
15    #[error("encrypt user key: {0}")]
16    Encrypt(String),
17    #[error("post bundle: {0}")]
18    Post(#[from] crate::http::HttpError),
19}
20
21/// Build and post an encrypted key bundle for `target_device_id`.
22///
23/// `user_master_key_b64` is the local device's stored encryption key
24/// (`base64url(32-byte AES-256 secret)`). `peer_pub_b64` comes from
25/// the WS event payload; the relay vouches for its origin.
26pub async fn respond(
27    client: &RestClient,
28    target_device_id: &str,
29    peer_pub_b64: &str,
30    user_master_key_b64: &str,
31) -> Result<(), RespondError> {
32    let (eph_priv_b64, eph_pub_b64) = crypto::generate_ephemeral_keypair();
33
34    let shared = crypto::derive_shared_key(&eph_priv_b64, peer_pub_b64)
35        .map_err(RespondError::DeriveShared)?;
36
37    let raw_master = URL_SAFE_NO_PAD
38        .decode(user_master_key_b64)
39        .map_err(|e| RespondError::Encrypt(format!("master key decode: {}", e)))?;
40
41    let encrypted = crypto::encrypt(&shared, &raw_master).map_err(RespondError::Encrypt)?;
42
43    client
44        .post_key_bundle(target_device_id, &eph_pub_b64, &encrypted)
45        .await?;
46    Ok(())
47}