Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 38 additions & 32 deletions lib/shamir/shamir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,79 @@ defmodule KeyX.Shamir do
alias KeyX.Shamir.Arithmetic

@spec split_secret(non_neg_integer, non_neg_integer, binary) :: list(binary)
def split_secret(_k, n, _secret) when n > 255, do: raise "too many parts, n <= 255"
def split_secret(k, n, _secret) when k > n, do: raise "k cannot be less than total shares"
def split_secret(k, _n, _secret) when k < 2, do: raise "k cannot be less than 2"
def split_secret(_k, _n, secret) when length(secret) == 0, do: raise "secret cannot be zero"
def split_secret(_k, n, _secret) when n > 255, do: raise("too many parts, n <= 255")
def split_secret(k, n, _secret) when k > n, do: raise("k cannot be less than total shares")
def split_secret(k, _n, _secret) when k < 2, do: raise("k cannot be less than 2")
def split_secret(_k, _n, secret) when length(secret) == 0, do: raise("secret cannot be zero")

def split_secret(k, n, secret) do
set_random_seed() # Generate random x coordinates
# Generate random x coordinates
set_random_seed()
x_coorinates = 0..254 |> rand_shuffle

# This is where implementations can differ, presumably. The H.C. Vault developers noted:
# // Make random polynomials for each byte of the secret
# // Construct a random polynomial for each byte of the secret.
# // Because we are using a field of size 256, we can only represent
# // a single byte as the intercept of the polynomial, so we must
# // use a new polynomial for each byte.
# // Because we are using a field of size 256, we can only represent
# // a single byte as the intercept of the polynomial, so we must
# // use a new polynomial for each byte.
# We could use larger numbers (large field set (larger primes?)),
# but maintaining compatibility is a key goal in this project
shares_init = for _ <- 1..n, do: []

shares = Enum.reduce :binary.bin_to_list(secret), shares_init, fn(val, shares) ->
poly = Arithmetic.polynomial(val, k-1)
shares =
Enum.reduce(:binary.bin_to_list(secret), shares_init, fn val, shares ->
poly = Arithmetic.polynomial(val, k - 1)

for {x_val,y_acc} <- Enum.zip(x_coorinates, shares), into: [] do
x = x_val + 1
y = poly |> Arithmetic.evaluate(x)
[ y_acc, y ]
end
end
for {x_val, y_acc} <- Enum.zip(x_coorinates, shares), into: [] do
x = x_val + 1
y = poly |> Arithmetic.evaluate(x)
[y_acc, y]
end
end)

for {share,x} <- Enum.zip(shares, x_coorinates), into: [] do
:binary.list_to_bin([share, (x + 1) ])
for {share, x} <- Enum.zip(shares, x_coorinates), into: [] do
:binary.list_to_bin([share, x + 1])
end
end

@spec recover_secret( list(binary) ) :: binary
@spec recover_secret(list(binary)) :: binary
def recover_secret(shares) do
# Constants
shares = Enum.map(shares, &:binary.bin_to_list/1)
sizes = for share <- shares, into: [], do: length(share)
[ size | other_sz ] = sizes |> Enum.uniq
[size | other_sz] = sizes |> Enum.uniq()

y_len = size - 1
x_samples = for share <- shares, do: List.last(share)

# Error checking
unless [] = other_sz, do: raise "shares must match in size"
unless length(x_samples) == MapSet.size(MapSet.new(x_samples)), do: raise "Duplicated shares"
unless [] == other_sz, do: raise("shares must match in size")
unless length(x_samples) == MapSet.size(MapSet.new(x_samples)), do: raise("Duplicated shares")

# Evaluate polynomials and return secret!
res = for idx <- 0..(y_len-1), into: [] do
y_samples = for share <- shares, into: [] do
share |> Enum.at(idx)
res =
for idx <- 0..(y_len - 1), into: [] do
y_samples =
for share <- shares, into: [] do
share |> Enum.at(idx)
end

KeyX.Shamir.Arithmetic.interpolate(x_samples, y_samples, 0)
end
KeyX.Shamir.Arithmetic.interpolate(x_samples, y_samples, 0)
end

res |> :binary.list_to_bin
res |> :binary.list_to_bin()
end


