# Crypto.Md5Hex

> MD5 of a string's UTF-8 bytes rendered as 32-character lowercase hex — the canonical C# fingerprint form. Deterministic, so it pushes down into SQL as Postgres md5(). For NON-adversarial content fingerprints and change detection only — MD5 is collision-broken and must never be used for security. Use Crypto.Sha256Hex instead.

<!-- id: function-crypto-md5hex · area: function · stability: stable · html: https://osysharp.com/reference/function/crypto-md5hex/ -->

## Summary        {#summary}
`Crypto.Md5Hex(s)` returns the MD5 digest of `s`'s UTF-8 bytes as a **32-character lowercase hex string** —
the canonical C# `BitConverter.ToString(md5).Replace("-", "").ToLowerInvariant()` form. It is deterministic:
the same input always yields the same digest.

## Signature      {#signature}
```osy syntax
Crypto.Md5Hex(<string> s) -> string
```

## Description    {#description}

### Not a security primitive        {#not-a-security-primitive}
**MD5 is collision-broken.** An attacker can construct two different inputs that produce the same digest — cheaply,
on a laptop. So `Crypto.Md5Hex` must **never** be used to sign, authenticate, verify, or fingerprint anything an
attacker could influence, and never for passwords. If a check answers the question "did this come from someone I
trust?" or "has anyone tampered with this?", MD5 is the wrong tool and using it there is a real vulnerability, not a
style issue.

Reach for these instead:

| If you need to… | Use |
|---|---|
| Hash a value that could be attacker-influenced | [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) |
| Prove a message came from a key holder, unaltered | [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) |
| Store a password | `Security.HashPassword` (BCrypt — salted and deliberately slow) |

### What it IS for — a non-adversarial content fingerprint                  {#what-it-is-for}
`Crypto.Md5Hex` is kept because it is genuinely the right tool for a **non-adversarial content fingerprint**: hash a
synthesized string and compare it to a stored hash to decide whether downstream work needs to re-run. Nobody is
attacking your cache-invalidation key, and MD5 is cheap and stable.

Because it is deterministic, it **pushes down into SQL**: inside a query it renders as Postgres's `md5()`, which
produces byte-identical lowercase hex, so in-memory and in-database results agree. (`Crypto.Sha256Hex` pushes down too,
so you can use the secure hash in a query without giving that up.)

## Examples       {#examples}
```osy title="fingerprint gate — change detection, not a security check" test app=crypto-md5
// Safe use: deciding whether OUR OWN content changed, so we can skip redundant work.
// Nobody gains anything by forcing a cache miss here.
bool Changed(string content, string storedHash) {
  return Crypto.Md5Hex(content) != storedHash;
}
// Crypto.Md5Hex("hello")  ->  "5d41402abc4b2a76b9719d911017c592"
```

## See also       {#see-also}
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — the secure default hash; use this whenever the input could be attacker-influenced
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — authenticate a message under a shared key, and verify the tag in constant time
- [Text.Split](https://osysharp.com/reference/function/text-split/) — other in-memory string builtins
