import type { IdentifierEntropy } from "./capability.js"; /** * How hard is an identifier to guess, when there is no schema to read it off? * * The share-link rule needs one fact before it will demote a granted read from * a finding: could a stranger have obtained the identifier this request * carried? With a database behind the run that is read off the column's type * and default — `bigserial` enumerates, `gen_random_uuid()` does not — and the * value we planted is deliberately never looked at, because measuring the value * would be measuring our own randomness rather than the application's. * * With no database there is no column and no default. What there is, and what * this reads, is **the identifiers the application itself generated**: create * three resources and look at what came back. The standing objection does not * apply, because the generator under measurement is the application's. * * Three samples rather than two, and the reason is the monotonicity check. Any * two distinct values are ordered — a pair proves nothing about whether the * next one is predictable. Three consecutive creations that come back in order * are evidence that the generator counts, and that is a hard disqualifier * however many bits the value appears to carry: `n_3` follows `n_2`, and so * does every time-prefixed id in ordinary use. * * Everything here is one-sided in the same direction the schema-reading version * is. `unguessable` is claimed only where the samples settle it; an unrecognised * shape, a sample short of three, or a disagreement between samples all come * back guessable, which *keeps the finding*. That costs a demotion we might have * been entitled to. The opposite error would suppress a real hole, and this * whole class of rule exists on the understanding that it can only ever be * allowed to take findings away for a reason the run established. */ export interface IdentifierShape { /** What the samples were recognised as, in words, for the report. */ format: string; /** Bits of the value an attacker cannot predict. Counted conservatively. */ bits: number; /** * The format embeds a timestamp, so the space to search is not the whole * value: an attacker who knows roughly when a resource was created walks a * window rather than the field. ObjectId, ULID, KSUID and UUIDv7 are all * this, and all of them look wide until the prefix comes off. */ timeOrdered: boolean; /** * Every character position varied between the samples, and they are all the * same length. * * The test that separates an identifier drawn uniformly at random from one * with structure in it. `1717171700-8f3a91c2b7d4e6` is seventeen characters * of a wide alphabet and a fixed separator sitting in the middle of it, which * is the shape of a timestamp with a short random tail — and counting its * length would credit it with nearly twice the randomness it has. Anything * that is not uniform is treated as unrecognised, which keeps the finding. */ uniform: boolean; /** The prefix every sample shares, which carries no information at all. */ prefix: string; /** The suffix every sample shares. Same. */ suffix: string; } /** The bar, in bits, for a value space too large to walk. */ export declare const ENOUGH_BITS = 64; /** * What shape are these identifiers, and how much of one is unpredictable? * * The samples have to agree. An application that answers with a uuid one time * and a counter the next is not one this can say anything about, and the answer * is zero bits rather than the larger of the two. */ export declare function shapeOf(samples: string[]): IdentifierShape; /** * Could a stranger guess an identifier of this kind? * * Every one of the disqualifiers below keeps a finding rather than removing * one, which is the only direction a rule like this may be wrong in. */ export declare function measureIdentifierEntropy( /** The identifiers the application assigned, in the order it assigned them. */ samples: string[]): IdentifierEntropy & { bits: number; format: string; }; /** * How much of an identifier could not have matched by coincidence? * * A different question to the one above, asked for a different purpose. Self- * attestation says a resource carries its creator's account identifier, and the * thing that could go wrong there is not that an attacker guesses the value — * it is that some unrelated field happens to hold it. With account ids of `1` * and `2`, a `version` column of `1` on the first user's resource and `2` on the * second's would read as proof of ownership; with 122-bit account ids it could * not. * * So this counts bits and nothing else. Ordering does not matter — nobody is * guessing anything — and two samples are enough, which is what there is: a run * has two accounts. */ export declare function distinguishingBits(samples: string[]): { bits: number; format: string; };