Skip to main content

jmap_cid_types/
blobext.rs

1//! Base64 conversion for [`Sha256`], for comparison with the RFC 9404
2//! (JMAP Blob Management Extensions) `digest:sha-256` property.
3//!
4//! draft-atwood-jmap-cid-00 §Relationship to JMAP Blob Management
5//! Extensions says the CID `sha256` field "uses lowercase hex encoding
6//! rather than base64", and that clients may cross-validate the two
7//! "provided they convert encodings before comparing". RFC 9404 §4.1
8//! defines `digest:<algorithm>` as "The base64 encoding of the digest",
9//! with base64 as specified in RFC 4648: the §4 alphabet with `=`
10//! padding. A 32-byte SHA-256 digest is always 44 base64 characters.
11//!
12//! The reverse path accepts only canonical padded base64. Missing
13//! padding, the §5 URL-safe alphabet, embedded whitespace and non-zero
14//! trailing bits are all rejected with [`Sha256Base64Error`].
15
16use crate::Sha256;
17use base64::Engine as _;
18use base64::engine::general_purpose::STANDARD;
19use std::fmt;
20
21/// Error from [`Sha256::from_base64`].
22///
23/// `#[non_exhaustive]` at the type level and per variant, so new
24/// variants and new fields stay semver-additive.
25#[non_exhaustive]
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Sha256Base64Error {
28    /// The string is not canonical padded RFC 4648 §4 base64.
29    #[non_exhaustive]
30    InvalidBase64,
31    /// The string decodes to something other than 32 bytes.
32    #[non_exhaustive]
33    WrongLength {
34        /// The decoded byte length, saturated at `u32::MAX`.
35        got: u32,
36    },
37}
38
39impl fmt::Display for Sha256Base64Error {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::InvalidBase64 => f.write_str("SHA-256 digest is not canonical padded base64"),
43            Self::WrongLength { got } => {
44                write!(
45                    f,
46                    "base64 SHA-256 digest must decode to 32 bytes (got {got})"
47                )
48            }
49        }
50    }
51}
52
53impl std::error::Error for Sha256Base64Error {}
54
55impl Sha256 {
56    /// The digest as padded RFC 4648 §4 base64, the encoding of the
57    /// RFC 9404 `digest:sha-256` property.
58    pub fn to_base64(&self) -> String {
59        STANDARD.encode(self.to_bytes())
60    }
61
62    /// Parse a padded RFC 4648 §4 base64 digest, such as an RFC 9404
63    /// `digest:sha-256` value, into its [`Sha256`].
64    pub fn from_base64(s: &str) -> Result<Self, Sha256Base64Error> {
65        let bytes = STANDARD
66            .decode(s)
67            .map_err(|_| Sha256Base64Error::InvalidBase64)?;
68        let digest: [u8; 32] =
69            bytes
70                .as_slice()
71                .try_into()
72                .map_err(|_| Sha256Base64Error::WrongLength {
73                    got: u32::try_from(bytes.len()).unwrap_or(u32::MAX),
74                })?;
75        Ok(Self::from(digest))
76    }
77}