Current section

Files

Jump to
siphash lib siphash.ex
Raw

lib/siphash.ex

defmodule SipHash do
use Bitwise
@moduledoc """
Module for hashing values using the SipHash hash family.
"""
# alias both SipHash.State/Util
alias SipHash.State, as: State
alias SipHash.Util, as: Utils
# define the (constant) grouping pattern
@eighth_group_regex ~r/.{1,8}/
@doc """
Based on the algorithm as described in https://131002.net/siphash/siphash.pdf,
and therefore requires a key alongside the input to use as a seed. This key
is required to be 16 bytes, and is measured by `Kernel.byte_size/1`. An error
will be raised if this is not the case. The default implementation is a 2-4
hash, but this can be controlled through the options provided.
Your input *must* be a binary. It's possible to add a catch-all to `SipHash.hash/3`
which simply wraps the input in `Kernel.inspect/1`, but such usage is not
encourage. It's better to be more explicit about what is being hashed, and
`Kernel.inspect/1` does not always perform the fastest available conversion
(for example, using Poison to encode Maps is far faster, whilst also being more
reliable). In addition, the output of `Kernel.inspect/1` is specific to Elixir,
making it annoyingly unportable.
The current implementation is just a functional hash, with more effort on the
code being readable rather than optimized, and as such it's likely to change
in future (if it needs speeding up).
## Options
* `:case` - either of `:upper` or `:lower`, defaults to using `:upper`
* `:c` and `:d` - the number of compression rounds, default to `2` and `4`
* `:padding` - when `true`, pads left with zeroes to 16 chars as necessary
## Examples
iex> SipHash.hash("0123456789ABCDEF", "hello")
"3D1974E948748CE2"
iex> SipHash.hash("0123456789ABCDEF", "this is a longer input")
"C7D6D8A1E7D359DD"
iex> SipHash.hash("0123456789ABCDEF", "hello", case: :lower)
"3d1974e948748ce2"
iex> SipHash.hash("0123456789ABCDEF", "zymotechnics", padding: :true)
"09B57037CD3F8F0C"
iex> SipHash.hash("0123456789ABCDEF", "hello", c: 4, d: 8)
"CFFB51E5125013AF"
iex> SipHash.hash("invalid_bytes", "hello")
** (RuntimeError) Key must be exactly 16 bytes.
iex> SipHash.hash("FEDCBA9876543210", %{ "test" => "one" })
** (FunctionClauseError) no function clause matching in SipHash.hash/3
"""
@spec hash(binary, binary, [ { atom, atom } ]) :: binary
def hash(key, input, opts \\ [])
when is_binary(key) and is_binary(input) and is_list(opts) do
if byte_size(key) != 16 do
raise "Key must be exactly 16 bytes."
end
length = byte_size(input)
remain = rem(length, 8)
s_case = Keyword.get(opts, :case, :upper)
c_pass = Keyword.get(opts, :c, 2)
d_pass = Keyword.get(opts, :d, 4)
l_pad = Keyword.get(opts, :padding, false)
state = State.initialize(key)
chunks = Regex.scan(@eighth_group_regex, input)
last_m = Utils.apply_mask64(length <<< 56)
state =
chunks
|> Stream.map(&List.first/1)
|> Stream.take_while(&(byte_size(&1) == 8))
|> Enum.reduce(state, (&State.apply_block(&2, &1, c_pass)))
if remain > 0 do
{ _, last_m } =
chunks
|> List.last
|> List.first
|> String.reverse
|> String.codepoints
|> Enum.reduce({ remain, last_m }, fn(point, acc) ->
{ index, last_m } = acc
next_index = index - 1
point_bytes = Utils.bytes_to_long(point)
right_side =
if remain >= index and index != 1 do
(point_bytes <<< (next_index * 8))
else
point_bytes
end
{ next_index, (last_m ||| right_side) }
end)
end
state
|> State.apply_block(last_m, c_pass)
|> State.finalize(d_pass)
|> Integer.to_string(16)
|> (&(if s_case == :upper, do: &1, else: String.downcase(&1))).()
|> (&(if l_pad == false, do: &1, else: String.rjust(&1, 16, ?0))).()
end
@doc """
Wrapper around `SipHash.hash/3` to rotate the arguments, allowing for more
convenient usage when creating a pipeline (you can rotate key/input as needed).
## Examples
iex> SipHash.hash_r("hello", "0123456789ABCDEF")
"3D1974E948748CE2"
iex> SipHash.hash_r("this is a longer input", "0123456789ABCDEF")
"C7D6D8A1E7D359DD"
iex> SipHash.hash_r("hello", "0123456789ABCDEF", case: :lower)
"3d1974e948748ce2"
iex> SipHash.hash_r("zymotechnics", "0123456789ABCDEF", padding: :true)
"09B57037CD3F8F0C"
iex> SipHash.hash_r("hello", "invalid_bytes")
** (RuntimeError) Key must be exactly 16 bytes.
iex> SipHash.hash_r(%{ "test" => "one" }, "FEDCBA9876543210")
** (FunctionClauseError) no function clause matching in SipHash.hash/3
"""
@spec hash_r(binary, binary, [ { atom, atom } ]) :: binary
def hash_r(input, key, opts \\ []), do: hash(key, input, opts)
end