agent-phone 0.1.0

Minimal sync RPC between two AI agents (Rust port of @p-vbordei/agent-phone). Self-custody keys, Noise-framework handshake, DID-bound WebSocket.
Documentation
//! DID + key handling: Ed25519 keypairs, did:key codec, Ed25519↔X25519 conversion.

use crate::error::{Error, Result};
use ed25519_dalek::{SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use sha2::{Digest, Sha512};

pub const MULTICODEC_ED25519_PUB: [u8; 2] = [0xED, 0x01];

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyPair {
    pub public_key: [u8; 32],
    pub private_key: [u8; 32],
}

pub fn generate_key_pair() -> KeyPair {
    let signing = SigningKey::generate(&mut OsRng);
    let verifying: VerifyingKey = signing.verifying_key();
    KeyPair {
        public_key: verifying.to_bytes(),
        private_key: signing.to_bytes(),
    }
}

pub fn encode_did_key(public_key: &[u8]) -> Result<String> {
    if public_key.len() != 32 {
        return Err(Error::InvalidDid(format!(
            "Ed25519 pubkey must be 32 bytes, got {}",
            public_key.len()
        )));
    }
    let mut prefixed = Vec::with_capacity(34);
    prefixed.extend_from_slice(&MULTICODEC_ED25519_PUB);
    prefixed.extend_from_slice(public_key);
    let encoded = bs58::encode(&prefixed).into_string();
    Ok(format!("did:key:z{}", encoded))
}

pub fn decode_did_key(did: &str) -> Result<[u8; 32]> {
    let prefix = "did:key:z";
    if !did.starts_with(prefix) {
        return Err(Error::InvalidDid("not a did:key identifier".into()));
    }
    let decoded = bs58::decode(&did[prefix.len()..])
        .into_vec()
        .map_err(|e| Error::InvalidDid(format!("base58 decode failed: {e}")))?;
    if decoded.len() < 34
        || decoded[0] != MULTICODEC_ED25519_PUB[0]
        || decoded[1] != MULTICODEC_ED25519_PUB[1]
    {
        return Err(Error::InvalidDid(
            "did:key is not an Ed25519 key (wrong multicodec prefix or truncated)".into(),
        ));
    }
    let mut out = [0u8; 32];
    out.copy_from_slice(&decoded[2..34]);
    Ok(out)
}

/// Convert an Ed25519 public key to an X25519 public key
/// (Edwards y → Montgomery u = (1+y)/(1-y) mod p).
pub fn ed25519_pub_to_x25519(ed_pub: &[u8; 32]) -> [u8; 32] {
    let verifying = VerifyingKey::from_bytes(ed_pub).expect("valid Edwards point");
    let edwards = verifying.to_edwards();
    let montgomery = edwards.to_montgomery();
    montgomery.to_bytes()
}

/// Convert an Ed25519 private key (32-byte seed) to an X25519 private key.
/// Matches `edwardsToMontgomeryPriv` from `@noble/curves`:
/// take SHA-512(seed)[0..32] and clamp.
pub fn ed25519_priv_to_x25519(ed_priv: &[u8; 32]) -> [u8; 32] {
    let mut h = Sha512::new();
    h.update(ed_priv);
    let digest = h.finalize();
    let mut out = [0u8; 32];
    out.copy_from_slice(&digest[..32]);
    // RFC 7748 clamping.
    out[0] &= 248;
    out[31] &= 127;
    out[31] |= 64;
    out
}