use std::fmt::Display;
use crate::{
key_algos::{Ed25519, KeyAlgo, StaticKeyAlgo},
url::{DidMethod, DidUrl},
utf8bytes::Utf8Bytes,
varint::decode_varint,
};
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct DidKey {
s: Utf8Bytes,
mb_value: Vec<u8>,
key_algo: KeyAlgo,
pubkey_bytes: std::ops::RangeFrom<usize>,
}
pub const PREFIX: &str = "did:key:";
impl DidKey {
pub const PREFIX: &'static str = PREFIX;
pub fn as_str(&self) -> &str {
self.s.as_str()
}
pub fn as_slice(&self) -> &[u8] {
self.s.as_slice()
}
pub fn as_utf8_bytes(&self) -> &Utf8Bytes {
&self.s
}
pub fn key_algo(&self) -> KeyAlgo {
self.key_algo
}
pub fn pub_key(&self) -> &[u8] {
let result = match self.key_algo {
KeyAlgo::Ed25519 => &self.mb_value[self.pubkey_bytes.clone()],
};
debug_assert_eq!(result.len(), self.key_algo.pub_key_len());
result
}
}
fn decode_multibase(
s: &Utf8Bytes,
out_buf: &mut Vec<u8>,
) -> Result<(), MultibaseDecodeError> {
out_buf.clear();
let multibase_part = &s.as_slice()[PREFIX.len()..];
let base = multibase_part[0];
if base != b'z' {
return Err(MultibaseDecodeError::WrongBase(base));
}
bs58::decode(&multibase_part[1..])
.with_alphabet(bs58::Alphabet::BITCOIN)
.onto(out_buf)?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum MultibaseDecodeError {
#[error(
"Expected \"base58-btc\" encoding which should be identified in multibase as ascii 'z' (0x7a) but got {0:x}"
)]
WrongBase(u8),
#[error(transparent)]
Bs58(#[from] bs58::decode::Error),
}
impl TryFrom<DidUrl> for DidKey {
type Error = FromUrlError;
fn try_from(value: DidUrl) -> Result<Self, Self::Error> {
let m = value.method();
if m != DidMethod::Key {
return Err(FromUrlError::WrongMethod(m));
}
debug_assert_eq!(
value.as_slice().len() - value.method_specific_id().as_slice().len(),
PREFIX.len(),
"sanity check that prefix has expected length"
);
let s = value.as_utf8_bytes().clone();
let mut decoded_multibase = Vec::new();
decode_multibase(&s, &mut decoded_multibase)?;
let (multicodec_key_algo, tail_bytes) = decode_varint(&decoded_multibase)?;
let (key_algo, pub_key_len) = match multicodec_key_algo {
Ed25519::MULTICODEC_VALUE => (KeyAlgo::Ed25519, Ed25519::PUB_KEY_LEN),
_ => return Err(FromUrlError::UnknownKeyAlgo(multicodec_key_algo)),
};
if tail_bytes.len() != pub_key_len {
return Err(FromUrlError::MismatchedPubkeyLen(key_algo, pub_key_len));
}
let pubkey_bytes = (decoded_multibase.len() - pub_key_len)..;
Ok(Self {
s,
mb_value: decoded_multibase,
key_algo,
pubkey_bytes,
})
}
}
#[derive(thiserror::Error, Debug)]
pub enum FromUrlError {
#[error("Expected \"key\" method but got {0:?}")]
WrongMethod(DidMethod),
#[error(transparent)]
MultibaseDecode(#[from] MultibaseDecodeError),
#[error("unknown multicodec value for key algorithm: decoded varint as {0}")]
UnknownKeyAlgo(u16),
#[error(transparent)]
Varint(#[from] crate::varint::DecodeError),
#[error("{0:?} requires pubkeys of length {} but got {1} bytes", .0.pub_key_len())]
MismatchedPubkeyLen(KeyAlgo, usize),
}
impl Display for DidKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
#[cfg(test)]
mod test {
use super::*;
use eyre::WrapErr;
use hex_literal::hex;
use std::str::FromStr;
fn ed25519_examples() -> &'static [&'static str] {
&[
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
]
}
#[test]
fn test_try_from_url() -> eyre::Result<()> {
for &example in ed25519_examples() {
let url = DidUrl::from_str(example)
.wrap_err_with(|| format!("failed to parse DidUrl from {example}"))?;
assert_eq!(example, url.as_str());
let key_from_url = DidKey::try_from(url.clone())
.wrap_err_with(|| format!("failed to parse DidKey from {url}"))?;
assert_eq!(example, key_from_url.as_str());
assert_eq!(key_from_url.key_algo(), Ed25519);
}
Ok(())
}
#[test]
fn test_decode_multibase() -> eyre::Result<()> {
#[derive(Debug)]
struct Example {
decoded: &'static [u8],
encoded: &'static str,
}
let examples = [
Example {
decoded: b"Hello World!",
encoded: "2NEpo7TZRRrLZSi2U",
},
Example {
decoded: b"The quick brown fox jumps over the lazy dog.",
encoded: "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z",
},
Example {
decoded: &hex!("0000287fb4cd"),
encoded: "11233QC4",
},
];
let mut buf = Vec::new();
for e @ Example { decoded, encoded } in examples {
let s = format!("{}z{encoded}", PREFIX).into();
decode_multibase(&s, &mut buf)
.wrap_err_with(|| format!("Failed to decode example {e:?}"))?;
assert_eq!(buf, decoded, "failed comparison in example {e:?}");
}
Ok(())
}
}