# Security.Fingerprint

> A deterministic, KEYED fingerprint of a value — store it in a column anyone may read, look a code up by exact match, and a reader of the table still cannot reverse it or test guesses offline, because the key never leaves the platform. For discount codes, gift cards, invite and referral codes. Server-only; stable for the life of the app.

<!-- id: security-fingerprint · area: security · stability: stable · html: https://osysharp.com/reference/security/fingerprint/ -->

## Summary        {#summary}
`Security.Fingerprint(value)` answers the same 64-character lowercase hex string every time it is given the same
value — so it can be **stored and looked up by exact match** — but it is an HMAC-SHA256 under a per-app key the
platform derives and holds, so nobody holding the table can compute one from a guess. It is what to store instead of
a code you need to FIND again and never need to READ again.

## Signature      {#signature}
```osy syntax
Security.Fingerprint(<string> value) -> string   // 64 lowercase hex characters
```

## Description    {#description}
**Why not `Crypto.Sha256Hex`?** A plain hash of a short code is a lookup table away from the code: anyone with a copy
of the column hashes `SUMMER10`, `SUMMER20`, … and matches. A fingerprint cannot be checked that way, because the
guess has to be fingerprinted too, and only the platform can.

**The key is the platform's, never yours.** It is derived from key material the platform already holds for the app
(the same custody as `Crypto.Encrypt` and signed file URLs), with a purpose of its own. You do not declare a secret
for it, and no Osy# code can read it.

**It is stable** for one app across restarts, deploys, recompiles and key rotation — a stored fingerprint keeps
matching for as long as the app exists. **Two apps disagree**: the same code fingerprints differently in each.

**It runs on the server.** A body sent to the browser cannot compute one (the browser would need the key), and the
compiler refuses it there by name. Inside a query it is computed once, before the query, and bound as a parameter — the
database never sees the key either.

**Exact match only.** A fingerprint of `summer25` is not a fingerprint of `SUMMER25`. Normalize before you
fingerprint (`code.Trim().ToUpper()`) on both the store and the lookup side.

## Examples       {#examples}
```osy title="store a discount code so it can be found and not read" test app=security-fingerprint
[Principal] entity Shopper {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity DiscountCode {
  [Required, MaxLength(64)] string CodeHash;   // what is stored — 64 hex characters, never the code
  int Percent;
  security { allow read, create when IsAuthenticated; }
}

void Issue(string code, int percent) {
  new DiscountCode { CodeHash = Security.Fingerprint(code.Trim().ToUpper()), Percent = percent };
  UnitOfWork.Commit();
}

int PercentOff(string input) {
  var d = DiscountCode.Where(x => x.CodeHash == Security.Fingerprint(input.Trim().ToUpper())).FirstOrDefault();
  return d == null ? 0 : d.Percent;
}
```

```osy title="found by the code, and by nothing else" run app=security-fingerprint
[TestFixture]
void AShopper() { new Shopper { Email = "ana@shop.example" }; }

principal Ana => Shopper.Single(s => s.Email == "ana@shop.example");

// The body runs as Ana: with no `[runas]` it is a signed-out caller, and `IsAuthenticated` refuses it the create.
[Test(AShopper)]
[runas(Ana)]
void A_code_is_found_by_its_fingerprint() {
  Issue("summer25", 20);
  Assert.Equal(20, PercentOff(" SUMMER25 "));   // normalized on both sides
  Assert.Equal(0, PercentOff("SUMMER26"));
  Assert.Equal(64, DiscountCode.First().CodeHash.Length);
}
```

## See also       {#see-also}
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — the plain hash, for values that are not secret
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — authenticating a message with a key you share with someone else
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — keys you DO hold, for services outside the app
