Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

Security.Fingerprint

Security.Fingerprint(value) → string (64-char lowercase hex, HMAC-SHA256 under a key only the platform holds)

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.

stable2 examples compiled by CIsecuritycryptohashingstdlib

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#

Security.Fingerprint(<string> value) -> string   // 64 lowercase hex characters

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#

[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;
}
[TestFixture]
void AShopper() { new Shopper { Email = "[email protected]" }; }

principal Ana => Shopper.Single(s => s.Email == "[email protected]");

// 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#

Related

Crypto.Sha256Hex

SHA-256 of a string's UTF-8 bytes as 64-character lowercase hex — the secure default hash. Deterministic, so it pushes…

Crypto.HmacSha256Hex and Crypto.FixedTimeEquals

HMAC-SHA-256 authenticates a message under a shared key — proving it came from a key holder and was not altered, which…

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…