Cryptographic libraries almost always give you two views of the same digest: raw bytes and a hex string (hexdigest). Confusing them causes failed signature checks, doubled hex encoding, and security review findings that look silly but block releases.
Convert display strings carefully with Hex to String (for inspection only) and read Hex vs Base64 when APIs disagree on transport format.
Raw digest vs hexdigest
import hashlib
msg = b"test"
raw = hashlib.sha256(msg).digest() # 32 bytes
hx = hashlib.sha256(msg).hexdigest() # 64 hex chars
assert raw.hex() == hx
assert bytes.fromhex(hx) == raw
- Raw bytes: what MACs and verifiers usually want inside binary protocols.
- Hex string: what humans paste into tickets, Git, and config files.
Never feed a hex string into an API that expects raw bytes without decoding first—and never hex() a value that is already hex text.
The double-encoding bug
# Wrong: hex-encode an already-hex digest
bad = hashlib.sha256(b"test").hexdigest().encode().hex()
# Right: pick one representation
good = hashlib.sha256(b"test").hexdigest()
Symptoms: length doubles (64 → 128 chars for SHA-256), verifiers fail, and the string no longer matches published checksums.
Hex vs Base64 for digests
Git and most Linux distro checksum files use lowercase hex. Some web APIs prefer Base64 or Base64url. Neither is “more secure.” Match the verifier:
- Read the API doc for encoding.
- Compare lengths (SHA-256: 32 raw bytes, 64 hex chars, 44 Base64 chars with padding).
- Normalize case for hex before compare if the spec says case-insensitive.
Keys and secrets
- Display keys as hex or Base64 for humans; keep raw bytes in memory for crypto calls.
- Do not log live private keys. If you must log fingerprints, hash them and show short hex prefixes.
- Constant-time compare digests as bytes when avoiding timing leaks; convert hex to bytes first.
import hmac
def hex_digest_equal(a_hex: str, b_hex: str) -> bool:
return hmac.compare_digest(bytes.fromhex(a_hex), bytes.fromhex(b_hex))
Language footnotes
- PHP:
hash('sha256', $data, false)hex (default); third argtruefor raw. - Node:
crypto.createHash('sha256').update(data).digest('hex'). - Java: format
MessageDigestbytes withHexFormat(17+) or a tested helper.
Publishing checksums for downloads
Release pages usually show hex digests so users can verify ISOs and archives. Good practice:
- Name the algorithm (
SHA256, not just “checksum”). - Use lowercase hex unless an ecosystem standard says otherwise.
- Provide a command users can copy (
sha256sum file/shasum -a 256). - Publish the digest on HTTPS from the same vendor origin when possible.
If you also offer Base64 digests, label them clearly so nobody compares across encodings.
FAQ
Is a hex digest encrypted?
No. Digests are one-way fingerprints. Hex only prints them.
Should I store password hashes as hex?
Follow your password-hashing library. Many store an algorithm-specific string format, not bare SHA hex. Do not invent a scheme with raw SHA-256(password).
Why don’t two hex digests match?
Different inputs, different algorithms, uppercase/lowercase policies, or one side Base64. Also check for trailing newlines in files you hashed.
Related: hex decode pitfalls · PHP string to hex · hex overview.