diff --git a/Cargo.lock b/Cargo.lock index e6e86020a..f1142e2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -976,6 +976,7 @@ dependencies = [ "hex-literal", "pem-rfc7468", "sec1", + "sha2", "subtle", "tempfile", "zeroize", diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index d16b2589f..c4fc15b84 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -22,6 +22,7 @@ zeroize = { version = "1", default-features = false } # optional dependencies sec1 = { version = "=0.3.0-pre.1", optional = true, default-features = false, path = "../sec1" } +sha2 = { version = "0.10", optional = true, default-features = false } subtle = { version = "2", optional = true, default-features = false } [dev-dependencies] @@ -29,9 +30,10 @@ hex-literal = "0.3" tempfile = "3" [features] -default = ["ecdsa", "std"] +default = ["ecdsa", "fingerprint", "std"] alloc = ["zeroize/alloc"] ecdsa = ["sec1"] +fingerprint = ["sha2"] std = ["alloc", "base64ct/std"] [package.metadata.docs.rs] diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 802b00646..0318de0d2 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -7,6 +7,9 @@ use crate::{ }; use core::{fmt, str}; +/// SHA-256 hash function. +const SHA256: &str = "SHA256"; + /// ECDSA with SHA-256 + NIST P-256 const ECDSA_SHA2_P256: &str = "ecdsa-sha2-nistp256"; @@ -25,10 +28,10 @@ const SSH_ED25519: &str = "ssh-ed25519"; /// RSA const SSH_RSA: &str = "ssh-rsa"; -/// String-like fields. +/// String identifiers for cryptographic algorithms. /// -/// These fields receive a blanket impl of [`Decode`] and [`Encode`]. -pub(crate) trait StrField: AsRef + str::FromStr { +/// Receives a blanket impl of [`Decode`] and [`Encode`]. +pub(crate) trait AlgString: AsRef + str::FromStr { /// Decoding buffer type. /// /// This needs to be a byte array large enough to fit the largest @@ -36,14 +39,14 @@ pub(crate) trait StrField: AsRef + str::FromStr { type DecodeBuf: AsMut<[u8]> + Default; } -impl Decode for T { +impl Decode for T { fn decode(decoder: &mut impl Decoder) -> Result { let mut buf = T::DecodeBuf::default(); decoder.decode_str(buf.as_mut())?.parse() } } -impl Encode for T { +impl Encode for T { fn encoded_len(&self) -> Result { Ok(4 + self.as_ref().len()) } @@ -134,7 +137,7 @@ impl AsRef for Algorithm { } } -impl StrField for Algorithm { +impl AlgString for Algorithm { type DecodeBuf = [u8; 20]; // max length: "ecdsa-sha2-nistpXXX" } @@ -186,7 +189,7 @@ impl AsRef for CipherAlg { } } -impl StrField for CipherAlg { +impl AlgString for CipherAlg { type DecodeBuf = [u8; 4]; // max length: 'none' } @@ -250,7 +253,7 @@ impl AsRef for EcdsaCurve { } } -impl StrField for EcdsaCurve { +impl AlgString for EcdsaCurve { type DecodeBuf = [u8; 8]; // max length: 'nistpXXX' } @@ -268,6 +271,60 @@ impl str::FromStr for EcdsaCurve { } } +/// Hashing algorithms a.k.a. digest functions. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum HashAlg { + /// SHA-256 + Sha256, +} + +impl HashAlg { + /// Decode elliptic curve from the given string identifier. + /// + /// # Supported hash algorithms + /// + /// - `SHA256` + pub fn new(id: &str) -> Result { + match id { + SHA256 => Ok(HashAlg::Sha256), + _ => Err(Error::Algorithm), + } + } + + /// Get the string identifier for this hash algorithm. + pub fn as_str(self) -> &'static str { + match self { + HashAlg::Sha256 => SHA256, + } + } +} + +impl AsRef for HashAlg { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Default for HashAlg { + fn default() -> Self { + HashAlg::Sha256 + } +} + +impl fmt::Display for HashAlg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl str::FromStr for HashAlg { + type Err = Error; + + fn from_str(id: &str) -> Result { + HashAlg::new(id) + } +} + /// Key Derivation Function (KDF) algorithms. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] #[non_exhaustive] @@ -302,7 +359,7 @@ impl AsRef for KdfAlg { } } -impl StrField for KdfAlg { +impl AlgString for KdfAlg { type DecodeBuf = [u8; 4]; // max length: 'none' } diff --git a/ssh-key/src/decoder.rs b/ssh-key/src/decoder.rs index 715c66dba..c233648dd 100644 --- a/ssh-key/src/decoder.rs +++ b/ssh-key/src/decoder.rs @@ -23,7 +23,7 @@ pub(crate) trait Decode: Sized { /// Decoder extension trait. pub(crate) trait Decoder { - /// Decode as much Base64 as is needed to exactly fill `out`. + /// Decode as much data as is needed to exactly fill `out`. /// /// This is the base decoding method on which the rest of the trait is /// implemented in terms of. @@ -31,7 +31,7 @@ pub(crate) trait Decoder { /// # Returns /// - `Ok(bytes)` if the expected amount of data was read /// - `Err(Error::Length)` if the exact amount of data couldn't be read - fn decode_base64<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]>; + fn decode_raw<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]>; /// Get the length of the remaining data after Base64 decoding. fn decoded_len(&self) -> usize; @@ -43,7 +43,7 @@ pub(crate) trait Decoder { #[cfg(feature = "ecdsa")] fn decode_u8(&mut self) -> Result { let mut buf = [0]; - self.decode_base64(&mut buf)?; + self.decode_raw(&mut buf)?; Ok(buf[0]) } @@ -56,7 +56,7 @@ pub(crate) trait Decoder { /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 fn decode_u32(&mut self) -> Result { let mut bytes = [0u8; 4]; - self.decode_base64(&mut bytes)?; + self.decode_raw(&mut bytes)?; Ok(u32::from_be_bytes(bytes)) } @@ -84,7 +84,7 @@ pub(crate) trait Decoder { fn decode_byte_slice<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { let len = self.decode_usize()?; let result = out.get_mut(..len).ok_or(Error::Length)?; - self.decode_base64(result)?; + self.decode_raw(result)?; Ok(result) } @@ -99,7 +99,7 @@ pub(crate) trait Decoder { fn decode_byte_vec(&mut self) -> Result> { let len = self.decode_usize()?; let mut result = vec![0u8; len]; - self.decode_base64(&mut result)?; + self.decode_raw(&mut result)?; Ok(result) } @@ -134,7 +134,7 @@ pub(crate) trait Decoder { fn drain(&mut self, n_bytes: usize) -> Result<()> { let mut byte = [0]; for _ in 0..n_bytes { - self.decode_base64(&mut byte)?; + self.decode_raw(&mut byte)?; } Ok(()) } @@ -147,7 +147,7 @@ pub(crate) trait Decoder { } impl Decoder for Base64Decoder<'_> { - fn decode_base64<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { + fn decode_raw<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { Ok(self.decode(out)?) } @@ -161,7 +161,7 @@ impl Decoder for Base64Decoder<'_> { } impl Decoder for pem::Decoder<'_> { - fn decode_base64<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { + fn decode_raw<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { Ok(self.decode(out)?) } diff --git a/ssh-key/src/encoder.rs b/ssh-key/src/encoder.rs index 645c2af69..3b5acd953 100644 --- a/ssh-key/src/encoder.rs +++ b/ssh-key/src/encoder.rs @@ -6,6 +6,9 @@ use crate::Result; use core::str; use pem_rfc7468 as pem; +#[cfg(feature = "fingerprint")] +use sha2::{Digest, Sha256}; + /// Get the estimated length of data when encoded as Base64. /// /// This is an upper bound where the actual length might be slightly shorter. @@ -28,11 +31,11 @@ pub(crate) trait Encode: Sized { /// Encoder extension trait. pub(crate) trait Encoder { - /// Encode the given byte slice as Base64. + /// Encode the given byte slice containing raw unstructured data. /// /// This is the base encoding method on which the rest of the trait is /// implemented in terms of. - fn encode_base64(&mut self, bytes: &[u8]) -> Result<()>; + fn encode_raw(&mut self, bytes: &[u8]) -> Result<()>; /// Encode a `uint32` as described in [RFC4251 § 5]: /// @@ -42,7 +45,7 @@ pub(crate) trait Encoder { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 fn encode_u32(&mut self, num: u32) -> Result<()> { - self.encode_base64(&num.to_be_bytes()) + self.encode_raw(&num.to_be_bytes()) } /// Encode a `usize` as a `uint32` as described in [RFC4251 § 5]. @@ -64,7 +67,7 @@ pub(crate) trait Encoder { /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 fn encode_byte_slice(&mut self, bytes: &[u8]) -> Result<()> { self.encode_usize(bytes.len())?; - self.encode_base64(bytes) + self.encode_raw(bytes) } /// Encode a `string` as described in [RFC4251 § 5]: @@ -90,13 +93,21 @@ pub(crate) trait Encoder { } impl Encoder for Base64Encoder<'_> { - fn encode_base64(&mut self, bytes: &[u8]) -> Result<()> { + fn encode_raw(&mut self, bytes: &[u8]) -> Result<()> { Ok(self.encode(bytes)?) } } impl Encoder for pem::Encoder<'_, '_> { - fn encode_base64(&mut self, bytes: &[u8]) -> Result<()> { + fn encode_raw(&mut self, bytes: &[u8]) -> Result<()> { Ok(self.encode(bytes)?) } } + +#[cfg(feature = "fingerprint")] +impl Encoder for Sha256 { + fn encode_raw(&mut self, bytes: &[u8]) -> Result<()> { + self.update(bytes); + Ok(()) + } +} diff --git a/ssh-key/src/fingerprint.rs b/ssh-key/src/fingerprint.rs new file mode 100644 index 000000000..3474b2a5c --- /dev/null +++ b/ssh-key/src/fingerprint.rs @@ -0,0 +1,153 @@ +//! SSH public key fingerprints. + +use crate::{encoder::Encode, Error, HashAlg, PublicKey, Result}; +use base64ct::{Base64Unpadded, Encoding}; +use core::{fmt, str}; +use sha2::{Digest, Sha256}; + +/// Error message for malformed encoded strings which are expected to be +/// well-formed according to type-level invariants. +const ENCODING_ERR_MSG: &str = "Base64 encoding error"; + +/// Size of a SHA-256 hash encoded as Base64. +const SHA256_BASE64_LEN: usize = 43; + +/// Size of a SHA-256 hash serialized as binary. +const SHA256_BIN_LEN: usize = 32; + +/// Prefix of SHA-256 fingerprints. +const SHA256_PREFIX: &str = "SHA256:"; + +/// SSH public key fingerprints. +/// +/// Fingerprints have an associated key fingerprint algorithm, i.e. a hash +/// function which is used to compute the fingerprint. +#[cfg_attr(docsrs, doc(cfg(feature = "fingerprint")))] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub enum Fingerprint { + /// Fingerprints computed using SHA-256. + Sha256(Sha256Fingerprint), +} + +impl Fingerprint { + /// Get the hash algorithm used for this fingerprint. + pub fn algorithm(self) -> HashAlg { + match self { + Self::Sha256(_) => HashAlg::Sha256, + } + } + + /// Get the SHA-256 fingerprint, if this is one. + pub fn sha256(self) -> Option { + match self { + Self::Sha256(fingerprint) => Some(fingerprint), + } + } + + /// Is this fingerprint SHA-256? + pub fn is_sha256(self) -> bool { + self.sha256().is_some() + } +} + +impl From for Fingerprint { + fn from(fingerprint: Sha256Fingerprint) -> Fingerprint { + Fingerprint::Sha256(fingerprint) + } +} + +impl str::FromStr for Fingerprint { + type Err = Error; + + fn from_str(id: &str) -> Result { + if id.starts_with(SHA256_PREFIX) { + Sha256Fingerprint::from_str(id).map(Into::into) + } else { + Err(Error::Algorithm) + } + } +} + +impl fmt::Display for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Sha256(fingerprint) => write!(f, "{}", fingerprint), + } + } +} + +/// SSH key fingerprints calculated using the SHA-256 hash function. +#[cfg_attr(docsrs, doc(cfg(feature = "fingerprint")))] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct Sha256Fingerprint([u8; SHA256_BASE64_LEN]); + +impl Sha256Fingerprint { + /// Create a new SHA-256 fingerprint from the given binary digest. + /// + /// Use [`FromStr`] to parse an existing Base64-encoded fingerprint. + pub fn new(digest_bytes: &[u8; SHA256_BIN_LEN]) -> Self { + let mut base64 = [0u8; SHA256_BASE64_LEN]; + Base64Unpadded::encode(digest_bytes, &mut base64).expect(ENCODING_ERR_MSG); + Self(base64) + } + + /// Borrow the Base64 encoding of the digest as a string. + /// + /// Does not include the `SHA256:` algorithm prefix. + pub fn as_base64(&self) -> &str { + str::from_utf8(&self.0).expect("invalid Base64 encoding") + } + + /// Decode a Base64-encoded fingerprint to binary. + pub fn to_bytes(&self) -> [u8; SHA256_BIN_LEN] { + let mut decoded_bytes = [0u8; SHA256_BIN_LEN]; + let decoded_len = Base64Unpadded::decode(&self.0, &mut decoded_bytes) + .expect(ENCODING_ERR_MSG) + .len(); + + assert_eq!(SHA256_BIN_LEN, decoded_len); + decoded_bytes + } +} + +impl TryFrom for Sha256Fingerprint { + type Error = Error; + + fn try_from(public_key: PublicKey) -> Result { + Sha256Fingerprint::try_from(&public_key) + } +} + +impl TryFrom<&PublicKey> for Sha256Fingerprint { + type Error = Error; + + fn try_from(public_key: &PublicKey) -> Result { + let mut digest = Sha256::new(); + public_key.key_data().encode(&mut digest)?; + Ok(Self::new(&digest.finalize().into())) + } +} + +impl fmt::Display for Sha256Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", SHA256_PREFIX, self.as_base64()) + } +} + +impl str::FromStr for Sha256Fingerprint { + type Err = Error; + + fn from_str(id: &str) -> Result { + let id = id.strip_prefix(SHA256_PREFIX).ok_or(Error::Algorithm)?; + + let mut decoded_bytes = [0u8; SHA256_BIN_LEN]; + match Base64Unpadded::decode(id, &mut decoded_bytes)?.len() { + SHA256_BIN_LEN => id + .as_bytes() + .try_into() + .map(Self) + .map_err(|_| Error::Length), + _ => Err(Error::Length), + } + } +} diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 3879e2aae..9076422ae 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -119,11 +119,13 @@ mod decoder; mod encoder; mod error; +#[cfg(feature = "fingerprint")] +mod fingerprint; #[cfg(feature = "alloc")] mod mpint; pub use crate::{ - algorithm::{Algorithm, CipherAlg, EcdsaCurve, KdfAlg, KdfOpts}, + algorithm::{Algorithm, CipherAlg, EcdsaCurve, HashAlg, KdfAlg, KdfOpts}, authorized_keys::AuthorizedKeys, error::{Error, Result}, private::PrivateKey, @@ -137,3 +139,6 @@ pub use crate::mpint::MPInt; #[cfg(feature = "ecdsa")] #[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))] pub use sec1; + +#[cfg(feature = "fingerprint")] +pub use crate::fingerprint::{Fingerprint, Sha256Fingerprint}; diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index f96b093b5..0282a6467 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -217,7 +217,7 @@ impl PrivateKey { pem_encoder.encode_u32(checkint)?; self.key_data.encode(&mut pem_encoder)?; pem_encoder.encode_str(self.comment())?; - pem_encoder.encode_base64(&PADDING_BYTES[..padding_len])?; + pem_encoder.encode_raw(&PADDING_BYTES[..padding_len])?; let encoded_len = pem_encoder.finish()?; Ok(str::from_utf8(&out[..encoded_len])?) diff --git a/ssh-key/src/private/ecdsa.rs b/ssh-key/src/private/ecdsa.rs index 9b6789f27..172be8e80 100644 --- a/ssh-key/src/private/ecdsa.rs +++ b/ssh-key/src/private/ecdsa.rs @@ -44,7 +44,7 @@ impl EcdsaPrivateKey { } let mut bytes = [0u8; SIZE]; - decoder.decode_base64(&mut bytes)?; + decoder.decode_raw(&mut bytes)?; Ok(Self { bytes }) } @@ -63,10 +63,10 @@ impl Encode for EcdsaPrivateKey { encoder.encode_usize(usize::from(self.needs_leading_zero()) + SIZE)?; if self.needs_leading_zero() { - encoder.encode_base64(&[0])?; + encoder.encode_raw(&[0])?; } - encoder.encode_base64(&self.bytes) + encoder.encode_raw(&self.bytes) } } diff --git a/ssh-key/src/private/ed25519.rs b/ssh-key/src/private/ed25519.rs index dcce7415b..0ba6327d9 100644 --- a/ssh-key/src/private/ed25519.rs +++ b/ssh-key/src/private/ed25519.rs @@ -121,7 +121,7 @@ impl Decode for Ed25519Keypair { } let mut bytes = Zeroizing::new([0u8; Self::BYTE_SIZE]); - decoder.decode_base64(&mut *bytes)?; + decoder.decode_raw(&mut *bytes)?; let (priv_bytes, pub_bytes) = bytes.split_at(Ed25519PrivateKey::BYTE_SIZE); diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index c424a438b..ee56f6048 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -33,6 +33,9 @@ use { }, }; +#[cfg(feature = "fingerprint")] +use crate::{Fingerprint, HashAlg, Sha256Fingerprint}; + #[cfg(feature = "std")] use std::{fs, path::Path}; @@ -149,6 +152,17 @@ impl PublicKey { pub fn key_data(&self) -> &KeyData { &self.key_data } + + /// Compute key fingerprint. + /// + /// Use [`Default::default()`] to use the default hash function (SHA-256). + #[cfg(feature = "fingerprint")] + #[cfg_attr(docsrs, doc(cfg(feature = "fingerprint")))] + pub fn fingerprint(&self, hash_alg: HashAlg) -> Result { + match hash_alg { + HashAlg::Sha256 => Sha256Fingerprint::try_from(self).map(Into::into), + } + } } impl From for PublicKey { diff --git a/ssh-key/src/public/ed25519.rs b/ssh-key/src/public/ed25519.rs index 225809074..8e5178b01 100644 --- a/ssh-key/src/public/ed25519.rs +++ b/ssh-key/src/public/ed25519.rs @@ -33,7 +33,7 @@ impl Decode for Ed25519PublicKey { } let mut bytes = [0u8; Self::BYTE_SIZE]; - decoder.decode_base64(&mut bytes)?; + decoder.decode_raw(&mut bytes)?; Ok(Self(bytes)) } } diff --git a/ssh-key/tests/public_key.rs b/ssh-key/tests/public_key.rs index 929e46698..662835f2f 100644 --- a/ssh-key/tests/public_key.rs +++ b/ssh-key/tests/public_key.rs @@ -70,6 +70,15 @@ fn decode_dsa_openssh() { ); assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:Nh0Me49Zh9fDw/VYUfq43IJmI1T+XrjiYONPND8GzaM" + ); } #[cfg(feature = "ecdsa")] @@ -93,6 +102,15 @@ fn decode_ecdsa_p256_openssh() { #[cfg(feature = "alloc")] assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:JQ6FV0rf7qqJHZqIj4zNH8eV0oB8KLKh9Pph3FTD98g" + ); } #[cfg(feature = "ecdsa")] @@ -117,6 +135,15 @@ fn decode_ecdsa_p384_openssh() { #[cfg(feature = "alloc")] assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:nkGE8oV7pHvOiPKHtQRs67WUPiVLRxbNu//gV/k4Vjw" + ); } #[cfg(feature = "ecdsa")] @@ -142,6 +169,15 @@ fn decode_ecdsa_p521_openssh() { #[cfg(feature = "alloc")] assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:l3AUUMK6Q2BbuiqvMx2fs97f8LUYq7sWCAx7q5m3S6M" + ); } #[test] @@ -156,6 +192,15 @@ fn decode_ed25519_openssh() { #[cfg(feature = "alloc")] assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:UCUiLr7Pjs9wFFJMDByLgc3NrtdU344OgUM45wZPcIQ" + ); } #[cfg(feature = "alloc")] @@ -182,6 +227,15 @@ fn decode_rsa_3072_openssh() { ); assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:Fmxts/GcV77PakFnf1Ueki5mpU4ZjUQWGRjZGAo3n/I" + ); } #[cfg(feature = "alloc")] @@ -211,6 +265,15 @@ fn decode_rsa_4096_openssh() { ); assert_eq!("user@example.com", ossh_key.comment()); + + #[cfg(feature = "fingerprint")] + assert_eq!( + &ossh_key + .fingerprint(Default::default()) + .unwrap() + .to_string(), + "SHA256:FKAyeywtQNZLl1YTzIzCV/ThadBlnWMaD7jHQYDseEY" + ); } #[cfg(feature = "alloc")]