Packages

sss is an implement of Shamir's Secret Sharing Algorithm 256-bits in Elixir.

Current section

Files

Jump to
sss lib sss.ex
Raw

lib/sss.ex

# @author nghiatc
# @since Feb 17, 2025
defmodule SSS do
@moduledoc """
Documentation for `SSS`.
"""
# The largest PRIME 256-bit
# https://primes.utm.edu/lists/2small/200bit.html
# prime = 2^n - k = 2^256 - 189
@prime 115792089237316195423570985008687907853269984665640564039457584007913129639747
def get_prime, do: @prime
@doc """
Returns a random number from the range 1 <= n <= @prime-1 inclusive
"""
def random_number() do
:rand.uniform(@prime-1)
end
@doc """
Computes the multiplicative inverse of the number on the field @prime.
"""
def modinv(a, m) do
{g, x, _y} = Integer.extended_gcd(a, m)
if g != 1 do
raise("modular inverse does not exist")
else
Integer.mod(x, m)
end
end
@doc """
Returns the Int number base10 in Hex representation.
note: this is not a string representation; the Hex output is exactly 256 bits long.
"""
def to_hex(number) do
hex_string = Integer.to_string(number, 16) |> String.pad_leading(64, "0")
hex_string
end
@doc """
Returns the number Hex in base 10 Int representation.
note: this is not coming from a string representation; the Hex input is
exactly 256 bits long, and the output is an arbitrary size base 10 integer.
"""
def from_hex(hex) do
String.to_integer(hex, 16)
end
@doc """
Returns the Int number base10 in base64 representation.
note: this is not a string representation; the base64 output is exactly 256 bits long.
"""
def to_base64(number) do
number |> :binary.encode_unsigned() |> Base.url_encode64(padding: true)
end
@doc """
Returns the number base64 in base 10 Int representation.
note: this is not coming from a string representation; the base64 input
is exactly 256 bits long, and the output is an arbitrary size base 10 integer.
"""
def from_base64(base64_str) do
base64_str |> Base.url_decode64!(padding: true) |> :binary.decode_unsigned()
end
defmodule UniqueList do
def generate(0, set), do: MapSet.to_list(set)
def generate(n, set) do
before = MapSet.size(set)
random_num = SSS.random_number()
set = MapSet.put(set, random_num)
if MapSet.size(set) > before do
generate(n-1, set)
else
generate(n, set)
end
end
end
defmodule EncodeSecret do
def encode_secret(step, count, hex_secret, result) when count - step < count do
i = count - step
sub = if (i + 1) * 64 < String.length(hex_secret) do
String.to_integer(String.slice(hex_secret, i * 64, 64), 16)
else
String.to_integer(String.pad_trailing(String.slice(hex_secret, i * 64, String.length(hex_secret) - (i * 64)), 64, "0"), 16)
end
result = result ++ [sub]
encode_secret(step-1, count, hex_secret, result)
end
def encode_secret(0, _count, _hex_secret, result) do
result
end
end
@doc """
Converts a string secret into a 256-bit Int array, array based upon size of the input string.
All values are right-padded to length 256, even if the most significant bit is zero.
"""
def split_secret_to_int(secret) do
hex_secret = :binary.encode_hex(secret)
count = div((String.length(hex_secret) + 63), 64) # ceil_div = div(a + b - 1, b)
result = SSS.EncodeSecret.encode_secret(count, count, hex_secret, [])
result
end
@doc """
Converts an array of Ints to the original string secret, removing any least significant nulls.
"""
def merge_int_to_string(secrets) do
hex_data = Enum.map(secrets, fn item -> SSS.to_hex(item) end) |> Enum.join("")
hex_data = String.replace_trailing(hex_data, "00", "")
secret = :binary.decode_hex(hex_data)
secret
end
@doc """
Update an Element in a 2D Matrix(mxn) at index x-row, y-column
"""
def update_matrix_2d(matrix, x, y, value) do
updated_matrix = List.replace_at(matrix, x, List.replace_at(Enum.at(matrix, x), y, value))
updated_matrix
end
@doc """
Get an Element in a 2D Matrix(mxn) at index x-row, y-column
"""
def get_matrix_2d(matrix, x, y) do
value = Enum.at(Enum.at(matrix, x), y)
value
end
@doc """
Create a Zero Matrix(mxn) m-row and n-column
"""
def gen_zero_matrix_2d(m, n) do
zero_matrix = List.duplicate(List.duplicate(0, n), m)
zero_matrix
end
@doc """
Create a l-layer, m-row, n-column zero matrix(lxmxn)
"""
def gen_zero_matrix_3d(l, m, n) do
zero_3d_matrix = List.duplicate(List.duplicate(List.duplicate(0, n), m), l)
zero_3d_matrix
end
@doc """
Update an Element in a 3D Matrix(lxmxn) at index x-layer, y-row, z-column
"""
def update_matrix_3d(matrix_3d, x, y, z, value) do
updated_matrix = List.replace_at(matrix_3d, x,
List.replace_at(Enum.at(matrix_3d, x), y,
List.replace_at(Enum.at(Enum.at(matrix_3d, x), y), z, value)
)
)
updated_matrix
end
@doc """
Get an Element in a 3D Matrix(lxmxn) at index x-layer, y-row, z-column
"""
def get_matrix_3d(matrix_3d, x, y, z) do
value = Enum.at(Enum.at(Enum.at(matrix_3d, x), y), z)
value
end
def validator_create(minimum, shares, secret) do
if minimum <= 0 || shares <= 0 do
raise ArgumentError, "minimum or shares is invalid"
end
if minimum > shares do
raise ArgumentError, "cannot require more shares then existing"
end
if String.length(secret) == 0 do
raise ArgumentError, "secret is empty"
end
end
defmodule SetPolynomialMinimum do
def set_polynomial_minimum(step, count, part, numbers, polynomial) when count - step < count do
i = count - step
x = Enum.at(numbers, (i - 1) + part * count)
polynomial = SSS.update_matrix_2d(polynomial, part, i, x)
set_polynomial_minimum(step-1, count, part, numbers, polynomial)
end
def set_polynomial_minimum(0, _count, _part, _numbers, polynomial) do
polynomial
end
end
defmodule SetPolynomialPart do
def set_polynomial_part(step, count, minimum, numbers, secrets, polynomial) when count - step < count do
part = count - step
s = Enum.at(secrets, part)
polynomial = SSS.update_matrix_2d(polynomial, part, 0, s)
polynomial = SSS.SetPolynomialMinimum.set_polynomial_minimum(minimum-1, minimum, part, numbers, polynomial)
set_polynomial_part(step-1, count, minimum, numbers, secrets, polynomial)
end
def set_polynomial_part(0, _count, _minimum, _numbers, _secrets, polynomial) do
polynomial
end
end
defmodule Polynomial do
@moduledoc """
Evaluates a polynomial with coefficients specified in reverse order:
evaluatePolynomial([a, b, c, d], x):
return a + bx + cx^2 + dx^3
Horner's method: ((dx + c)x + b)x + a
"""
def evaluate_polynomial(0, polynomial, part, value, result) do
result = Integer.mod(result * value + SSS.get_matrix_2d(polynomial, part, 0), SSS.get_prime)
result
end
def evaluate_polynomial(s, polynomial, part, value, result) do
if s > 0 do
result = Integer.mod(result * value + SSS.get_matrix_2d(polynomial, part, s), SSS.get_prime)
evaluate_polynomial(s-1, polynomial, part, value, result)
else
result
end
end
end
defmodule CreatePointSecret do
def create_point_secret(step, count, share, minimum, numbers, polynomial, is_base64, result) when count - step < count do
j = count - step
x = Enum.at(numbers, j + share * count)
y = SSS.get_matrix_2d(polynomial, j, minimum-1)
y = SSS.Polynomial.evaluate_polynomial(minimum-2, polynomial, j, x, y)
s = if is_base64 do
SSS.to_base64(x) <> SSS.to_base64(y)
else
SSS.to_hex(x) <> SSS.to_hex(y)
end
result = result <> s
create_point_secret(step-1, count, share, minimum, numbers, polynomial, is_base64, result)
end
def create_point_secret(0, _count, _share, _minimum, _numbers, _polynomial, _is_base64, result) do
result
end
end
@doc """
Returns a new array of secret shares (encoding x,y pairs as Base64 or Hex strings)
created by Shamir's Secret Sharing Algorithm requiring a minimum number of
share to recreate, of length shares, from the input secret raw as a string.
"""
def create(minimum, shares, secret, is_base64) do
try do
validator_create(minimum, shares, secret)
# Convert the secrets to its respective 256-bit Int representation.
secrets = split_secret_to_int(secret)
# IO.inspect(secrets)
# [49937119214509114343548691117920141602615245118674498473442528546336026425464,
# 54490394935207621375798110592323721342715286901477912489156510121370884536440,
# 54490394935207621375798110592323721342715286901477912489156510121370884536440,
# 54490394935207621375798110592321034758823134506407685127331664012878869954560]
# List unique numbers in the polynomial and array shares
parts = length(secrets)
n = parts * (minimum - 1) + parts * shares
numbers = SSS.UniqueList.generate(n, MapSet.new())
# Create the polynomial of degree (minimum - 1); that is, the highest
# order term is (minimum-1), though as there is a constant term with
# order 0, there are (minimum) number of coefficients.
#
# However, the polynomial object is a 2d array, because we are constructing
# a different polynomial for each part of the secrets.
#
# polynomial[parts][minimum]
polynomial = gen_zero_matrix_2d(parts, minimum)
polynomial = SetPolynomialPart.set_polynomial_part(parts, parts, minimum, numbers, secrets, polynomial)
# IO.inspect(polynomial)
# [
# [49937119214509114343548691117920141602615245118674498473442528546336026425464,
# 3331610641690559845569293452519156689672205589426266039906276531268282534352,
# 3449414794780110805560807496417991613898224245219726297399471851840616607059],
# [54490394935207621375798110592323721342715286901477912489156510121370884536440,
# 8375165756357712393989323944985270402269773385317059146875412268233859265345,
# 20974281055222873671621056844777992892465701255324192649072266731850168978512],
# [54490394935207621375798110592323721342715286901477912489156510121370884536440,
# 26551649736996319523308626035337288866988142478182735537656910010563570288933,
# 30168152852942397170062579339393015055940095427731866340022962626704826834008],
# [54490394935207621375798110592321034758823134506407685127331664012878869954560,
# 37017445994805835743927682915662589031883038797457847082397075036203361423551,
# 41353851645870733950560656760353246098763225697893792174345635206631048655938]
# ]
# Create the points object; this holds the (x, y) points of each share.
# Again, because secrets is an array, each share could have multiple parts
# over which we are computing Shamir's Algorithm. The last dimension is
# always two, as it is storing an x, y pair of points.
#
# Note: this array is technically unnecessary due to creating result
# in the inner loop. Can disappear later if desired.
#
# For every share...
numbers = Enum.slice(numbers, parts * (minimum - 1), parts * shares)
# IO.puts("length numbers: #{length(numbers)} == #{parts * shares}")
result = for share <- 0..(shares-1) do
SSS.CreatePointSecret.create_point_secret(parts, parts, share, minimum, numbers, polynomial, is_base64, "")
end
{:ok, result}
rescue
e in ArgumentError -> {:error, e.message}
end
end
@doc """
Returns a new array of secret shares (encoding x,y pairs as Base64 or Hex strings)
created by Shamir's Secret Sharing Algorithm requiring a minimum number of
share to recreate, of length shares, from the input secret raw as a string.
"""
def create!(minimum, shares, secret, is_base64) do
case create(minimum, shares, secret, is_base64) do
{:ok, result} ->
result
{:error, _msg} -> []
end
end
@doc """
Takes in a given string to check if it is a valid secret
Requirements:
Length multiple of 128
Can decode each 64 character block as Hex
Returns raise if exist
"""
def is_valid_share_hex(candidate) do
if String.length(candidate) == 0 || Integer.mod(String.length(candidate), 128) != 0 do
raise ArgumentError, "Share is empty or invalid"
end
count = div(String.length(candidate), 64)
for i <- 0..(count-1) do
part = String.slice(candidate, i*64, 64)
decode = from_hex(part)
if decode <= 0 || decode >= @prime do
raise ArgumentError, "Share is invalid"
end
end
end
@doc """
Takes in a given string to check if it is a valid secret
Requirements:
Length multiple of 88
Can decode each 44 character block as Base64
Returns raise if exist
"""
def is_valid_share_base64(candidate) do
if String.length(candidate) == 0 || Integer.mod(String.length(candidate), 88) != 0 do
raise ArgumentError, "Share is empty or invalid"
end
count = div(String.length(candidate), 44)
for i <- 0..(count-1) do
part = String.slice(candidate, i*44, 44)
decode = from_base64(part)
if decode <= 0 || decode >= @prime do
raise ArgumentError, "Share is invalid"
end
end
end
@doc """
Validator combine shares input.
"""
def validator_combine(shares, is_base64) do
if shares == nil || length(shares) == 0 do
raise ArgumentError, "List shares is NiL or empty"
end
for share <- shares do
if is_base64 do
is_valid_share_base64(share)
else
is_valid_share_hex(share)
end
end
end
defmodule SetMatrix3DPointsXYHex do
def set_matrix_3d_points_xy_hex(step, count, i, share, points) when count - step < count do
j = count - step
pair = String.slice(share, j*128, 128)
x = SSS.from_hex(String.slice(pair, 0, 64))
y = SSS.from_hex(String.slice(pair, 64, 64))
points = SSS.update_matrix_3d(points, i, j, 0, x)
points = SSS.update_matrix_3d(points, i, j, 1, y)
set_matrix_3d_points_xy_hex(step-1, count, i, share, points)
end
def set_matrix_3d_points_xy_hex(0, _count, _i, _share, points) do
points
end
end
defmodule SetMatrix3DPointsSharesHex do
def set_matrix_3d_points_shares_hex(step, count, shares, points) when count - step < count do
i = count - step
share = Enum.at(shares, i)
num_pair = div(String.length(share), 128)
points = SSS.SetMatrix3DPointsXYHex.set_matrix_3d_points_xy_hex(num_pair, num_pair, i, share, points)
set_matrix_3d_points_shares_hex(step-1, count, shares, points)
end
def set_matrix_3d_points_shares_hex(0, _count, _shares, points) do
points
end
end
@doc """
Takes a string array of shares encoded in Hex created via Shamir's Algorithm.
Each string must be of equal length of a multiple of 128 characters
as a single 128 character share is a pair of 256-bit numbers (x, y).
"""
def decode_share_hex(shares) do
# Recreate the original object of x, y points, based upon number of shares
# and size of each share (number of parts in the secret).
num_share = length(shares)
parts = div(String.length(Enum.at(shares, 0)), 128)
points = gen_zero_matrix_3d(num_share, parts, 2)
points = SSS.SetMatrix3DPointsSharesHex.set_matrix_3d_points_shares_hex(num_share, num_share, shares, points)
points
end
defmodule SetMatrix3DPointsXYBase64 do
def set_matrix_3d_points_xy_base64(step, count, i, share, points) when count - step < count do
j = count - step
pair = String.slice(share, j*88, 88)
x = SSS.from_base64(String.slice(pair, 0, 44))
y = SSS.from_base64(String.slice(pair, 44, 44))
points = SSS.update_matrix_3d(points, i, j, 0, x)
points = SSS.update_matrix_3d(points, i, j, 1, y)
set_matrix_3d_points_xy_base64(step-1, count, i, share, points)
end
def set_matrix_3d_points_xy_base64(0, _count, _i, _share, points) do
points
end
end
defmodule SetMatrix3DPointsSharesBase64 do
def set_matrix_3d_points_shares_base64(step, count, shares, points) when count - step < count do
i = count - step
share = Enum.at(shares, i)
num_pair = div(String.length(share), 88)
points = SSS.SetMatrix3DPointsXYBase64.set_matrix_3d_points_xy_base64(num_pair, num_pair, i, share, points)
set_matrix_3d_points_shares_base64(step-1, count, shares, points)
end
def set_matrix_3d_points_shares_base64(0, _count, _shares, points) do
points
end
end
@doc """
Takes a string array of shares encoded in Base64 created via Shamir's Algorithm.
Each string must be of equal length of a multiple of 88 characters
as a single 88 character share is a pair of 256-bit numbers (x, y).
"""
def decode_share_base64(shares) do
# Recreate the original object of x, y points, based upon number of shares
# and size of each share (number of parts in the secret).
num_share = length(shares)
parts = div(String.length(Enum.at(shares, 0)), 88)
points = gen_zero_matrix_3d(num_share, parts, 2)
points = SSS.SetMatrix3DPointsSharesBase64.set_matrix_3d_points_shares_base64(num_share, num_share, shares, points)
points
end
defmodule LPIProductLoop do
def lpi_product_loop(step, count, j, i, ax, points, numerator, denominator) when count - step < count do
k = count - step
if k != i do
# combine them via half products.
# x=0 ==> [(0-bx)/(ax-bx)] * ...
bx = SSS.get_matrix_3d(points, k, j, 0)
numerator = Integer.mod((numerator * -bx), SSS.get_prime()) # (0 - bx) * ...
denominator = Integer.mod((denominator * (ax - bx)), SSS.get_prime()) # (ax - bx) * ...
lpi_product_loop(step-1, count, j, i, ax, points, numerator, denominator)
else
lpi_product_loop(step-1, count, j, i, ax, points, numerator, denominator)
end
end
def lpi_product_loop(0, _count, _j, _i, _ax, _points, numerator, denominator) do
{numerator, denominator}
end
end
defmodule LPISumLoop do
def lpi_sum_loop(step, count, j, points, secrets) when count - step < count do
i = count - step
# remember the current x and y values.
ax = SSS.get_matrix_3d(points, i, j, 0)
ay = SSS.get_matrix_3d(points, i, j, 1)
num_share = length(points)
# and for every other point...
{numerator, denominator} = SSS.LPIProductLoop.lpi_product_loop(num_share, num_share, j, i, ax, points, 1, 1)
# LPI product: x=0, y = ay * [(x-bx)/(ax-bx)] * ...
# multiply together the points (ay)(numerator)(denominator)^-1 ...
fx = ay
fx = Integer.mod(fx * numerator, SSS.get_prime())
fx = Integer.mod(fx * SSS.modinv(denominator, SSS.get_prime()), SSS.get_prime())
# LPI sum: s = fx + fx + ...
sum = Integer.mod(Enum.at(secrets, j) + fx, SSS.get_prime())
secrets = List.replace_at(secrets, j, sum)
lpi_sum_loop(step-1, count, j, points, secrets)
end
def lpi_sum_loop(0, _count, _j, _points, secrets) do
secrets
end
end
defmodule LPISecretsLoop do
def lpi_secrets_loop(step, count, points, secrets) when count - step < count do
j = count - step
num_share = length(points)
# and every share...
secrets = SSS.LPISumLoop.lpi_sum_loop(num_share, num_share, j, points, secrets)
lpi_secrets_loop(step-1, count, points, secrets)
end
def lpi_secrets_loop(0, _count, _points, secrets) do
secrets
end
end
@doc """
Takes a string array of shares encoded in Base64 or Hex created via Shamir's Algorithm
Note: the polynomial will converge if the specified minimum number of shares
or more are passed to this function. Passing thus does not affect it
Passing fewer however, simply means that the returned secret is wrong.
"""
def combine(shares, is_base64) do
try do
validator_combine(shares, is_base64)
# Recreate the original object of x, y points, based upon number of shares
# and size of each share (number of parts in the secret).
#
# points[shares][parts][2]
points = if is_base64 do
decode_share_base64(shares)
else
decode_share_hex(shares)
end
# IO.inspect(points)
# Use Lagrange Polynomial Interpolation (LPI) to reconstruct the secrets.
# For each part of the secrets (clearest to iterate over)...
parts = length(Enum.at(points, 0))
secrets = List.duplicate(0, parts)
secrets = SSS.LPISecretsLoop.lpi_secrets_loop(parts, parts, points, secrets)
{:ok, merge_int_to_string(secrets)}
rescue
e in ArgumentError -> {:error, e.message}
end
end
@doc """
Takes a string array of shares encoded in Base64 or Hex created via Shamir's Algorithm
Note: the polynomial will converge if the specified minimum number of shares
or more are passed to this function. Passing thus does not affect it
Passing fewer however, simply means that the returned secret is wrong.
"""
def combine!(shares, is_base64) do
case combine(shares, is_base64) do
{:ok, result} ->
result
{:error, _msg} -> ""
end
end
end