armor/
lib.rs

1// ASCII Armor: binary to text encoding library and command-line utility.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2024 by
6//     Dr. Maxim Orlovsky <orlovsky@ubideco.org>
7//
8// Copyright 2024 UBIDECO Institute, Switzerland
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#[cfg(not(any(feature = "baid64", feature = "base85")))]
23compile_error!("either baid64 or base85 feature must be specified, you provide none of them.");
24
25#[cfg(all(feature = "baid64", feature = "base85"))]
26compile_error!("either baid64 or base85 feature must be specified, you provide both of them.");
27
28#[macro_use]
29extern crate amplify;
30
31use core::fmt::{self, Debug, Display, Formatter};
32use core::str::FromStr;
33
34#[cfg(feature = "strict")]
35use amplify::confinement::U24 as U24MAX;
36#[cfg(feature = "strict")]
37use amplify::confinement::{self, Confined};
38use amplify::num::u24;
39use amplify::{Bytes32, hex};
40use sha2::{Digest, Sha256};
41#[cfg(feature = "strict")]
42use strict_encoding::{StrictDeserialize, StrictSerialize};
43
44pub const ASCII_ARMOR_MAX_LEN: usize = u24::MAX.to_usize();
45pub const ASCII_ARMOR_ID: &str = "Id";
46pub const ASCII_ARMOR_CHECKSUM_SHA256: &str = "Check-SHA256";
47
48pub struct DisplayAsciiArmored<'a, A: AsciiArmor>(&'a A);
49
50impl<'a, A: AsciiArmor> DisplayAsciiArmored<'a, A> {
51    fn data_digest(&self) -> (Vec<u8>, Option<Bytes32>) {
52        let data = self.0.to_ascii_armored_data();
53        let digest = Sha256::digest(&data);
54        (data, Some(Bytes32::from_byte_array(digest)))
55    }
56}
57
58impl<'a, A: AsciiArmor> Display for DisplayAsciiArmored<'a, A> {
59    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
60        writeln!(f, "-----BEGIN {}-----", A::PLATE_TITLE)?;
61
62        let (data, digest) = self.data_digest();
63        for header in self.0.ascii_armored_headers() {
64            writeln!(f, "{header}")?;
65        }
66        if let Some(digest) = digest {
67            writeln!(f, "{ASCII_ARMOR_CHECKSUM_SHA256}: {digest}")?;
68        }
69        writeln!(f)?;
70
71        #[cfg(feature = "base85")]
72        let data = base85::encode(&data);
73        #[cfg(feature = "baid64")]
74        let data = {
75            use baid64::base64::Engine;
76            baid64::base64::prelude::BASE64_STANDARD.encode(&data)
77        };
78        let mut data = data.as_str();
79        while data.len() >= 80 {
80            let (line, rest) = data.split_at(80);
81            writeln!(f, "{}", line)?;
82            data = rest;
83        }
84        writeln!(f, "{}", data)?;
85
86        writeln!(f, "\n-----END {}-----", A::PLATE_TITLE)?;
87
88        Ok(())
89    }
90}
91
92#[derive(Clone, Eq, PartialEq, Hash, Debug)]
93pub struct ArmorHeader {
94    pub title: String,
95    pub values: Vec<String>,
96    pub params: Vec<(String, String)>,
97}
98
99impl ArmorHeader {
100    pub fn new(title: &'static str, value: String) -> Self {
101        ArmorHeader {
102            title: title.to_owned(),
103            values: vec![value],
104            params: none!(),
105        }
106    }
107    pub fn with(title: &'static str, values: impl IntoIterator<Item = String>) -> Self {
108        ArmorHeader {
109            title: title.to_owned(),
110            values: values.into_iter().collect(),
111            params: none!(),
112        }
113    }
114}
115
116impl Display for ArmorHeader {
117    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
118        let pfx = if self.values.len() == 1 { " " } else { "\n\t" };
119        write!(f, "{}:{}", self.title, pfx)?;
120        for (i, val) in self.values.iter().enumerate() {
121            if i > 0 {
122                f.write_str(",\n\t")?;
123            }
124            write!(f, "{}", val)?;
125        }
126
127        for (name, val) in &self.params {
128            write!(f, ";\n\t{name}={val}")?;
129        }
130        Ok(())
131    }
132}
133
134#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
135#[display(doc_comments)]
136pub enum ArmorParseError {
137    /// armored header has invalid format ("{0}").
138    InvalidHeaderFormat(String),
139
140    /// armored header '{0}' has invalid parameter '{1}'.
141    InvalidHeaderParam(String, String),
142
143    /// the provided text doesn't contain recognizable ASCII-armored encoding.
144    WrongStructure,
145
146    /// ASCII armor data has invalid Base85 encoding.
147    Base85,
148
149    /// ASCII armor data has invalid Base64 encoding.
150    Base64,
151
152    /// header providing checksum for the armored data must not contain additional
153    /// parameters.
154    NonEmptyChecksumParams,
155
156    /// multiple checksum headers provided.
157    MultipleChecksums,
158
159    /// ASCII armor contains unparsable checksum. Details: {0}
160    #[from]
161    UnparsableChecksum(hex::Error),
162
163    /// ASCII armor checksum doesn't match the actual data.
164    MismatchedChecksum,
165
166    /// unrecognized header '{0}' in ASCII armor.
167    UnrecognizedHeader(String),
168}
169
170impl FromStr for ArmorHeader {
171    type Err = ArmorParseError;
172
173    fn from_str(s: &str) -> Result<Self, Self::Err> {
174        let (title, rest) =
175            s.split_once(':').ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?;
176        let rest = rest.trim();
177        let mut split = rest.split(';');
178        let values =
179            split.next().ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?.trim();
180        let mut params = vec![];
181        for param in split {
182            let (name, val) = s.split_once('=').ok_or_else(|| {
183                ArmorParseError::InvalidHeaderParam(title.to_owned(), param.to_owned())
184            })?;
185            params.push((name.trim().to_owned(), val.trim().to_owned()));
186        }
187        let values = values.split(',').map(|val| val.trim().to_owned()).collect();
188        Ok(ArmorHeader {
189            title: title.to_owned(),
190            values,
191            params,
192        })
193    }
194}
195
196pub trait AsciiArmor: Sized {
197    type Err: Debug + From<ArmorParseError>;
198
199    const PLATE_TITLE: &'static str;
200
201    fn to_ascii_armored_string(&self) -> String { format!("{}", self.display_ascii_armored()) }
202    fn display_ascii_armored(&self) -> DisplayAsciiArmored<'_, Self> { DisplayAsciiArmored(self) }
203    fn ascii_armored_headers(&self) -> Vec<ArmorHeader> { none!() }
204    fn ascii_armored_digest(&self) -> Option<Bytes32> { DisplayAsciiArmored(self).data_digest().1 }
205    fn to_ascii_armored_data(&self) -> Vec<u8>;
206
207    fn from_ascii_armored_str(s: &str) -> Result<Self, Self::Err> {
208        let first = format!("-----BEGIN {}-----", Self::PLATE_TITLE);
209        let last = format!("-----END {}-----", Self::PLATE_TITLE);
210
211        let mut lines = s.lines().skip_while(|line| line != &first);
212        lines.next();
213        let mut checksum = None;
214        let mut headers = vec![];
215        for line in lines.by_ref() {
216            if line.is_empty() {
217                break;
218            }
219            let header = ArmorHeader::from_str(line)?;
220            if header.title == ASCII_ARMOR_CHECKSUM_SHA256 {
221                if !header.params.is_empty() {
222                    return Err(ArmorParseError::NonEmptyChecksumParams.into());
223                }
224                if header.values.is_empty() || header.values.len() > 1 {
225                    return Err(ArmorParseError::MultipleChecksums.into());
226                }
227                checksum = Some(header.values[0].to_owned());
228            } else {
229                headers.push(header);
230            }
231        }
232        let armor = lines.take_while(|line| line != &last).collect::<String>();
233        if armor.trim().is_empty() {
234            return Err(ArmorParseError::WrongStructure.into());
235        }
236        #[cfg(feature = "base85")]
237        let data = base85::decode(&armor).map_err(|_| ArmorParseError::Base85)?;
238        #[cfg(feature = "baid64")]
239        let data = {
240            use baid64::base64::Engine;
241            baid64::base64::prelude::BASE64_STANDARD
242                .decode(&armor)
243                .map_err(|_| ArmorParseError::Base64)?
244        };
245        if let Some(checksum) = checksum {
246            let checksum =
247                Bytes32::from_str(&checksum).map_err(ArmorParseError::UnparsableChecksum)?;
248            let expected = Bytes32::from_byte_array(Sha256::digest(&data));
249            if checksum != expected {
250                return Err(ArmorParseError::MismatchedChecksum.into());
251            }
252        }
253        let me = Self::with_headers_data(headers, data)?;
254        Ok(me)
255    }
256
257    fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err>;
258}
259
260#[cfg(feature = "strict")]
261#[derive(Debug, Display, Error, From)]
262#[display(doc_comments)]
263pub enum StrictArmorError {
264    /// ASCII armor misses required Id header.
265    MissedId,
266
267    /// multiple Id headers.
268    MultipleIds,
269
270    /// Id header of the ASCII armor contains unparsable information. Details: {0}
271    #[from]
272    InvalidId(baid64::Baid64ParseError),
273
274    /// the actual ASCII armor doesn't match the provided id.
275    ///
276    /// Actual id: {actual}.
277    ///
278    /// Expected id: {expected}.
279    MismatchedId { actual: String, expected: String },
280
281    /// unable to decode the provided ASCII armor. Details: {0}
282    #[from]
283    Deserialize(strict_encoding::DeserializeError),
284
285    /// ASCII armor contains more than 16MB of data.
286    #[from(confinement::Error)]
287    TooLarge,
288
289    #[from]
290    #[display(inner)]
291    Armor(ArmorParseError),
292}
293
294#[cfg(feature = "strict")]
295pub trait StrictArmor: StrictSerialize + StrictDeserialize {
296    type Id: Copy + Eq + Debug + Display + FromStr<Err = baid64::Baid64ParseError>;
297
298    const PLATE_TITLE: &'static str;
299
300    fn armor_id(&self) -> Self::Id;
301    fn checksum_armor(&self) -> bool { false }
302    fn armor_headers(&self) -> Vec<ArmorHeader> { none!() }
303    fn parse_armor_headers(&mut self, _headers: Vec<ArmorHeader>) -> Result<(), StrictArmorError> {
304        Ok(())
305    }
306}
307
308#[cfg(feature = "strict")]
309impl<T> AsciiArmor for T
310where T: StrictArmor
311{
312    type Err = StrictArmorError;
313    const PLATE_TITLE: &'static str = <T as StrictArmor>::PLATE_TITLE;
314
315    fn ascii_armored_headers(&self) -> Vec<ArmorHeader> {
316        let mut headers = vec![ArmorHeader::new(ASCII_ARMOR_ID, self.armor_id().to_string())];
317        headers.extend(self.armor_headers());
318        headers
319    }
320
321    fn to_ascii_armored_data(&self) -> Vec<u8> {
322        self.to_strict_serialized::<U24MAX>().expect("data too large for ASCII armoring").release()
323    }
324
325    fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err> {
326        let id =
327            headers.iter().find(|h| h.title == ASCII_ARMOR_ID).ok_or(StrictArmorError::MissedId)?;
328        // Proceed and check id
329        if id.values.is_empty() || id.values.len() > 1 {
330            return Err(StrictArmorError::MultipleIds);
331        }
332        let expected = T::Id::from_str(&id.values[0]).map_err(StrictArmorError::from)?;
333        let data = Confined::try_from(data).map_err(StrictArmorError::from)?;
334        let mut me =
335            Self::from_strict_serialized::<U24MAX>(data).map_err(StrictArmorError::from)?;
336        me.parse_armor_headers(headers)?;
337        let actual = me.armor_id();
338        if expected != actual {
339            return Err(StrictArmorError::MismatchedId {
340                expected: expected.to_string(),
341                actual: actual.to_string(),
342            });
343        }
344        Ok(me)
345    }
346}
347
348impl AsciiArmor for Vec<u8> {
349    type Err = ArmorParseError;
350    const PLATE_TITLE: &'static str = "DATA";
351
352    fn to_ascii_armored_data(&self) -> Vec<u8> { self.clone() }
353
354    fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err> {
355        assert!(headers.is_empty());
356        Ok(data)
357    }
358}
359
360#[cfg(test)]
361mod test {
362    use super::*;
363
364    #[test]
365    fn roundtrip() {
366        let noise = Sha256::digest("some test data");
367        let data = noise.as_slice().repeat(100).to_vec();
368        let armor = data.to_ascii_armored_string();
369        let data2 = Vec::<u8>::from_ascii_armored_str(&armor).unwrap();
370        let armor2 = data2.to_ascii_armored_string();
371        assert_eq!(data, data2);
372        assert_eq!(armor, armor2);
373    }
374
375    #[test]
376    fn format() {
377        let noise = Sha256::digest("some test data");
378        let data = noise.as_slice().repeat(100).to_vec();
379        let armored_context = data.to_ascii_armored_string();
380        let mut lines = armored_context.lines();
381        let mut current = lines.next().unwrap_or_default();
382        assert_eq!(current, "-----BEGIN DATA-----");
383        for line in lines {
384            assert!(line.len() <= 80, "a line should less than or equal 80 chars");
385            current = line;
386        }
387        assert_eq!(current, "-----END DATA-----");
388    }
389
390    #[cfg(feature = "strict")]
391    #[test]
392    fn strict_format() {
393        use strict_encoding::{DefaultBasedStrictDumb, StrictDecode, StrictEncode, StrictType};
394
395        #[derive(
396            Copy,
397            Clone,
398            Debug,
399            Default,
400            Display,
401            Eq,
402            StrictType,
403            StrictEncode,
404            StrictDecode,
405            PartialEq
406        )]
407        #[strict_type(lib = "ARMORtest")]
408        #[display("{inner}")]
409        pub struct Sid {
410            inner: u8,
411        }
412        impl DefaultBasedStrictDumb for Sid {}
413        impl FromStr for Sid {
414            type Err = baid64::Baid64ParseError;
415            fn from_str(s: &str) -> Result<Self, Self::Err> {
416                Ok(Self {
417                    inner: s.len() as u8,
418                })
419            }
420        }
421
422        #[derive(Default, Debug, StrictType, StrictEncode, StrictDecode, PartialEq)]
423        #[strict_type(lib = "ARMORtest")]
424        struct S {
425            inner: u8,
426        }
427        impl DefaultBasedStrictDumb for S {}
428        impl StrictSerialize for S {}
429        impl StrictDeserialize for S {}
430        impl StrictArmor for S {
431            const PLATE_TITLE: &'static str = "S";
432            type Id = Sid;
433            fn armor_id(&self) -> Self::Id { Default::default() }
434        }
435
436        let s = S::default();
437        let display_ascii_armored = s.display_ascii_armored();
438
439        #[cfg(feature = "base85")]
440        assert_eq!(
441            s.to_ascii_armored_string(),
442            format!(
443                r#"-----BEGIN S-----
444Id: 0
445Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
446
44700
448
449-----END S-----
450"#
451            )
452        );
453        #[cfg(feature = "baid64")]
454        assert_eq!(
455            s.to_ascii_armored_string(),
456            format!(
457                r#"-----BEGIN S-----
458Id: 0
459Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
460
461AA==
462
463-----END S-----
464"#
465            )
466        );
467
468        assert_eq!(display_ascii_armored.data_digest().0, vec![0]);
469
470        // check checksum error will raise
471        // 6e34....a01e is one bit more than 6e34....a01d
472        #[cfg(feature = "base85")]
473        assert!(
474            S::from_ascii_armored_str(
475                r#"-----BEGIN S-----
476Id: 0
477Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
478
47900
480
481-----END S-----
482"#
483            )
484            .is_err()
485        );
486        #[cfg(feature = "baid64")]
487        assert!(
488            S::from_ascii_armored_str(
489                r#"-----BEGIN S-----
490Id: 0
491Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
492
493AA==
494
495-----END S-----
496"#
497            )
498            .is_err()
499        );
500
501        // check id error will raise
502        // 1 is one bit more than 0
503        #[cfg(feature = "base85")]
504        assert!(
505            S::from_ascii_armored_str(&format!(
506                r#"-----BEGIN S-----
507Id: 1
508Check-SHA256: {}
509
51000
511
512-----END S-----
513"#,
514                display_ascii_armored.data_digest().1.unwrap_or_default()
515            ))
516            .is_err()
517        );
518        #[cfg(feature = "baid64")]
519        assert!(
520            S::from_ascii_armored_str(&format!(
521                r#"-----BEGIN S-----
522Id: 1
523Check-SHA256: {}
524
525AA==
526
527-----END S-----
528"#,
529                display_ascii_armored.data_digest().1.unwrap_or_default()
530            ))
531            .is_err()
532        );
533    }
534}