igc-net 0.3.0

igc-net protocol rust library — publish and add metadata to IGC flight files
Documentation
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;

use serde::{Deserialize, Deserializer, Serialize, Serializer};

const DID_KEY_PREFIX: &str = "did:key:z";
const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
const BASE58_ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum DidKeyError {
    #[error("invalid did:key: {0:?}")]
    InvalidFormat(String),
    #[error("did:key is not an Ed25519 multicodec")]
    UnsupportedCodec,
    #[error("did:key does not contain a valid Ed25519 public key")]
    InvalidPublicKey,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DidKey(String);

impl DidKey {
    pub fn parse(value: impl Into<String>) -> Result<Self, DidKeyError> {
        let value = value.into();
        parse_did_key_public_key(&value)?;
        Ok(Self(value))
    }

    pub fn from_public_key(public_key: iroh::PublicKey) -> Self {
        let mut multicodec_bytes = Vec::with_capacity(34);
        multicodec_bytes.extend_from_slice(&ED25519_MULTICODEC_PREFIX);
        multicodec_bytes.extend_from_slice(public_key.as_bytes());
        Self(format!(
            "{DID_KEY_PREFIX}{}",
            encode_base58btc(&multicodec_bytes)
        ))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn into_string(self) -> String {
        self.0
    }

    pub fn public_key(&self) -> iroh::PublicKey {
        parse_did_key_public_key(self.as_str()).expect("validated did:key")
    }

    pub fn method_specific_id(&self) -> &str {
        self.0
            .strip_prefix("did:key:")
            .expect("validated did:key prefix")
    }

    pub fn key_id_fragment(&self) -> &str {
        self.method_specific_id()
    }

    pub fn key_id(&self) -> String {
        format!("{}#{}", self.as_str(), self.key_id_fragment())
    }
}

impl fmt::Display for DidKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl Deref for DidKey {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl Borrow<str> for DidKey {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl FromStr for DidKey {
    type Err = DidKeyError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl TryFrom<String> for DidKey {
    type Error = DidKeyError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::parse(value)
    }
}

impl TryFrom<&str> for DidKey {
    type Error = DidKeyError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::parse(value)
    }
}

impl From<DidKey> for String {
    fn from(value: DidKey) -> Self {
        value.into_string()
    }
}

impl Serialize for DidKey {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for DidKey {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        Self::parse(value).map_err(serde::de::Error::custom)
    }
}

fn parse_did_key_public_key(value: &str) -> Result<iroh::PublicKey, DidKeyError> {
    let encoded = value
        .strip_prefix(DID_KEY_PREFIX)
        .ok_or_else(|| DidKeyError::InvalidFormat(value.to_string()))?;
    if encoded.is_empty() {
        return Err(DidKeyError::InvalidFormat(value.to_string()));
    }

    let decoded =
        decode_base58btc(encoded).map_err(|_| DidKeyError::InvalidFormat(value.to_string()))?;
    if decoded.len() != 34 || decoded[..2] != ED25519_MULTICODEC_PREFIX {
        return Err(DidKeyError::UnsupportedCodec);
    }

    let public_key_bytes: [u8; 32] = decoded[2..]
        .try_into()
        .map_err(|_| DidKeyError::InvalidPublicKey)?;
    iroh::PublicKey::from_bytes(&public_key_bytes).map_err(|_| DidKeyError::InvalidPublicKey)
}

fn decode_base58btc(input: &str) -> Result<Vec<u8>, ()> {
    let mut bytes = Vec::<u8>::new();
    for ch in input.bytes() {
        let value = base58_value(ch).ok_or(())? as u32;
        let mut carry = value;
        for byte in bytes.iter_mut().rev() {
            let accum = (*byte as u32) * 58 + carry;
            *byte = (accum & 0xff) as u8;
            carry = accum >> 8;
        }
        while carry > 0 {
            bytes.insert(0, (carry & 0xff) as u8);
            carry >>= 8;
        }
    }

    let leading_zero_count = input.bytes().take_while(|byte| *byte == b'1').count();
    let mut decoded = vec![0u8; leading_zero_count];
    decoded.extend(bytes);
    Ok(decoded)
}

fn encode_base58btc(bytes: &[u8]) -> String {
    if bytes.is_empty() {
        return String::new();
    }

    let mut digits = Vec::<u8>::new();
    for byte in bytes {
        let mut carry = *byte as u32;
        for digit in digits.iter_mut().rev() {
            let accum = (*digit as u32) * 256 + carry;
            *digit = (accum % 58) as u8;
            carry = accum / 58;
        }
        while carry > 0 {
            digits.insert(0, (carry % 58) as u8);
            carry /= 58;
        }
    }

    let mut encoded = String::with_capacity(bytes.len() * 2);
    for _ in bytes.iter().take_while(|byte| **byte == 0) {
        encoded.push('1');
    }
    for digit in digits {
        encoded.push(BASE58_ALPHABET[digit as usize] as char);
    }
    encoded
}

fn base58_value(ch: u8) -> Option<u8> {
    BASE58_ALPHABET
        .iter()
        .position(|candidate| *candidate == ch)
        .map(|value| value as u8)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trip_ed25519_did_key() {
        let secret_key = iroh::SecretKey::from_bytes(&[7u8; 32]);
        let did_key = DidKey::from_public_key(secret_key.public());

        let parsed = DidKey::parse(did_key.as_str()).unwrap();
        assert_eq!(parsed, did_key);
        assert_eq!(parsed.public_key(), secret_key.public());
    }

    #[test]
    fn rejects_non_base58_input() {
        let err = DidKey::parse("did:key:z0OIl").unwrap_err();
        assert!(matches!(err, DidKeyError::InvalidFormat(_)));
    }

    #[test]
    fn rejects_non_ed25519_multicodec() {
        let bad = format!("did:key:z{}", encode_base58btc(&[0x12, 0x20, 0u8]));
        let err = DidKey::parse(bad).unwrap_err();
        assert!(matches!(err, DidKeyError::UnsupportedCodec));
    }

    #[test]
    fn key_id_uses_did_url_fragment_form() {
        let did_key = DidKey::from_public_key(iroh::SecretKey::from_bytes(&[8u8; 32]).public());
        assert_eq!(
            did_key.key_id(),
            format!("{}#{}", did_key.as_str(), did_key.key_id_fragment())
        );
        assert_eq!(did_key.key_id_fragment(), did_key.method_specific_id());
    }
}