skytool 0.1.0-pre.2

an experimental API client for BlueSky / ATProto
Documentation
use std::str::FromStr;

use tap::Tap;

use crate::input::data::source::FileInputSource;

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error(transparent)]
pub(crate) enum Error {
  EllipticCurve(#[from] elliptic_curve::Error),
  Io(#[from] std::io::Error),
  #[error("unsupported curve: {curve}")]
  UnsupportedCurve {
    curve: String,
  },
}

#[derive(Debug, Clone, clap::Parser)]
#[command(about = "derives public keys from private keys")]
#[remain::sorted]
#[rustfmt::skip]
pub(crate) struct Command {
  #[command(flatten)]
  file: FileInputSource,
  #[arg(
    env  = "OUTPUT_FORMAT",
    long = "output-format",
    id   = "output.format",
    value_name    = "FORMAT",
    default_value = "jwk",
    help_heading  = "Output Options"
  )]
  format: Format,
}

impl Command {
  pub(crate) async fn run(self, valkey: &crate::integrations::fred::ValkeyOptions) -> Result<Box<dyn crate::Output>, Error> {
    let Self {
      file: FileInputSource { path },
      format,
    } = self;

    let key = PublicKey::try_from(path.as_path())?;

    let output = format.encode(&key);
    println!("{output}");

    Ok(Box::new(()))
  }
}

#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub(crate) enum Format {
  #[clap(name = "did:key")]
  DidKey,
  #[default]
  Jwk,
  Multibase,
}

impl Format {
  pub(crate) fn encode(&self, key: &PublicKey) -> String {
    match self {
      Self::Jwk => key.to_jwk_string(),
      Self::Multibase => key.to_multibase(),
      Self::DidKey => key
        .to_multibase()
        .tap_mut(|multibase| multibase.insert_str(0, "did:key:")),
    }
  }
}

pub(crate) enum PublicKey {
  K256(k256::PublicKey),
  P256(p256::PublicKey),
}

impl From<k256::PublicKey> for PublicKey {
  fn from(value: k256::PublicKey) -> Self {
    Self::K256(value)
  }
}

impl From<p256::PublicKey> for PublicKey {
  fn from(value: p256::PublicKey) -> Self {
    Self::P256(value)
  }
}

impl TryFrom<elliptic_curve::JwkEcKey> for PublicKey {
  type Error = Error;
  fn try_from(jwk: elliptic_curve::JwkEcKey) -> Result<Self, Self::Error> {
    match jwk.crv() {
      "k256" | "secp256k1" => Ok(Self::K256(jwk.to_public_key()?)),
      "p256" | "P-256" => Ok(Self::P256(jwk.to_public_key()?)),
      _ => Err(Error::UnsupportedCurve {
        curve: jwk.crv().to_owned(),
      }),
    }
  }
}

impl TryFrom<&camino::Utf8Path> for PublicKey {
  type Error = Error;
  fn try_from(path: &camino::Utf8Path) -> Result<Self, Self::Error> {
    elliptic_curve::JwkEcKey::from_str(&fs_err::read_to_string(path)?)?.try_into()
  }
}

trait ToJwkString {
  fn to_jwk_string(&self) -> String;
}

impl ToJwkString for PublicKey {
  fn to_jwk_string(&self) -> String {
    match self {
      Self::K256(key) => key.to_jwk_string(),
      Self::P256(key) => key.to_jwk_string(),
    }
  }
}

trait ToMultibase {
  fn to_multibase(&self) -> String;
}

impl ToMultibase for k256::PublicKey {
  fn to_multibase(&self) -> String {
    let mut input = Vec::<u8>::new();
    input.extend_from_slice(&[0xE7, 0x01]);
    input.extend_from_slice(&self.to_sec1_bytes());
    multibase::encode(multibase::Base::Base58Btc, &input)
  }
}

impl ToMultibase for p256::PublicKey {
  fn to_multibase(&self) -> String {
    let mut input = Vec::<u8>::new();
    input.extend_from_slice(&[0x80, 0x24]);
    input.extend_from_slice(&self.to_sec1_bytes());
    multibase::encode(multibase::Base::Base58Btc, &input)
  }
}

impl ToMultibase for PublicKey {
  fn to_multibase(&self) -> String {
    match self {
      Self::K256(key) => key.to_multibase(),
      Self::P256(key) => key.to_multibase(),
    }
  }
}