use multibase::Base;
use unsigned_varint::{decode, encode};
use crate::error::Error;
use crate::key::ed25519::{ED25519_PUBLIC_KEY_LEN, Ed25519PublicKey};
pub(crate) const ED25519_MULTICODEC: u64 = 0xed;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Multikey {
pub encoded: String,
pub key: Ed25519PublicKey,
}
impl Multikey {
#[must_use]
pub fn encode_ed25519(key: &Ed25519PublicKey) -> String {
let mut buf = encode::u64_buffer();
let prefix = encode::u64(ED25519_MULTICODEC, &mut buf);
let mut bytes = Vec::with_capacity(prefix.len() + ED25519_PUBLIC_KEY_LEN);
bytes.extend_from_slice(prefix);
bytes.extend_from_slice(key.as_bytes());
multibase::encode(Base::Base58Btc, bytes)
}
pub fn decode(encoded: &str) -> Result<Self, Error> {
let (_base, bytes) = multibase::decode(encoded)?;
let (codec, rest) = decode::u64(&bytes).map_err(|_| Error::InvalidMultikeyPrefix)?;
if codec != ED25519_MULTICODEC {
return Err(Error::UnsupportedAlgorithm(format!(
"Multikey codec 0x{codec:x} is not Ed25519 (0x{ED25519_MULTICODEC:x})"
)));
}
let key = Ed25519PublicKey::from_bytes(rest)?;
Ok(Self {
encoded: encoded.to_owned(),
key,
})
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::key::ed25519::Ed25519SigningKey;
#[test]
fn encode_then_decode_roundtrips() {
let signing = Ed25519SigningKey::generate().expect("rng");
let public = signing.public_key();
let encoded = Multikey::encode_ed25519(&public);
assert!(
encoded.starts_with('z'),
"Multikey must be base58-btc encoded (prefix `z`)",
);
let decoded = Multikey::decode(&encoded).expect("round-trip decode");
assert_eq!(decoded.key, public);
assert_eq!(decoded.encoded, encoded);
}
#[test]
fn decode_rejects_wrong_codec() {
let mut bytes = Vec::new();
let mut buf = encode::u64_buffer();
let prefix = encode::u64(0x1205, &mut buf);
bytes.extend_from_slice(prefix);
bytes.extend_from_slice(&[0u8; 32]); let encoded = multibase::encode(Base::Base58Btc, bytes);
let err = Multikey::decode(&encoded).expect_err("RSA codec must be rejected");
assert!(matches!(err, Error::UnsupportedAlgorithm(_)));
}
#[test]
fn decode_rejects_wrong_body_length() {
let mut bytes = Vec::new();
let mut buf = encode::u64_buffer();
let prefix = encode::u64(ED25519_MULTICODEC, &mut buf);
bytes.extend_from_slice(prefix);
bytes.extend_from_slice(&[0u8; 16]); let encoded = multibase::encode(Base::Base58Btc, bytes);
let err = Multikey::decode(&encoded).expect_err("short body must be rejected");
assert!(matches!(
err,
Error::InvalidMultikeyLength {
expected: 32,
actual: 16
}
));
}
#[test]
fn decode_rejects_garbage() {
let err = Multikey::decode("not-a-multibase-string").expect_err("bad multibase");
assert!(matches!(err, Error::InvalidMultibase(_)));
}
#[test]
fn decodes_known_good_fixture() {
let key_bytes =
hex_literal::hex!("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
let public = Ed25519PublicKey::from_bytes(&key_bytes).expect("valid key");
let encoded = Multikey::encode_ed25519(&public);
assert!(
encoded.starts_with("z6Mk"),
"Ed25519 Multikey should begin with `z6Mk`, got `{encoded}`",
);
let decoded = Multikey::decode(&encoded).expect("decode");
assert_eq!(decoded.key.as_bytes(), &key_bytes);
}
}