Skip to main content

aph_core/crypto/
multibase.rs

1//! Multibase base58btc — the encoding APH proof values and `did:key`
2//! identifiers use.
3//!
4//! Multibase prefixes an encoded string with a character naming its base;
5//! `z` is base58btc. A proof value therefore looks like `z3Wgv...`, and a
6//! `did:key` identifier like `did:key:z6Mk...`.
7//!
8//! This is wire-visible in every envelope, so the encoding must match other
9//! implementations byte-for-byte. base58btc is deterministic, so any correct
10//! implementation agrees.
11
12/// Multibase tag for base58btc.
13const BASE58BTC_TAG: char = 'z';
14
15/// Encodes bytes as multibase base58btc (a `z`-prefixed string).
16pub fn base58btc_encode(input: &[u8]) -> String {
17  let mut out = String::with_capacity(1 + input.len() * 2);
18  out.push(BASE58BTC_TAG);
19  out.push_str(&bs58::encode(input).into_string());
20  out
21}
22
23/// Decodes a multibase base58btc string.
24///
25/// Returns `APH_E001` when the string lacks the `z` tag or is not valid
26/// base58 — an unreadable proof value must fail verification rather than be
27/// silently treated as an empty signature.
28pub fn base58btc_decode(input: &str) -> std::result::Result<std::vec::Vec<u8>, crate::errors::AphError> {
29  let body = match input.strip_prefix(BASE58BTC_TAG) {
30    std::option::Option::Some(b) => b,
31    std::option::Option::None => {
32      return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
33    }
34  };
35  match bs58::decode(body).into_vec() {
36    std::result::Result::Ok(bytes) => std::result::Result::Ok(bytes),
37    std::result::Result::Err(_) => {
38      std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature)
39    }
40  }
41}
42
43#[cfg(test)]
44mod tests {
45  #[test]
46  fn round_trip_preserves_bytes() {
47    // The encoding carries signatures and public keys; a lossy round trip
48    // would corrupt a proof value in transit.
49    let data = [1u8, 2, 3, 250, 255, 0, 128];
50    let encoded = super::base58btc_encode(&data);
51    std::assert!(encoded.starts_with('z'));
52    std::assert_eq!(super::base58btc_decode(&encoded).unwrap(), data.to_vec());
53  }
54
55  #[test]
56  fn leading_zero_bytes_survive() {
57    // base58 drops leading zeros unless they are encoded as '1' characters.
58    // A public key beginning with a zero byte would otherwise decode short
59    // and fail signature verification for non-obvious reasons.
60    let data = [0u8, 0, 0, 42];
61    let encoded = super::base58btc_encode(&data);
62    std::assert_eq!(super::base58btc_decode(&encoded).unwrap(), data.to_vec());
63  }
64
65  #[test]
66  fn missing_multibase_tag_is_rejected() {
67    // Bare base58 without the 'z' tag is a different encoding. Accepting it
68    // would mean guessing at the base, so it fails closed.
69    std::assert!(super::base58btc_decode("6MkfAkf").is_err());
70  }
71
72  #[test]
73  fn invalid_base58_is_rejected_not_panicking() {
74    // Proof values arrive from the wire: characters outside the base58
75    // alphabet (0, O, I, l) must produce an error, never a panic.
76    std::assert!(super::base58btc_decode("z0OIl").is_err());
77  }
78
79  #[test]
80  fn empty_payload_round_trips() {
81    // A bare "z" is a legal encoding of zero bytes; it must not be confused
82    // with a missing tag.
83    std::assert_eq!(super::base58btc_decode("z").unwrap(), std::vec::Vec::<u8>::new());
84  }
85}