identity_core/convert/
base_encoding.rs

1// Copyright 2020-2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::error::Error;
5use crate::error::Result;
6
7/// A [Multibase]-supported base. See [multibase::Base] for more information.
8///
9/// Excludes the identity (0x00) base as arbitrary bytes cannot be encoded to a valid UTF-8 string
10/// in general.
11///
12/// [Multibase]: https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03
13#[allow(missing_docs)]
14#[derive(Debug, Clone, Copy)]
15pub enum Base {
16  /// 8-bit binary (encoder and decoder keeps data unmodified).
17  /// Base2 (alphabet: 01).
18  Base2,
19  /// Base8 (alphabet: 01234567).
20  Base8,
21  /// Base10 (alphabet: 0123456789).
22  Base10,
23  /// Base16 lower hexadecimal (alphabet: 0123456789abcdef).
24  Base16Lower,
25  /// Base16 upper hexadecimal (alphabet: 0123456789ABCDEF).
26  Base16Upper,
27  /// Base32, rfc4648 no padding (alphabet: abcdefghijklmnopqrstuvwxyz234567).
28  Base32Lower,
29  /// Base32, rfc4648 no padding (alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567).
30  Base32Upper,
31  /// Base32, rfc4648 with padding (alphabet: abcdefghijklmnopqrstuvwxyz234567).
32  Base32PadLower,
33  /// Base32, rfc4648 with padding (alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567).
34  Base32PadUpper,
35  /// Base32hex, rfc4648 no padding (alphabet: 0123456789abcdefghijklmnopqrstuv).
36  Base32HexLower,
37  /// Base32hex, rfc4648 no padding (alphabet: 0123456789ABCDEFGHIJKLMNOPQRSTUV).
38  Base32HexUpper,
39  /// Base32hex, rfc4648 with padding (alphabet: 0123456789abcdefghijklmnopqrstuv).
40  Base32HexPadLower,
41  /// Base32hex, rfc4648 with padding (alphabet: 0123456789ABCDEFGHIJKLMNOPQRSTUV).
42  Base32HexPadUpper,
43  /// z-base-32 (used by Tahoe-LAFS) (alphabet: ybndrfg8ejkmcpqxot1uwisza345h769).
44  Base32Z,
45  /// Base36, [0-9a-z] no padding (alphabet: 0123456789abcdefghijklmnopqrstuvwxyz).
46  Base36Lower,
47  /// Base36, [0-9A-Z] no padding (alphabet: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ).
48  Base36Upper,
49  /// Base58 flicker (alphabet: 123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ).
50  Base58Flickr,
51  /// Base58 bitcoin (alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz).
52  Base58Btc,
53  /// Base64, rfc4648 no padding (alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/).
54  Base64,
55  /// Base64, rfc4648 with padding (alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/).
56  Base64Pad,
57  /// Base64 url, rfc4648 no padding (alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_).
58  Base64Url,
59  /// Base64 url, rfc4648 with padding (alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_).
60  Base64UrlPad,
61}
62
63/// Wrap [multibase::Base] to exclude the identity (0x00) and avoid exporting from a pre-1.0 crate.
64impl From<Base> for multibase::Base {
65  fn from(base: Base) -> Self {
66    match base {
67      Base::Base2 => multibase::Base::Base2,
68      Base::Base8 => multibase::Base::Base8,
69      Base::Base10 => multibase::Base::Base10,
70      Base::Base16Lower => multibase::Base::Base16Lower,
71      Base::Base16Upper => multibase::Base::Base16Upper,
72      Base::Base32Lower => multibase::Base::Base32Lower,
73      Base::Base32Upper => multibase::Base::Base32Upper,
74      Base::Base32PadLower => multibase::Base::Base32PadLower,
75      Base::Base32PadUpper => multibase::Base::Base32PadUpper,
76      Base::Base32HexLower => multibase::Base::Base32HexLower,
77      Base::Base32HexUpper => multibase::Base::Base32HexUpper,
78      Base::Base32HexPadLower => multibase::Base::Base32HexPadLower,
79      Base::Base32HexPadUpper => multibase::Base::Base32HexPadUpper,
80      Base::Base32Z => multibase::Base::Base32Z,
81      Base::Base36Lower => multibase::Base::Base36Lower,
82      Base::Base36Upper => multibase::Base::Base36Upper,
83      Base::Base58Flickr => multibase::Base::Base58Flickr,
84      Base::Base58Btc => multibase::Base::Base58Btc,
85      Base::Base64 => multibase::Base::Base64,
86      Base::Base64Pad => multibase::Base::Base64Pad,
87      Base::Base64Url => multibase::Base::Base64Url,
88      Base::Base64UrlPad => multibase::Base::Base64UrlPad,
89    }
90  }
91}
92
93/// Provides utility functions for encoding and decoding between various bases.
94pub struct BaseEncoding;
95
96impl BaseEncoding {
97  /// Encodes the given `data` to the specified [`base`](Base).
98  pub fn encode<T>(data: &T, base: Base) -> String
99  where
100    T: AsRef<[u8]> + ?Sized,
101  {
102    multibase::Base::from(base).encode(data)
103  }
104
105  /// Decodes the given `data` encoded as the specified [`base`](Base).
106  pub fn decode<T>(data: &T, base: Base) -> Result<Vec<u8>>
107  where
108    T: AsRef<str> + ?Sized,
109  {
110    multibase::Base::from(base)
111      .decode(data)
112      .map_err(|err| Error::DecodeBase(base, err))
113  }
114
115  /// Encodes the given `data` to [`Base::Base58Btc`].
116  ///
117  /// Equivalent to `encode(data, Base58Btc)`.
118  pub fn encode_base58<T>(data: &T) -> String
119  where
120    T: AsRef<[u8]> + ?Sized,
121  {
122    Self::encode(data, Base::Base58Btc)
123  }
124
125  /// Decodes the given `data` encoded as [`Base::Base58Btc`].
126  ///
127  /// Equivalent to `decode(data, Base58Btc)`.
128  pub fn decode_base58<T>(data: &T) -> Result<Vec<u8>>
129  where
130    T: AsRef<str> + ?Sized,
131  {
132    Self::decode(data, Base::Base58Btc)
133  }
134
135  /// Encodes the given `data` as [Multibase] with the given [`base`](Base), defaults to
136  /// [`Base::Base58Btc`] if omitted.
137  ///
138  /// NOTE: [`encode_multibase`](Self::encode_multibase) is different from [`encode`](Self::encode) because the
139  /// [Multibase] format prepends a character representing the base-encoding to the output.
140  ///
141  /// [Multibase]: https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03
142  pub fn encode_multibase<T>(data: &T, base: Option<Base>) -> String
143  where
144    T: AsRef<[u8]> + ?Sized,
145  {
146    multibase::encode(multibase::Base::from(base.unwrap_or(Base::Base58Btc)), data)
147  }
148
149  /// Decodes the given `data` encoded as [Multibase], with the [`base`](Base) inferred from the
150  /// leading character.
151  ///
152  /// [Multibase]: https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03
153  pub fn decode_multibase<T>(data: &T) -> Result<Vec<u8>>
154  where
155    T: AsRef<str> + ?Sized,
156  {
157    if data.as_ref().is_empty() {
158      return Ok(Vec::new());
159    }
160    multibase::decode(data)
161      .map(|(_base, output)| output)
162      .map_err(Error::DecodeMultibase)
163  }
164}
165
166#[cfg(test)]
167mod tests {
168  use quickcheck_macros::quickcheck;
169
170  use super::*;
171
172  #[test]
173  fn test_decode_base58_empty() {
174    assert_eq!(BaseEncoding::decode_base58("").unwrap(), Vec::<u8>::new());
175  }
176
177  #[quickcheck]
178  fn test_base58_random(data: Vec<u8>) {
179    assert_eq!(
180      BaseEncoding::decode_base58(&BaseEncoding::encode_base58(&data)).unwrap(),
181      data
182    );
183  }
184
185  #[quickcheck]
186  fn test_base64_random(data: Vec<u8>) {
187    assert_eq!(
188      BaseEncoding::decode(&BaseEncoding::encode(&data, Base::Base64Url), Base::Base64Url).unwrap(),
189      data
190    );
191  }
192
193  /// Base58 test vectors from Internet Engineering Task Force (IETF) Draft.
194  /// https://datatracker.ietf.org/doc/html/draft-msporny-base58-02#section-5
195  #[test]
196  fn test_b58() {
197    let test_vectors: [(&[u8], &str); 3] = [
198      (b"Hello World!", "2NEpo7TZRRrLZSi2U"),
199      (
200        b"The quick brown fox jumps over the lazy dog.",
201        "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z",
202      ),
203      (&[0, 0, 0, 40, 127, 180, 205], "111233QC4"),
204    ];
205    for (test_decoded, test_encoded) in test_vectors {
206      let encoded: String = BaseEncoding::encode_base58(test_decoded);
207      assert_eq!(encoded, test_encoded, "encode failed on {test_decoded:?}");
208
209      let decoded: Vec<u8> = BaseEncoding::decode_base58(test_encoded).unwrap();
210      assert_eq!(decoded, test_decoded, "decode failed on {test_encoded}");
211    }
212  }
213
214  #[test]
215  fn test_decode_multibase_empty() {
216    assert_eq!(BaseEncoding::decode_multibase("").unwrap(), Vec::<u8>::new());
217  }
218
219  #[quickcheck]
220  fn test_multibase_random(data: Vec<u8>) {
221    assert_eq!(
222      BaseEncoding::decode_multibase(&BaseEncoding::encode_multibase(&data, None)).unwrap(),
223      data
224    );
225  }
226
227  #[quickcheck]
228  fn test_multibase_bases_random(data: Vec<u8>) {
229    let bases = [
230      Base::Base2,
231      Base::Base8,
232      Base::Base10,
233      Base::Base16Lower,
234      Base::Base16Upper,
235      Base::Base32Lower,
236      Base::Base32Upper,
237      Base::Base32PadLower,
238      Base::Base32PadUpper,
239      Base::Base32HexLower,
240      Base::Base32HexUpper,
241      Base::Base32HexPadLower,
242      Base::Base32HexPadUpper,
243      Base::Base32Z,
244      Base::Base36Lower,
245      Base::Base36Upper,
246      Base::Base58Flickr,
247      Base::Base58Btc,
248      Base::Base64,
249      Base::Base64Pad,
250      Base::Base64Url,
251      Base::Base64UrlPad,
252    ];
253    for base in bases {
254      assert_eq!(
255        BaseEncoding::decode_multibase(&BaseEncoding::encode_multibase(&data, Some(base))).unwrap(),
256        data
257      );
258    }
259  }
260
261  /// Multibase test vectors from Internet Engineering Task Force (IETF) draft.
262  /// https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03#appendix-B
263  #[test]
264  fn test_multibase() {
265    // Encode.
266    let data: &str = r"Multibase is awesome! \o/";
267    for (base, expected) in [
268      (Base::Base16Upper, "F4D756C74696261736520697320617765736F6D6521205C6F2F"),
269      (Base::Base32Upper, "BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP"),
270      (Base::Base58Btc, "zYAjKoNbau5KiqmHPmSxYCvn66dA1vLmwbt"),
271      (Base::Base64Pad, "MTXVsdGliYXNlIGlzIGF3ZXNvbWUhIFxvLw=="),
272    ] {
273      let encoded: String = BaseEncoding::encode_multibase(data, Some(base));
274      assert_eq!(encoded, expected);
275    }
276
277    // Decode.
278    let expected: Vec<u8> = data.as_bytes().to_vec();
279    for encoded in [
280      "F4D756C74696261736520697320617765736F6D6521205C6F2F",
281      "BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP",
282      "zYAjKoNbau5KiqmHPmSxYCvn66dA1vLmwbt",
283      "MTXVsdGliYXNlIGlzIGF3ZXNvbWUhIFxvLw==",
284    ] {
285      let decoded: Vec<u8> = BaseEncoding::decode_multibase(encoded).unwrap();
286      assert_eq!(decoded, expected, "failed on {encoded}");
287    }
288  }
289}