umsh_cli/
peer_ref.rs

1//! `<peer-ref>` token resolution used by every peer-naming command.
2//!
3//! Accepts any of:
4//! - a registered alias (ASCII, up to 16 chars),
5//! - the canonical fixed-width base58 address (exactly 44 chars),
6//! - hex-encoded 32-byte pubkey (64 hex chars, optional `0x` prefix),
7//! - base64-encoded 32-byte pubkey (standard or URL-safe alphabet).
8//!
9//! 3-byte hints are NOT accepted — they aren't unique and the CLI refuses
10//! to guess.
11
12use umsh_core::PublicKey;
13
14/// Decode a `<peer-ref>` token into a `PublicKey`. Alias lookup is the
15/// caller's concern; this helper only handles the encoded forms.
16///
17/// The canonical forms (44-char base58 and 64-char hex, distinguished by
18/// length) are tried first via [`PublicKey::from_str`], then base64.
19///
20/// After successful decoding the bytes are validated as a well-formed
21/// Ed25519 compressed public-key point on the curve (when the
22/// `software-crypto` feature is enabled). Bytes that decode to 32 bytes but
23/// do not lie on the curve return `None` so a typo'd hex string can never
24/// be accepted as a peer key.
25///
26/// Returns `None` if the token doesn't decode to a full 32-byte key in any
27/// of the supported encodings, or if the decoded bytes are not a valid
28/// Ed25519 point.
29pub fn try_parse_pubkey(token: &str) -> Option<PublicKey> {
30    let canonical = token.strip_prefix("0x").unwrap_or(token);
31    let key = canonical
32        .parse::<PublicKey>()
33        .ok()
34        .or_else(|| try_b64(token))?;
35    #[cfg(feature = "software-crypto")]
36    {
37        if !umsh_crypto::is_valid_ed25519_public_key(&key) {
38            return None;
39        }
40    }
41    Some(key)
42}
43
44fn try_b64(token: &str) -> Option<PublicKey> {
45    // Minimal base64 decoder for 32-byte keys. Supports both standard and
46    // URL-safe alphabets; padding is optional.
47    let bytes = token.as_bytes();
48    if bytes.is_empty() {
49        return None;
50    }
51    // Strip trailing '=' padding.
52    let end = bytes
53        .iter()
54        .rposition(|&b| b != b'=')
55        .map(|i| i + 1)
56        .unwrap_or(0);
57    let src = &bytes[..end];
58
59    let mut out = [0u8; 32];
60    let mut buf: u32 = 0;
61    let mut bits: u32 = 0;
62    let mut oi = 0;
63
64    for &b in src {
65        let v: u32 = match b {
66            b'A'..=b'Z' => (b - b'A') as u32,
67            b'a'..=b'z' => (b - b'a') as u32 + 26,
68            b'0'..=b'9' => (b - b'0') as u32 + 52,
69            b'+' | b'-' => 62,
70            b'/' | b'_' => 63,
71            _ => return None,
72        };
73        buf = (buf << 6) | v;
74        bits += 6;
75        if bits >= 8 {
76            bits -= 8;
77            if oi >= 32 {
78                return None;
79            }
80            out[oi] = ((buf >> bits) & 0xFF) as u8;
81            oi += 1;
82        }
83    }
84    if oi == 32 { Some(PublicKey(out)) } else { None }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use umsh_crypto::NodeIdentity;
91    use umsh_crypto::software::SoftwareIdentity;
92
93    /// A real Ed25519 public key derived from a known seed. Generated at
94    /// test time so the constant cannot silently drift away from a
95    /// valid point on the curve.
96    fn valid_key() -> [u8; 32] {
97        SoftwareIdentity::from_secret_bytes(&[0x11; 32])
98            .public_key()
99            .0
100    }
101
102    #[test]
103    fn decodes_hex() {
104        let key = valid_key();
105        let hex = std::format!("{:x}", PublicKey(key));
106        assert_eq!(try_parse_pubkey(&hex).unwrap().0, key);
107        let prefixed = std::format!("0x{}", hex.to_uppercase());
108        assert_eq!(try_parse_pubkey(&prefixed).unwrap().0, key);
109    }
110
111    #[test]
112    fn decodes_base58() {
113        let key = valid_key();
114        let b58 = PublicKey(key).to_string();
115        assert_eq!(b58.len(), 44);
116        assert_eq!(try_parse_pubkey(&b58).unwrap().0, key);
117    }
118
119    #[test]
120    fn rejects_short_or_junk() {
121        assert!(try_parse_pubkey("").is_none());
122        assert!(try_parse_pubkey("hello").is_none());
123        assert!(try_parse_pubkey("0x01").is_none());
124    }
125
126    #[test]
127    fn rejects_non_curve_point() {
128        // Y = 2 (little-endian) is well-formed hex but does not lie on
129        // the Ed25519 curve. Without curve validation this would have
130        // been blindly accepted as a peer key and only failed later at
131        // ECDH time with `IdentityAgreementFailed`.
132        let bogus = "0200000000000000000000000000000000000000000000000000000000000000";
133        assert!(try_parse_pubkey(bogus).is_none());
134    }
135}