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 = "base64", feature = "base85")))]
23compile_error!("either base64 or base85 feature must be specified, you provide none of them.");
24
25#[cfg(all(feature = "base64", feature = "base85"))]
26compile_error!("either base64 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;
36use amplify::confinement::{self, Confined};
37use amplify::num::u24;
38use amplify::{Bytes32, hex};
39use sha2::{Digest, Sha256};
40#[cfg(feature = "strict")]
41use strict_encoding::{StrictDeserialize, StrictSerialize};
42
43pub const ASCII_ARMOR_MAX_LEN: usize = u24::MAX.to_usize();
44pub const ASCII_ARMOR_ID: &str = "Id";
45pub const ASCII_ARMOR_CHECKSUM_SHA256: &str = "Check-SHA256";
46
47pub struct DisplayAsciiArmored<'a, A: AsciiArmor>(&'a A);
48
49impl<'a, A: AsciiArmor> DisplayAsciiArmored<'a, A> {
50    fn data_digest(&self) -> (Vec<u8>, Option<Bytes32>) {
51        let data = self.0.to_ascii_armored_data();
52        let digest = Sha256::digest(&data);
53        (data, Some(Bytes32::from_byte_array(digest)))
54    }
55}
56
57impl<'a, A: AsciiArmor> Display for DisplayAsciiArmored<'a, A> {
58    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
59        writeln!(f, "-----BEGIN {}-----", A::PLATE_TITLE)?;
60
61        let (data, digest) = self.data_digest();
62        for header in self.0.ascii_armored_headers() {
63            writeln!(f, "{header}")?;
64        }
65        if let Some(digest) = digest {
66            writeln!(f, "{ASCII_ARMOR_CHECKSUM_SHA256}: {digest}")?;
67        }
68        writeln!(f)?;
69
70        #[cfg(feature = "base85")]
71        let data = base85::encode(&data);
72        #[cfg(feature = "base64")]
73        let data = {
74            use base64::Engine;
75            base64::prelude::BASE64_STANDARD.encode(&data)
76        };
77        let mut data = data.as_str();
78        while data.len() >= 80 {
79            let (line, rest) = data.split_at(80);
80            writeln!(f, "{}", line)?;
81            data = rest;
82        }
83        writeln!(f, "{}", data)?;
84
85        writeln!(f, "\n-----END {}-----", A::PLATE_TITLE)?;
86
87        Ok(())
88    }
89}
90
91#[derive(Clone, Eq, PartialEq, Hash, Debug)]
92pub struct ArmorHeader {
93    pub title: String,
94    pub values: Vec<String>,
95    pub params: Vec<(String, String)>,
96}
97
98impl ArmorHeader {
99    pub fn new(title: &'static str, value: String) -> Self {
100        ArmorHeader {
101            title: title.to_owned(),
102            values: vec![value],
103            params: none!(),
104        }
105    }
106    pub fn with(title: &'static str, values: impl IntoIterator<Item = String>) -> Self {
107        ArmorHeader {
108            title: title.to_owned(),
109            values: values.into_iter().collect(),
110            params: none!(),
111        }
112    }
113}
114
115impl Display for ArmorHeader {
116    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117        let pfx = if self.values.len() == 1 { " " } else { "\n\t" };
118        write!(f, "{}:{}", self.title, pfx)?;
119        for (i, val) in self.values.iter().enumerate() {
120            if i > 0 {
121                f.write_str(",\n\t")?;
122            }
123            write!(f, "{}", val)?;
124        }
125
126        for (name, val) in &self.params {
127            write!(f, ";\n\t{name}={val}")?;
128        }
129        Ok(())
130    }
131}
132
133#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
134#[display(doc_comments)]
135pub enum ArmorParseError {
136    /// armored header has invalid format ("{0}").
137    InvalidHeaderFormat(String),
138
139    /// armored header '{0}' has invalid parameter '{1}'.
140    InvalidHeaderParam(String, String),
141
142    /// the provided text doesn't contain recognizable ASCII-armored encoding.
143    WrongStructure,
144
145    /// ASCII armor data has invalid Base85 encoding.
146    Base85,
147
148    /// ASCII armor data has invalid Base64 encoding.
149    Base64,
150
151    /// header providing checksum for the armored data must not contain additional
152    /// parameters.
153    NonEmptyChecksumParams,
154
155    /// multiple checksum headers provided.
156    MultipleChecksums,
157
158    /// ASCII armor contains unparsable checksum. Details: {0}
159    #[from]
160    UnparsableChecksum(hex::Error),
161
162    /// ASCII armor checksum doesn't match the actual data.
163    MismatchedChecksum,
164
165    /// unrecognized header '{0}' in ASCII armor.
166    UnrecognizedHeader(String),
167}
168
169impl FromStr for ArmorHeader {
170    type Err = ArmorParseError;
171
172    fn from_str(s: &str) -> Result<Self, Self::Err> {
173        let (title, rest) =
174            s.split_once(':').ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?;
175        let rest = rest.trim();
176        let mut split = rest.split(';');
177        let values =
178            split.next().ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?.trim();
179        let mut params = vec![];
180        for param in split {
181            let (name, val) = s.split_once('=').ok_or_else(|| {
182                ArmorParseError::InvalidHeaderParam(title.to_owned(), param.to_owned())
183            })?;
184            params.push((name.trim().to_owned(), val.trim().to_owned()));
185        }
186        let values = values.split(',').map(|val| val.trim().to_owned()).collect();
187        Ok(ArmorHeader {
188            title: title.to_owned(),
189            values,
190            params,
191        })
192    }
193}
194
195pub trait AsciiArmor: Sized {
196    type Err: Debug + From<ArmorParseError>;
197
198    const PLATE_TITLE: &'static str;
199
200    fn to_ascii_armored_string(&self) -> String { format!("{}", self.display_ascii_armored()) }
201    fn display_ascii_armored(&self) -> DisplayAsciiArmored<Self> { DisplayAsciiArmored(self) }
202    fn ascii_armored_headers(&self) -> Vec<ArmorHeader> { none!() }
203    fn ascii_armored_digest(&self) -> Option<Bytes32> { DisplayAsciiArmored(self).data_digest().1 }
204    fn to_ascii_armored_data(&self) -> Vec<u8>;
205
206    fn from_ascii_armored_str(s: &str) -> Result<Self, Self::Err> {
207        let first = format!("-----BEGIN {}-----", Self::PLATE_TITLE);
208        let last = format!("-----END {}-----", Self::PLATE_TITLE);
209
210        let mut lines = s.lines().skip_while(|line| line != &first);
211        lines.next();
212        let mut checksum = None;
213        let mut headers = vec![];
214        for line in lines.by_ref() {
215            if line.is_empty() {
216                break;
217            }
218            let header = ArmorHeader::from_str(line)?;
219            if header.title == ASCII_ARMOR_CHECKSUM_SHA256 {
220                if !header.params.is_empty() {
221                    return Err(ArmorParseError::NonEmptyChecksumParams.into());
222                }
223                if header.values.is_empty() || header.values.len() > 1 {
224                    return Err(ArmorParseError::MultipleChecksums.into());
225                }
226                checksum = Some(header.values[0].to_owned());
227            } else {
228                headers.push(header);
229            }
230        }
231        let armor = lines.take_while(|line| line != &last).collect::<String>();
232        if armor.trim().is_empty() {
233            return Err(ArmorParseError::WrongStructure.into());
234        }
235        #[cfg(feature = "base85")]
236        let data = base85::decode(&armor).map_err(|_| ArmorParseError::Base85)?;
237        #[cfg(feature = "base64")]
238        let data = {
239            use baid64::base64::Engine;
240            baid64::base64::prelude::BASE64_STANDARD
241                .decode(&armor)
242                .map_err(|_| ArmorParseError::Base64)?
243        };
244        if let Some(checksum) = checksum {
245            let checksum =
246                Bytes32::from_str(&checksum).map_err(ArmorParseError::UnparsableChecksum)?;
247            let expected = Bytes32::from_byte_array(Sha256::digest(&data));
248            if checksum != expected {
249                return Err(ArmorParseError::MismatchedChecksum.into());
250            }
251        }
252        let me = Self::with_headers_data(headers, data)?;
253        Ok(me)
254    }
255
256    fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err>;
257}
258
259#[cfg(feature = "strict")]
260#[derive(Debug, Display, Error, From)]
261#[display(doc_comments)]
262pub enum StrictArmorError {
263    /// ASCII armor misses required Id header.
264    MissedId,
265
266    /// multiple Id headers.
267    MultipleIds,
268
269    #[cfg(feature = "baid64")]
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::{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 FromStr for Sid {
413            type Err = baid64::Baid64ParseError;
414            fn from_str(s: &str) -> Result<Self, Self::Err> {
415                Ok(Self {
416                    inner: s.len() as u8,
417                })
418            }
419        }
420
421        #[derive(Default, Debug, StrictType, StrictEncode, StrictDecode, PartialEq)]
422        #[strict_type(lib = "ARMORtest")]
423        struct S {
424            inner: u8,
425        }
426        impl StrictSerialize for S {}
427        impl StrictDeserialize for S {}
428        impl StrictArmor for S {
429            const PLATE_TITLE: &'static str = "S";
430            type Id = Sid;
431            fn armor_id(&self) -> Self::Id { Default::default() }
432        }
433
434        let s = S::default();
435        let display_ascii_armored = s.display_ascii_armored();
436
437        #[cfg(feature = "base85")]
438        assert_eq!(
439            s.to_ascii_armored_string(),
440            format!(
441                r#"-----BEGIN S-----
442Id: 0
443Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
444
44500
446
447-----END S-----
448"#
449            )
450        );
451        #[cfg(feature = "base64")]
452        assert_eq!(
453            s.to_ascii_armored_string(),
454            format!(
455                r#"-----BEGIN S-----
456Id: 0
457Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
458
459AA==
460
461-----END S-----
462"#
463            )
464        );
465
466        assert_eq!(display_ascii_armored.data_digest().0, vec![0]);
467
468        // check checksum error will raise
469        // 6e34....a01e is one bit more than 6e34....a01d
470        #[cfg(feature = "base85")]
471        assert!(
472            S::from_ascii_armored_str(
473                r#"-----BEGIN S-----
474Id: 0
475Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
476
47700
478
479-----END S-----
480"#
481            )
482            .is_err()
483        );
484        #[cfg(feature = "base64")]
485        assert!(
486            S::from_ascii_armored_str(
487                r#"-----BEGIN S-----
488Id: 0
489Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
490
491AA==
492
493-----END S-----
494"#
495            )
496            .is_err()
497        );
498
499        // check id error will raise
500        // 1 is one bit more than 0
501        #[cfg(feature = "base85")]
502        assert!(
503            S::from_ascii_armored_str(&format!(
504                r#"-----BEGIN S-----
505Id: 1
506Check-SHA256: {}
507
50800
509
510-----END S-----
511"#,
512                display_ascii_armored.data_digest().1.unwrap_or_default()
513            ))
514            .is_err()
515        );
516        #[cfg(feature = "base64")]
517        assert!(
518            S::from_ascii_armored_str(&format!(
519                r#"-----BEGIN S-----
520Id: 1
521Check-SHA256: {}
522
523AA==
524
525-----END S-----
526"#,
527                display_ascii_armored.data_digest().1.unwrap_or_default()
528            ))
529            .is_err()
530        );
531    }
532}