baid64/
lib.rs

1// Base64 encoding extended with HRP and mnemonic checksum information
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2024 by
6//     Dr Maxim Orlovsky <orlovsky@ubideco.//>
7//
8// Copyright (C) 2024 UBIDECO Institute, Switzerland. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22#[macro_use]
23extern crate amplify;
24pub extern crate base64;
25
26use std::error::Error;
27use std::fmt::{self, Display, Formatter};
28
29use base64::Engine;
30use sha2::Digest;
31
32pub const ID_MIN_LEN: usize = 4;
33pub const HRI_MAX_LEN: usize = 16;
34
35pub const BAID64_ALPHABET: &str =
36    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~";
37
38fn check<const LEN: usize>(hri: &'static str, payload: [u8; LEN]) -> [u8; 4] {
39    let key = sha2::Sha256::digest(hri.as_bytes());
40    let mut sha = sha2::Sha256::new_with_prefix(key);
41    sha.update(payload);
42    let sha = sha.finalize();
43    [sha[0], sha[1], sha[1], sha[2]]
44}
45
46pub trait DisplayBaid64<const LEN: usize = 32> {
47    const HRI: &'static str;
48    const CHUNKING: bool;
49    const PREFIX: bool;
50    const EMBED_CHECKSUM: bool;
51    const MNEMONIC: bool;
52    const CHUNK_FIRST: usize = 8;
53    const CHUNK_LEN: usize = 7;
54
55    fn to_baid64_payload(&self) -> [u8; LEN];
56    fn to_baid64_string(&self) -> String { self.display_baid64().to_string() }
57    fn to_baid64_mnemonic(&self) -> String { self.display_baid64().mnemonic }
58    fn display_baid64(&self) -> Baid64Display<LEN> {
59        Baid64Display::with(
60            Self::HRI,
61            self.to_baid64_payload(),
62            Self::CHUNKING,
63            Self::CHUNK_FIRST,
64            Self::CHUNK_LEN,
65            Self::PREFIX,
66            Self::MNEMONIC,
67            Self::EMBED_CHECKSUM,
68        )
69    }
70    fn fmt_baid64(&self, f: &mut Formatter) -> fmt::Result {
71        Display::fmt(&self.display_baid64(), f)
72    }
73}
74
75#[derive(Debug, Display, Error, From)]
76#[display(doc_comments)]
77pub enum Baid64ParseError {
78    /// invalid human-readable prefix in {0} ({1} is expected).
79    InvalidHri(String, &'static str),
80
81    /// invalid length of identifier {0}.
82    InvalidLen(String),
83
84    /// invalid checksum value in {0} - expected {1:#x} while found
85    /// {2:#x}.
86    InvalidChecksum(String, u32, u32),
87
88    /// invalid length of mnemonic in {0}.
89    InvalidMnemonicLen(String),
90
91    #[from]
92    #[display(inner)]
93    InvalidMnemonic(mnemonic::Error),
94
95    #[from]
96    #[display(inner)]
97    Base64(base64::DecodeError),
98
99    /// invalid Baid64 payload - {0}
100    InvalidPayload(String),
101}
102
103pub trait FromBaid64Str<const LEN: usize = 32>
104where
105    Self: DisplayBaid64<LEN> + TryFrom<[u8; LEN]>,
106    <Self as TryFrom<[u8; LEN]>>::Error: Error,
107{
108    fn from_baid64_str(mut s: &str) -> Result<Self, Baid64ParseError> {
109        let orig = s;
110
111        use base64::alphabet::Alphabet;
112        use base64::engine::GeneralPurpose;
113        use base64::engine::general_purpose::NO_PAD;
114
115        let mut checksum = None;
116
117        if let Some((hri, rest)) = s.rsplit_once(':') {
118            if hri != Self::HRI {
119                return Err(Baid64ParseError::InvalidHri(orig.to_owned(), Self::HRI));
120            }
121            s = rest;
122        }
123
124        if let Some((rest, sfx)) = s.split_once('#') {
125            let mut mnemo = Vec::<u8>::with_capacity(4);
126            mnemonic::decode(sfx, &mut mnemo)?;
127            if mnemo.len() != 4 {
128                return Err(Baid64ParseError::InvalidMnemonicLen(orig.to_string()));
129            }
130            checksum = Some([mnemo[0], mnemo[1], mnemo[2], mnemo[3]]);
131            s = rest;
132        }
133
134        let s = if s.contains('-') {
135            s.replace('-', "")
136        } else {
137            s.to_owned()
138        };
139
140        let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet");
141        let engine = GeneralPurpose::new(&alphabet, NO_PAD);
142        let data = engine.decode(s)?;
143
144        if data.len() != LEN && data.len() != LEN + 4 {
145            return Err(Baid64ParseError::InvalidLen(orig.to_owned()));
146        }
147        let mut payload = [0u8; LEN];
148        payload.copy_from_slice(&data[..LEN]);
149        if data.len() == LEN + 4 {
150            checksum = Some([data[LEN], data[LEN + 1], data[LEN + 2], data[LEN + 3]]);
151        }
152
153        let ck = check(Self::HRI, payload);
154        if matches!(checksum, Some(c) if c != ck) {
155            return Err(Baid64ParseError::InvalidChecksum(
156                orig.to_owned(),
157                u32::from_le_bytes(ck),
158                u32::from_le_bytes(checksum.unwrap()),
159            ));
160        }
161
162        Self::try_from(payload).map_err(|e| Baid64ParseError::InvalidPayload(e.to_string()))
163    }
164}
165
166#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
167pub struct Baid64Display<const LEN: usize = 32> {
168    hri: &'static str,
169    chunking: bool,
170    chunk_first: usize,
171    chunk_len: usize,
172    mnemonic: String,
173    prefix: bool,
174    suffix: bool,
175    embed_checksum: bool,
176    checksum: [u8; 4],
177    payload: [u8; LEN],
178}
179
180impl<const LEN: usize> Baid64Display<LEN> {
181    pub fn with(
182        hri: &'static str,
183        payload: [u8; LEN],
184        chunking: bool,
185        chunk_first: usize,
186        chunk_len: usize,
187        prefix: bool,
188        suffix: bool,
189        embed_checksum: bool,
190    ) -> Self {
191        debug_assert!(
192            hri.len() <= HRI_MAX_LEN,
193            "HRI is too long; it must not exceed {HRI_MAX_LEN} bytes"
194        );
195        debug_assert!(LEN > ID_MIN_LEN, "Baid64 id payload must be at least {ID_MIN_LEN} bytes");
196
197        let checksum = check(hri, payload);
198        let mnemonic = mnemonic::to_string(checksum);
199
200        Self {
201            hri,
202            chunking,
203            chunk_first,
204            chunk_len,
205            mnemonic,
206            prefix,
207            suffix,
208            embed_checksum,
209            checksum,
210            payload,
211        }
212    }
213
214    pub fn new(hri: &'static str, payload: [u8; LEN]) -> Self {
215        Self::with(hri, payload, false, 8, 7, false, false, false)
216    }
217    pub const fn use_hri(mut self) -> Self {
218        self.prefix = true;
219        self
220    }
221    pub const fn use_chunking(mut self) -> Self {
222        self.chunking = true;
223        self
224    }
225    pub const fn use_mnemonic(mut self) -> Self {
226        self.suffix = true;
227        self
228    }
229    pub const fn embed_checksum(mut self) -> Self {
230        self.embed_checksum = true;
231        self
232    }
233
234    pub const fn human_identifier(&self) -> &'static str { self.hri }
235
236    pub fn mnemonic(&self) -> &str { self.mnemonic.as_str() }
237    pub const fn checksum(&self) -> [u8; 4] { self.checksum }
238}
239
240impl<const LEN: usize> Display for Baid64Display<LEN> {
241    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
242        use base64::alphabet::Alphabet;
243        use base64::engine::GeneralPurpose;
244        use base64::engine::general_purpose::NO_PAD;
245
246        if (self.prefix && !f.sign_minus()) || (!self.prefix && f.sign_minus()) {
247            write!(f, "{}:", self.hri)?;
248        }
249
250        let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet");
251        let engine = GeneralPurpose::new(&alphabet, NO_PAD);
252
253        let mut payload = self.payload.to_vec();
254        if self.embed_checksum {
255            payload.extend(self.checksum);
256        }
257        let s = engine.encode(payload);
258
259        if self.chunking {
260            let bytes = s.as_bytes();
261            f.write_str(&String::from_utf8_lossy(&bytes[..self.chunk_first]))?;
262            for chunk in bytes[self.chunk_first..].chunks(self.chunk_len) {
263                write!(f, "-{}", &String::from_utf8_lossy(chunk))?;
264            }
265        } else {
266            f.write_str(&s)?;
267        }
268
269        if (self.suffix && !f.alternate()) || (!self.suffix && f.alternate()) {
270            write!(f, "#{}", self.mnemonic)?;
271        }
272
273        Ok(())
274    }
275}