It’s inconceivable that a blog titled “Hashed Potatoes” never had a post about hashes for the past few years. That changes now. Potatoes neither; but another time, perhaps?
Suppose you have a registration system, and you want to set up a hashing scheme to generate a unique ID for each user. You want this ID to be numeric (base 10), but you don’t know how many digits you should assign to it. You do, however, have an estimate of the maximum number of users that would be registering. How can you obtain a sensible lower bound to the number of digits needed?
random oracles
Hashing functions are designed to emulate the function of random oracles. A random oracle is an idealised black-box construct that behaves according to the following rules:
- A random oracle is a function that takes an object from some domain
and maps it to an object in a co-domain
.
- Given an input it has not received before, a random oracle will randomly select an object in
according to a uniform distribution to be the output.
- Given an input it has received before, a random oracle will output the same result it had outputted when it previously received that input.
This way, random oracles are designed to be irreversible. The only attack it should be susceptible to is to enumerate through every possible input until the desired output is obtained, i.e. a brute-force attack.
While the property of irreversibility is very useful, random oracles do not guarantee the property of injection. What this means is that it is possible for a valid random oracle to map two different inputs to the same output. This is a hash collision.
hash collisions
It would be horrible if a hash collision were to occur in our registration system, because two different people could end up with the same ID. That would allow one to impersonate the other in our system, and access information only privy to the other. However, due to the random nature of how the output is selected, there is always a nonzero probability of a hash collision.
Let us define the following quantities for convenience:
- The maximum expected number of users is
, i.e. the domain size is
. The domain here is the application domain, which is a subset of the hash domain.
- The number of possible hash outputs is
, i.e. the co-domain size is
.
- We want to fix the probability of a hash collision as
.
With these definitions, our question now becomes: given and
, how can we determine
?
a qualitative look
Before we jump into slogging through the math, let’s first make some qualitative observations.
Notice that if , we are guaranteed to have a hash collision. By the pigeonhole principle, if we assign a unique slot to each of the first
inputs, the next input must use an already existing hash. In mathematical terms,
.
Notice also that if increases for the same
, that means that we are more lenient, so we don’t need so many hash slots, thus
decreases. In mathematical terms,
.
If increases for the same
, that means that there will be more users, so there is another possible point of failure, thus
increases. In mathematical terms,
.
wrangling probabilities
Let’s start by deriving an expression for in terms of
and
. To have no hash collision, each of the
inputs must fill a different slot out of
total slots.
To analyse this, let’s assign each input to a slot sequentially:
- The first input can be assigned any slot, and there will be no hash collision. The probability of the first input being assigned any slot is
. We can rewrite this as
(you’ll see why later).
- The second input can be assigned any slot except the first input’s. The probability of the second input being assigned any slot except the first input’s is
.
- The
th input can be assigned any slot except those belonging to the first
inputs. The probability of that is
.
We need the condition for all inputs to be true, thus we need to apply an AND operation (multiplication) between all conditions to get the probability that there is no hash collision for inputs.
It is here that we run into our first issue. This expression is made complicated by the presence of factorials. How can we actually solve for ? As exponentials and factorials are multiplicative, let’s first take the logarithm of both sides.
At this point, we can convert the factorials into summations, but there is no further way to simplify this expression. We are stuck and there is no way to proceed further.
(Just kidding.)
There is indeed no way to simplify the above expression further, but let’s take a moment to analyse the expression above. In particular, let’s look at the case where is high enough, such that
is too expensive to evaluate. For example, if
, and each evaluation takes 1 ms, you’d need about 1 day to obtain the result. In this case, we need to rely on an approximation to obtain the result.
stirling’s approximation
Ultimately, there are two terms we need to approximate: and
. The tool we shall use is Stirling’s approximation.
We’ll make an important observation here: is the Riemann sum of
. As
increases, the integral becomes more accurate at approximating the sum. The solution for the integral is:
Let’s take this a step further and analyse the integral separately for now. Let’s take apply the trapezoidal rule to obtain a more accurate approximation:
Now that we have our approximation, let’s substitute this back into the original equation and simplify to get a very clean result:
This is as far as we’re going to get with Stirling’s approximation, but there’s still no clear way to solve for yet.
taylor series
Now, we can simplify the expression even further by adding an assumption. Let’s assume that we want the hash collision probability to be very small, to the extent where
, so
.
We can then apply the Taylor Series to obtain a first-order approximation of the right-hand side. We will include two terms in the series as there will be a cancellation of the most significant term.
Expanding the right-hand side and ignoring the terms in :
Rearranging, we can now solve for :
If is very small, we can simplify this one step further:
observations
This is a remarkably simple result, and we can make the following observations:
scales with
and
.
is
, or the
th triangular number.
Let’s get a physical intuition of these figures:
is the mean number of hashes for 1 collision to occur.
is the number of ways to choose any 2 users out of all the users.
- If
is large enough and
is small enough, we can assume that the likelihood of 3 or more users having the same hash is negligible, when compared to just 2 users having the same hash.
Thus, under these assumptions, the probability of a collision is just the number of ways to choose any 2 users divided by the number of possible hash outputs.
conclusion
What does this mean for us when setting up a hashing scheme? Well, several things.
- We can obtain the required number of digits as
.
- Notice that we made no assumptions on the radix in our calculations. Instead of base 10, we can use any other base. E.g. base 16 for hex, base 36 for alphanumeric, base 62 for case-sensitive alphanumeric.
- If we can include a token that splits the domain by a factor of
, the required co-domain size is reduced by a factor of approximately
. As such, it is best to include as many of such tokens as possible, while being careful to avoid exposing any sensitive keys.
