Here’s a simple puzzle with a neat story. A rich old woman is drafting her will and wants to distribute her expansive estate equally amongst her five children. But her children are very greedy, and the woman knows that if he leaves her will unprotected her children will resort to nefarious measures to try to get more than their fair share. In one fearful scenario, she worries that the older four children will team up to bully the youngest child entirely out of his claim! She desperately wants them to cooperate, so she decides to lock the will away, and the key is a secret integer . The question is, how can she distribute this secret number to her children so that the only way they can open the safe is if they are all present and willing?
A mathematical way to say this is: how can she distribute some information to her children so that, given all of their separate pieces of information, they can reconstruct the key, but for every choice of fewer than 5 children, there is no way to reliably recover the key? This is called the secret sharing problem. More generally, say we have an integer called the secret, a number of participants , and a number required for reconstruction . Then a secret sharing protocol is the data of a method for distributing information and a method for reconstructing the secret. The distributing method is an algorithm that accepts as input and produces as output a list of numbers . These are the numbers distributed to the participants. Then the reconstruction method is a function which accepts as input numbers and outputs a number . We want two properties to hold :
- The reconstruction function outputs when given any of the numbers output by .
- One cannot reliably reconstruct with fewer than of the numbers output by .
The question is: does an efficient secret sharing protocol exist for every possible choice of ? In fact it does, and the one we’ll describe in this post is far more secure than the word “reliable” suggests. It will be so hard as to be mathematically impossible to reconstruct the secret from fewer than the desired number of pieces. Independently discovered by Adi Shamir in 1979, the protocol we’ll see in this post is wonderfully simple, and as we describe it we’ll build up a program to implement it. This time we’ll work in the Haskell programming language, and you can download the program from this blog’s Github page. And finally, a shout out to my friend Karishma Chadha who worked together with me on this post. She knows Haskell a lot better than I do.
The key to the secret sharing protocol is a beautiful fact about polynomials. Specifically, if you give me points in the plane with distinct values, then there is a unique degree polynomial that passes through the points. Just as importantly (and as a byproduct of this fact), there are infinitely many degree polynomials that pass through the same points. For example, if I give you the points , the only quadratic (degree 2) polynomial that passes through all of them is . The proof that you can always find such a polynomial is pretty painless, so let’s take it slowly and write a program as we go. Suppose you give me some list of points and no two values are the same. The proof has two parts. First we have to prove existence, that some degree polynomial passes through the points, and then we have to prove that the polynomial is unique. The uniqueness part is easier, so let’s do the existence part first. Let’s start with just one point . What’s a degree zero polynomial that passes through it? Just the constant function . For two points it’s similarly easy, since we all probably remember from basic geometry that there’s a unique line passing through any two points. But let’s write the line in a slightly different way:
Why write it this way? Because now it should be obvious that the polynomial passes through our two points: if I plug in then the second term is zero and the first term is just , and likewise for .
For example, if we’re given we get:
Plugging in cancels the second term out, leaving , and plugging in cancels the first term, leaving .
Now the hard step is generalizing this to three points. But the suggestive form above gives us a hint on how to continue.
Notice that the numerators of the terms take on the form , that is, a product excluding . Thus, all terms will cancel out to 0 if we plug in , except one term, which has the form
Here, the fraction on the right side of the term cancels out to 1 when is plugged in, leaving only , the desired result. Now that we’ve written the terms in this general product form, we can easily construct examples for any number of points. We just do a sum of terms that look like this, one for each value. Try writing this out as a summation, if you feel comfortable with notation.
Let’s go further and write an algorithm to construct the polynomial for us. Some preliminaries: we encode a polynomial as a list of coefficients in degree-increasing order, so that is represented by
type Point = (Rational, Rational) type Polynomial = [Rational] --Polynomials are represented in ascending degree order
Then we can write some simple functions for adding and multiplying polynomials
addPoly :: Polynomial -> Polynomial -> Polynomial addPoly   =  addPoly  xs = xs addPoly xs  = xs addPoly (x:xs) (y:ys) = (x+y) : (addPoly xs ys) multNShift :: Polynomial -> (Rational, Int) -> Polynomial multNShift xs (y, shift) = (replicate shift 0) ++ ( map ((*) y) xs) multPoly :: Polynomial -> Polynomial -> Polynomial multPoly   =  multPoly  _ =  multPoly _  =  multPoly xs ys = foldr addPoly  $ map (multNShift ys) $ zip xs [0..]
multNShift multiplies a polynomial by a monomial (like ), and
multPoly does the usual distribution of terms, using multNShift to do most of the hard work. Then to construct the polynomial we need one more helper function to extract all elements of a list except a specific entry:
allBut :: Integer -> [a] -> [a] allBut i list = snd $ unzip $ filter (\ (index,_) -> i /= index) $ zip [0..] list
And now we can construct a polynomial from a list of points in the same way we did mathematically.
findPolynomial :: [Point] -> Polynomial findPolynomial points = let term (i, (xi,yi)) = let prodTerms = map (\ (xj, _) -> [-xj/(xi - xj), 1/(xi - xj)]) $ allBut i points in multPoly [yi] $ foldl multPoly  prodTerms in foldl addPoly  $ map term $ zip [0..] points
Here the sub-function
term constructs the -th term of the polynomial, and the remaining expression adds up all the terms. Remember that due to our choice of representation the awkward 1 sitting in the formula signifies the presence of . And that’s it! An example of it’s use to construct :
*Main> findPolynomial [(1,2), (2,5)] [(-1) % 1,3 % 1]
Now the last thing we need to do is show that the polynomial we constructed in this way is unique. Here’s a proof.
Suppose there are two degree polynomials and that pass through the given data points . Let , and we want to show that is the zero polynomial. This proves that is unique because the only assumptions we made at the beginning were that both passed through the given points. Now since both and are degree polynomials, is a polynomial of degree at most . It is also true that where . Thus, we have (at least) roots of this degree polynomial. But this can’t happen by the fundamental theorem of algebra! In more detail: if a nonzero degree polynomial really could have distinct roots, then you could factor it into at least linear terms like . But since there are copies of , would need to be a degree polynomial! The only way to resolve this contradiction is if is actually the zero polynomial, and thus , .
This completes the proof. Now that we know these polynomials exist and are unique, it makes sense to give them a name. So for a given set of points, call the unique degree polynomial that passes through them the interpolating polynomial for those points.
Secret Sharing with Interpolating Polynomials
Once you think to use interpolating polynomials, the connection to secret sharing seems almost obvious. If you want to distribute a secret to people so that of them can reconstruct it here’s what you do:
- Pick a random polynomial of degree so that the secret is .
- Distribute the points .
Then the reconstruction function is: take the points provided by at least participants, use them to reconstruct , and output . That’s it! Step 1 might seem hard at first, but you can just notice that is equivalent to the constant term of the polynomial, so you can pick random numbers for the other coefficients of and output them. In Haskell,
makePolynomial :: Rational -> Int -> StdGen -> Polynomial makePolynomial secret r generator = secret : map toRational (take (r-1) $ randomRs (1, (numerator(2*secret))) generator) share :: Rational -> Integer -> Int -> IO [Point] share secret k r = do generator <- getStdGen let poly = makePolynomial secret r generator ys = map (eval poly) $ map toRational [1..k] return $ zip [1..] ys
In words, we initialize the Haskell standard generator (which wraps the results inside an IO monad), then we construct a polynomial by letting the first coefficient be the secret and choosing random coefficients for the rest. And
findPolynomial is the reconstruction function.
Finally, just to flush the program out a little more, we write a function that encodes or decodes a string as an integer.
encode :: String -> Integer encode str = let nums = zip [0..] $ map (toInteger . ord) str integers = map (\(i, n) -> shift n (i*8)) nums in foldl (+) 0 integers decode :: Integer -> String decode 0 = "" decode num = if num < 0 then error "Can't decode a negative number" else chr (fromInteger (num .&. 127)) : (decode $ shift num (-8))
And then we have a function that shows the whole process in action.
example msg k r = let secret = toRational $ encode msg in do points (numerator x, numerator y)) points let subset = take r points encodedSecret = eval (findPolynomial subset) 0 putStrLn $ show $ numerator encodedSecret putStrLn $ decode $ numerator encodedSecret
And a function call:
*Main> example "Hello world!" 10 5 10334410032606748633331426632 [(1,34613972928232668944107982702),(2,142596447049264820443250256658),(3,406048862884360219576198642966),(4,916237517700482382735379150124),(5,1783927975542901326260203400662),(6,3139385067235193566437068631142),(7,5132372890379242119499357692158),(8,7932154809355236501627439048336),(9,11727493455321672728948666778334),(10,16726650726215353317537380574842)] 10334410032606748633331426632 Hello world!
The final question to really close this problem with a nice solution is, “How secure is this protocol?” That is, if you didn’t know the secret but you had numbers, could you find a way to recover the secret, oh, say, 0.01% of the time?
Pleasingly, the answer is a solid no. This protocol has something way stronger, what’s called information-theoretic security. In layman’s terms, this means it cannot possibly be broken, period. That is, without taking advantage of some aspect of the random number generator, which we assume is a secure random number generator. But with that assumption the security proof is trivial. Here it goes.
Pick a number that isn’t the secret . It’s any number you want. And say you only have of the correct numbers . Then there is a final number so that the protocol reconstructs instead of . This is no matter which of the unused -values you pick, no matter what and numbers you started with. This is simply because adding in defines a new polynomial , and you can use any point on as your -th number.
Here’s what this means. A person trying to break the secret sharing protocol would have no way to tell if they did it correctly! If the secret is a message, then a bad reconstruction could produce any message. In information theory terms, knowing of the numbers provides no information about the actual message. In our story from the beginning of the post, no matter how much computing power one of the greedy children may have, the only algorithm they have to open the safe is to try every combination. The mother could make the combination have length in the millions of digits, or even better, the mother could encode the will as an integer and distribute that as the secret. I imagine there are some authenticity issues there, since one could claim to have reconstructed a false will, signatures and all, but there appear to be measures to account for this.
One might wonder if this is the only known secret sharing protocol, and the answer is no. Essentially, any time you have an existence and uniqueness theorem in mathematics, and the objects you’re working with are efficiently constructible, then you have the potential for a secret sharing protocol. There are two more on Wikipedia. But people don’t really care to find new ones anymore because the known protocols are as good as it gets.
On a broader level, the existence of efficient secret sharing protocols is an important fact used in the field of secure multiparty computation. Here the goal is for a group of individuals to compute a function depending on secret information from all of them, without revealing their secret information to anyone. A classic example of this is to compute the average of seven salaries without revealing any of the salaries. This was a puzzle featured on Car Talk, and it has a cute answer. See if you can figure it out.
Until next time!