#[non_exhaustive]pub struct Sha256(/* private fields */);Expand description
SHA-256 digest carried on the JMAP wire as a 64-character lowercase hex string (draft-atwood-jmap-cid-00 §2).
§Example
use jmap_cid_types::Sha256;
// The SHA-256 of the empty string (FIPS 180-4 published vector).
let hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
let d: Sha256 = hex.parse()?;
assert_eq!(d.as_str(), hex);
// Round-trips bit-for-bit through serde as a bare JSON string.
let json = serde_json::to_string(&d)?;
assert_eq!(json, format!("\"{hex}\""));
let d2: Sha256 = serde_json::from_str(&json)?;
assert_eq!(d, d2);§Construction
All four ABNF-validating paths share the same parse logic from
Sha256::from_hex:
Sha256::from_hex— validate a&strcandidate.TryFrom<&str>/TryFrom<String>— validate viatry_into().FromStr::from_str— validate via.parse::<Sha256>().serde::Deserialize— validates viaTryFrom<String>per the#[serde(try_from = "String")]attribute.
One infallible conversion:
From<[u8; 32]>/From<&[u8; 32]>forSha256— format 32 raw digest bytes into the canonical lowercase-hex string. Infallible because every byte produces two lowercase-hex nibbles by construction. The input is the output of a hash function (the raw digest), NOT arbitrary input data to be hashed — this crate carries no hash computation. See theFrom<&[u8; 32]>impl for byte-ordering details.
§Wire format
The bare 64-character hex string. #[serde(try_from = "String")]
routes every deserialize path through the validating
TryFrom<String> impl, so every deserialize path applies the
same ABNF check Sha256::from_hex does. Serialize uses a
manual impl Serialize that emits the inner hex string via
serializer.collect_str (canonical-template parity with the
jmap-types::Id newtype; see bd:JMAP-sf5h.20).
§Allocation bounds
Sha256 does not bound allocation before validation. The
#[serde(try_from = "String")] adapter routes every deserialize
path through TryFrom<String>, which means serde_json (and any
other serde data format) materialises the full input string
before the 64-byte ABNF cap rejects oversize values. A hostile
JMAP peer responding with {"sha256": "aaaa…"} where the payload
is gigabytes of 'a' will force the consumer to allocate
O(payload-length) bytes per deserialize attempt before the
WrongLength error fires.
Consumers deserialising Sha256 (directly or transitively, e.g.
via jmap-base-client’s BlobUploadResponse.sha256 field) from
an untrusted or partially-trusted JMAP peer MUST enforce a
body-size limit at the transport layer:
reqwest::Response::bytes()does not bound by default; useResponse::bytes_stream()plus a wrappingtake(N)byte cap, or checkResponse::content_length()against a policy maximum before reading.tokio_tungsteniteWebSocket frames default to a 64 MiB limit per frame (WebSocketConfig::max_frame_size); JMAP push frames carrying a digest field do not need 64 MiB and the limit can be tightened.- Hand-rolled HTTP clients reading a
BodyMUST cap with atake(N)adapter before callingserde_json::from_reader.
In-scope threats:
- A JMAP client connected to a malicious or compromised JMAP server. The server controls the response body.
- A JMAP middleware (federation peer, mirror, cache) processing responses from a peer it does not fully trust.
- A JMAP client behind a proxy that rewrites response bodies (less likely on TLS, but possible on metadata or via injection).
Out of scope: a trusted server returning oversize bodies by accident. Transport-layer bounding handles both cases uniformly.
This crate intentionally does not switch to a custom
Deserialize impl with Visitor::visit_str length pre-check —
the workspace canonical pattern keeps validation centralised in
Sha256::from_hex so the ABNF check is a single source of truth
(see bd:JMAP-sf5h.9 for the decision record). Self-bounding at
the type level would shift validation responsibility into the
Deserialize impl and away from from_hex; the workspace prefers
transport-layer bounding over per-type bounding because the
transport bound applies uniformly to every field on a
hostile response, not just sha256 ones.
§Equality and threat model
PartialEq and Eq on Sha256 inherit the standard
variable-time String compare — short-circuits on the
first byte mismatch. This is appropriate for the in-scope
JMAP CID use case (draft-atwood-jmap-cid-00): the digest is
the SHA-256 of a public blob broadcast to every party
with read access, so there is no secret comparison and the
timing channel reveals nothing an attacker does not already
have.
Consumers repurposing Sha256 for secret-derived
comparisons — MAC-style verification, commitment opening,
capability tokens — MUST use a constant-time compare via
subtle::ConstantTimeEq on the underlying bytes. Extract
via Sha256::as_str().as_bytes(). The variable-time
short-circuit otherwise leaks the prefix length of a matching
digest to a remote attacker on the classic Bleichenbacher-class
timing pattern.
This crate intentionally does not ship a constant-time
comparison helper because the workspace policy is that
MAC/HMAC-shaped verification belongs in a separate type
(MacTag, SecretDigest) so the choice of constant-time
equality is part of the type contract, not a per-call-site
opt-in on a wire-format type.
Implementations§
Source§impl Sha256
impl Sha256
Sourcepub fn to_base64(&self) -> String
pub fn to_base64(&self) -> String
The digest as padded RFC 4648 §4 base64, the encoding of the
RFC 9404 digest:sha-256 property.
Sourcepub fn from_base64(s: &str) -> Result<Self, Sha256Base64Error>
pub fn from_base64(s: &str) -> Result<Self, Sha256Base64Error>
Parse a padded RFC 4648 §4 base64 digest, such as an RFC 9404
digest:sha-256 value, into its Sha256.
Source§impl Sha256
impl Sha256
Sourcepub fn to_cidv1_bytes(&self) -> [u8; 36]
pub fn to_cidv1_bytes(&self) -> [u8; 36]
The binary CIDv1 for this digest: 0x01 0x55 0x12 0x20
followed by the 32 digest bytes.
Sourcepub fn to_cidv1(&self) -> String
pub fn to_cidv1(&self) -> String
The CIDv1 string form for this digest: b followed by the
binary CIDv1 as lowercase base32 without padding.
Sourcepub fn from_cidv1_bytes(bytes: &[u8]) -> Result<Self, CidV1Error>
pub fn from_cidv1_bytes(bytes: &[u8]) -> Result<Self, CidV1Error>
Parse a binary CIDv1 back into its Sha256.
Rejects anything that is not exactly 36 bytes starting with
0x01 0x55 0x12 0x20.
Sourcepub fn from_cidv1(s: &str) -> Result<Self, CidV1Error>
pub fn from_cidv1(s: &str) -> Result<Self, CidV1Error>
Parse a CIDv1 string (b + lowercase unpadded base32) back
into its Sha256.
Source§impl Sha256
impl Sha256
Sourcepub fn from_hex(s: &str) -> Result<Self, Sha256DigestError>
pub fn from_hex(s: &str) -> Result<Self, Sha256DigestError>
Parse a candidate hex string into a Sha256.
Returns Sha256DigestError when the candidate is not
exactly 64 bytes long or contains any byte outside the
lowercase-hex set [0-9 a-f]. Errors report position so a
caller can surface a precise diagnostic.
Sourcepub fn into_inner(self) -> String
pub fn into_inner(self) -> String
Consume the value and return the inner String.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Sha256
impl<'de> Deserialize<'de> for Sha256
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for Sha256
Source§impl From<&[u8; 32]> for Sha256
impl From<&[u8; 32]> for Sha256
Source§fn from(b: &[u8; 32]) -> Self
fn from(b: &[u8; 32]) -> Self
Format 32 raw digest bytes as a canonical lowercase-hex
Sha256. Infallible because every byte produces two
lowercase-hex nibbles by construction.
The input is the output of a SHA-256 hash function —
e.g. sha2::Sha256::digest(data).into() — not the data to
be hashed. This crate intentionally carries no hash
computation.
§Byte ordering
The 32 input bytes are taken in the canonical
FIPS 180-4 SHA-256 output order: the most significant
byte of the digest first. b[0] occupies positions 0..=1
of the resulting hex string and b[31] occupies positions
62..=63. This matches the output of sha2::Sha256::digest,
ring::digest::digest(&SHA256, ...), and
openssl::sha::sha256. Consumers feeding digest output
from a non-standard source (HSM in non-standard endianness,
pre-reversed archive format) must reorder bytes before
the conversion or the wire value will be wrong.
impl StructuralPartialEq for Sha256
Source§impl TryFrom<String> for Sha256
impl TryFrom<String> for Sha256
Source§fn try_from(s: String) -> Result<Self, Self::Error>
fn try_from(s: String) -> Result<Self, Self::Error>
Validate the owned String against the ABNF and move it
into the Sha256 on success — no second allocation. This
is the path #[serde(try_from = "String")] takes, so every
serde_json::from_str::<Sha256>(...) deserialize avoids the
double-alloc that from_hex(&s) would incur.