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.
This module makes use of NIFs for better performance and throughput, but this
can be disabled by setting the `HASH_IMPL` environment variable to `embedded`.
This is controlled via the environment rather than a specific function arg as
the NIFs are automatically loaded during the start of the application. Please
note that the use of NIFs brings a significant performance improvement, and so
you should only disable them with good reason.
"""
# alias both SipHash.State/Util
alias SipHash.State, as: State
alias SipHash.Util, as: Util
# define types
@type s :: { number, number, number, number }
@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/2`, but such usage is not
encourage. It's better to be more explicit about what is being hashed, and
`Kernel.inspect/2` 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/2` is specific to Elixir,
making it annoyingly unportable.
By default, all values are returned as numbers (i.e. the result of the hash),
but you can set `:hex` to true as an option to get a hex string output. The
reason for this is that converting to hex takes an extra couple of µs, and the
default is intended to be the optimal use case. Please note that any of the
options related to hex string formatting will be ignored if `:hex` is not set
to true (e.g. `:case`).
## Options
* `:case` - either of `:upper` or `:lower`, defaults to using `:upper`
* `:c` and `:d` - the number of compression rounds, default to `2` and `4`
* `:hex` - when `true` returns the output as a hex string
* `:padding` - when `true`, pads left with zeroes to 16 chars as necessary
## Examples
iex> SipHash.hash("0123456789ABCDEF", "hello")
4402678656023170274
iex> SipHash.hash("0123456789ABCDEF", "hello", hex: true)
"3D1974E948748CE2"
iex> SipHash.hash("0123456789ABCDEF", "abcdefgh", hex: true)
"1AE57886F899E65F"
iex> SipHash.hash("0123456789ABCDEF", "my long strings", hex: true)
"1323400B0804036D"
iex> SipHash.hash("0123456789ABCDEF", "hello", hex: true, case: :lower)
"3d1974e948748ce2"
iex> SipHash.hash("0123456789ABCDEF", "zymotechnics", hex: true, padding: :true)
"09B57037CD3F8F0C"
iex> SipHash.hash("0123456789ABCDEF", "hello", c: 4, d: 8)
14986662229302055855
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
in_len = byte_size(input)
s_case = :upper
c_pass = 2
d_pass = 4
to_hex = false
l_pad = false
state = State.initialize(key)
case opts do
[] -> ;
[_h|_t] ->
s_case = Keyword.get(opts, :case, s_case)
c_pass = Keyword.get(opts, :c, c_pass)
d_pass = Keyword.get(opts, :d, d_pass)
to_hex = Keyword.get(opts, :hex, to_hex)
l_pad = Keyword.get(opts, :padding, l_pad)
end
input
|> Util.chunk_string(8)
|> Enum.reduce(state, fn(chunk, state) ->
case byte_size(chunk) do
8 ->
State.apply_block(state, chunk, c_pass)
l ->
{ chunk, state, l }
end
end)
|> State.apply_last_block(in_len, c_pass)
|> State.finalize(d_pass)
|> Util.to_hex(to_hex)
|> Util.to_case(s_case, :upper)
|> Util.pad_left(l_pad)
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")
4402678656023170274
"""
@spec hash_r(binary, binary | s, [ { atom, atom } ]) :: binary
def hash_r(input, key_or_state, opts \\ []), do: hash(key_or_state, input, opts)
end