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 charactersDescription#
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#
- Crypto.Sha256Hex — the plain hash, for values that are not secret
- Crypto.HmacSha256Hex and Crypto.FixedTimeEquals — authenticating a message with a key you share with someone else
- declaring secrets (app.Secrets) — keys you DO hold, for services outside the app