def set_random_seed() do
# https://hashrocket.com/blog/posts/the-adventures-of-generating-random-numbers-in-erlang-and-elixir
<< i1 :: unsigned-integer-32, i2 :: unsigned-integer-32, i3 :: unsigned-integer-32>> = :crypto.strong_rand_bytes(12)
<<i1::unsigned-integer-32, i2::unsigned-integer-32, i3::unsigned-integer-32>> =
:crypto.strong_rand_bytes(12)

:rand.seed(:exsplus, {i1, i2, i3})
end

def rand_shuffle(list) do
list |> Enum.sort_by( fn _x -> :rand.uniform() end )
list |> Enum.sort_by(fn _x -> :rand.uniform() end)
end

end
52 changes: 30 additions & 22 deletions lib/shamir/shamir_math.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule KeyX.Shamir.Arithmetic do
import Kernel, except: [+: 2, *: 2, /: 2]
import Bitwise
import Enum, only: [at: 2]

alias KeyX.Shamir.Tables
Expand All @@ -9,64 +8,73 @@ defmodule KeyX.Shamir.Arithmetic do

@spec polynomial(non_neg_integer, non_neg_integer) :: polynomial
def polynomial(intercept, degree) do
[ intercept | (:crypto.strong_rand_bytes(degree) |> :binary.bin_to_list()) ]
[intercept | :crypto.strong_rand_bytes(degree) |> :binary.bin_to_list()]
end

@spec evaluate(polynomial, non_neg_integer) :: non_neg_integer
def evaluate(poly, x) when x === 0 do
poly |> at(0)
end

def evaluate(poly, x) do
[ poly_tail | poly_rest_rev ] = Enum.reverse(poly)
[poly_tail | poly_rest_rev] = Enum.reverse(poly)

Enum.reduce(poly_rest_rev, poly_tail, fn(poly_coef, acc) ->
(acc * x) + poly_coef
Enum.reduce(poly_rest_rev, poly_tail, fn poly_coef, acc ->
acc * x + poly_coef
end)
end

def interpolate(x_samples, y_samples, x)
when length(x_samples) == length(x_samples) do
when length(x_samples) == length(x_samples) do
# Setup interpolation env
limit = length(x_samples) - 1

# Loop through all the x & y samples, reducing them to an answer
Enum.reduce 0..limit, 0, fn(i, result) ->
Enum.reduce(0..limit, 0, fn i, result ->
# skip i == j on inner reduce
inner_rng = Enum.reject(0..limit, &(&1 == i))

basis = Enum.reduce inner_rng, 1, fn(j, basis) ->
basis * ( (x + at(x_samples, j) )
/ (at(x_samples, i) + at(x_samples, j)) )
end
basis =
Enum.reduce(inner_rng, 1, fn j, basis ->
basis *
((x + at(x_samples, j)) /
(at(x_samples, i) + at(x_samples, j)))
end)

group = basis * at(y_samples, i)
result + group
end
end)
end
def interpolate(_x_samples, _y_samples, _x), do: raise "Invalid arguments"

def _lhs / rhs when rhs === 0, do: raise ArithmeticError
def interpolate(_x_samples, _y_samples, _x), do: raise("Invalid arguments")

def _lhs / rhs when rhs === 0, do: raise(ArithmeticError)

def lhs / rhs do
zero = 0
ret = Kernel.-(Tables.log(lhs), Tables.log(rhs))

ret =
Kernel.-(Tables.log(lhs), Tables.log(rhs))
|> Kernel.+(255)
|> rem(255)
|> Tables.exp
|> Tables.exp()

if (lhs ===0), do: zero, else: ret
if lhs === 0, do: zero, else: ret
end

# Multiplies two numbers in GF(2^8)
def lhs * rhs do
zero = 0
ret = Kernel.+(Tables.log(lhs), Tables.log(rhs))

ret =
Kernel.+(Tables.log(lhs), Tables.log(rhs))
|> rem(255)
|> Tables.exp
|> Tables.exp()

if :erlang.or(lhs === 0, rhs === 0), do: zero, else: ret
end

# @spec evaluate(polynomial, non_neg_integer) :: non_neg_integer
def lhs + rhs, do: lhs ^^^ rhs


# lhs ^^^ rhs
def lhs + rhs, do: Bitwise.bxor(lhs, rhs)
end