Skip to main content

affinidi_encoding/
multibase.rs

1//! Multibase encoding/decoding utilities
2//!
3//! Multibase is a protocol for self-describing base encodings.
4//! The first character indicates the encoding used.
5//!
6//! See: <https://github.com/multiformats/multibase>
7
8use crate::EncodingError;
9use crate::multicodec::MultiEncoded;
10
11/// Multibase prefix for base58btc (Bitcoin alphabet)
12pub const BASE58BTC_PREFIX: char = 'z';
13
14/// Decode a base58btc multibase string (must start with 'z')
15///
16/// Returns the decoded bytes without the prefix.
17pub fn decode_base58btc(s: &str) -> Result<Vec<u8>, EncodingError> {
18    let Some(encoded) = s.strip_prefix(BASE58BTC_PREFIX) else {
19        let prefix = s.chars().next().unwrap_or('\0');
20        return Err(EncodingError::InvalidMultibasePrefix(prefix));
21    };
22
23    bs58::decode(encoded)
24        .into_vec()
25        .map_err(|e| EncodingError::InvalidBase58(e.to_string()))
26}
27
28/// Encode bytes as base58btc with multibase prefix 'z'
29pub fn encode_base58btc(bytes: &[u8]) -> String {
30    format!("{}{}", BASE58BTC_PREFIX, bs58::encode(bytes).into_string())
31}
32
33/// Validate that a string is valid base58btc multibase (starts with 'z' and decodes correctly)
34pub fn validate_base58btc(s: &str) -> Result<(), EncodingError> {
35    decode_base58btc(s)?;
36    Ok(())
37}
38
39/// Decode a multikey string (multibase + multicodec encoded)
40///
41/// Returns just the key bytes without the multicodec prefix.
42/// This is the inverse of how keys are encoded in DID documents (publicKeyMultibase).
43pub fn decode_multikey(key: &str) -> Result<Vec<u8>, EncodingError> {
44    let bytes = decode_base58btc(key)?;
45    let multi_encoded = MultiEncoded::new(&bytes)?;
46    Ok(multi_encoded.data().to_vec())
47}
48
49/// Decode a multikey string and return both codec and key bytes
50pub fn decode_multikey_with_codec(key: &str) -> Result<(u64, Vec<u8>), EncodingError> {
51    let bytes = decode_base58btc(key)?;
52    let multi_encoded = MultiEncoded::new(&bytes)?;
53    Ok((multi_encoded.codec(), multi_encoded.data().to_vec()))
54}
55
56/// Encode key bytes with a multicodec prefix as a multibase (base58btc) string
57///
58/// This is the inverse of `decode_multikey`. The result is suitable for
59/// use as a DID key identifier or publicKeyMultibase value.
60pub fn encode_multikey(codec: u64, key_bytes: &[u8]) -> String {
61    let encoded = crate::multicodec::MultiEncodedBuf::encode_bytes(codec, key_bytes);
62    encode_base58btc(encoded.as_bytes())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_decode_base58btc() {
71        // "z" + base58btc("hello") = "zCn8eVZg"
72        let result = decode_base58btc("zCn8eVZg").unwrap();
73        assert_eq!(result, b"hello");
74    }
75
76    #[test]
77    fn test_encode_base58btc() {
78        let encoded = encode_base58btc(b"hello");
79        assert_eq!(encoded, "zCn8eVZg");
80    }
81
82    #[test]
83    fn test_roundtrip() {
84        let original = b"test data for encoding";
85        let encoded = encode_base58btc(original);
86        let decoded = decode_base58btc(&encoded).unwrap();
87        assert_eq!(decoded, original);
88    }
89
90    #[test]
91    fn test_invalid_prefix() {
92        let result = decode_base58btc("fABCDEF"); // 'f' is hex, not base58btc
93        assert!(matches!(
94            result.unwrap_err(),
95            EncodingError::InvalidMultibasePrefix('f')
96        ));
97    }
98
99    #[test]
100    fn test_invalid_base58() {
101        // '0', 'O', 'I', 'l' are not valid base58 characters
102        let result = decode_base58btc("z0OIl");
103        assert!(matches!(
104            result.unwrap_err(),
105            EncodingError::InvalidBase58(_)
106        ));
107    }
108
109    #[test]
110    fn test_did_key_identifier() {
111        // Real did:key identifier (ed25519)
112        let id = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
113        let result = decode_base58btc(id);
114        assert!(result.is_ok());
115        let bytes = result.unwrap();
116        // First byte should be 0xed (ed25519 multicodec prefix)
117        assert_eq!(bytes[0], 0xed);
118    }
119}