244 lines
8.3 KiB
Markdown
244 lines
8.3 KiB
Markdown
# Keccak, SHA-3, and SHAKE-N algorithms explained, with code in Erlang
|
|
|
|
Keccak is a hashing algorithm used for the SHA-3 standard. [The
|
|
standard][nist-standard] is semi-readable math clownery. Hashing algorithms by
|
|
nature have to be complicated and somewhat obfuscated. But, at the end of the
|
|
day, all Keccak does is take in some input bits, process them in a
|
|
deterministic way, and hand you output bits.
|
|
|
|
The goal of this document is just to explain what the processing steps are as
|
|
straightforwardly as possible.
|
|
|
|
Keccak is the "general case", and then SHA-3 and SHAKE-128 and so on are
|
|
special cases of Keccak.
|
|
|
|
## tldr
|
|
|
|
- [Erlang code (clear)](https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek.erl)
|
|
- [Erlang code (fast)](https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek_fast.erl)
|
|
- [erlang-sha3 library (uses fast version of kek)](https://github.com/zxq9/erlang-sha3/blob/63193654e3c05d8031300ffcd52092f75e8b5c2f/src/sha3.erl#L85-L112)
|
|
|
|
#### References
|
|
|
|
1. [Helpful lecture](https://www.youtube.com/watch?v=JWskjzgiIa4)
|
|
2. [Notes for that lecture](https://www.crypto-textbook.com/download/Understanding-Cryptography-Keccak.pdf)
|
|
3. [NIST standard][nist-standard] (btw: the double bar notation means "concatenate")
|
|
4. [SHA-3 Wikipedia](https://en.wikipedia.org/wiki/SHA-3)
|
|
|
|
#### Pitfall: "fast keccak" versus "clear keccak"
|
|
|
|
The main reference is the "clear" Erlang code. The "fast" version is the
|
|
"clear" version with optimizations applied. The fast version was written by
|
|
Hans Svensson.
|
|
|
|
In particular, fast keccak is arity 4, and clear keccak is arity 3.
|
|
|
|
There are a couple of trivial differences in code structure, so when you're
|
|
reading code, you need to keep in mind which version you're looking at. When I
|
|
have a code sample, I will link the GitHub permalink in a comment where the
|
|
code originates from, so there shouldn't be any confusion.
|
|
|
|
```erlang
|
|
%% From "clear keccak"
|
|
%% https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek.erl#L155-L159
|
|
-spec keccak(Capacity, Message, OutputBitLength) -> Digest
|
|
when Capacity :: pos_integer(),
|
|
Message :: bitstring(),
|
|
OutputBitLength :: pos_integer(),
|
|
Digest :: bitstring().
|
|
|
|
%% From "fast Keccak"
|
|
%% https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek_fast.erl#L142-L147
|
|
-spec keccak(Capacity, Message, Delimiter, OutputBitLength) -> Digest
|
|
when Capacity :: pos_integer(),
|
|
Message :: bitstring(),
|
|
Delimiter :: bitstring(),
|
|
OutputBitLength :: pos_integer(),
|
|
Digest :: bitstring().
|
|
```
|
|
|
|
The reason for this is one of Hans's optimizations was speeding up the manner
|
|
in which padding was applied, and that required factoring it out into an
|
|
argument.
|
|
|
|
#### Pitfall: "NIST SHA-3" versus "Keccak SHA-3"
|
|
|
|
There are two versions of "SHA-3". There's the "NIST version" and the "keccak
|
|
version". The only difference is that NIST adds two bits (yes bits, not bytes)
|
|
of padding
|
|
|
|
```erlang
|
|
%% From the erlang-sha3 lib, which uses the fast version
|
|
%% - ignore the capacity and what not for now, will be explained in a minute
|
|
%% - OutputBitLength is the N in SHA3-N. So it would be 512 for SHA3-512, 256
|
|
%% for SHA3-256, etc.
|
|
%% - Message is the input bitstring to be hashed
|
|
|
|
%% https://github.com/zxq9/erlang-sha3/blob/63193654e3c05d8031300ffcd52092f75e8b5c2f/src/sha3.erl#L107-L112
|
|
kek(OutputBitLength, Message, keccak) ->
|
|
Capacity = 2 * OutputBitLength,
|
|
keccak(Capacity, Message, <<>>, OutputBitLength);
|
|
%% ^ keccak sha3 adds no padding bits
|
|
kek(OutputBitLength, Message, nist) ->
|
|
Capacity = 2 * OutputBitLength,
|
|
keccak(Capacity, Message, <<2#01:2>>, OutputBitLength).
|
|
%% ^ nist sha3 concats these padding bits to the end of Message
|
|
```
|
|
|
|
The reason this problem exists is the "keccak version" was used in production
|
|
code before the NIST standard was published. So now there are two versions
|
|
of "SHA-3" out there.
|
|
|
|
#### Pitfall: SHAKE-N versus SHA3-N
|
|
|
|
Note that the padding and sha3-is-keccak-but-its-not pitfall only applies to
|
|
the SHA-3 fixed-length hashing functions. These produce a fixed-length (say,
|
|
512 bit) hash.
|
|
|
|
The SHAKE-N algorithms are sort of "arbitrary length SHA-3". So instead of
|
|
producing 512 bits, you tell it how long the output length should be. The
|
|
SHAKE-N algorithms have their own padding rule, and I don't believe they're
|
|
affected by the padding thing.
|
|
|
|
```erlang
|
|
%% From the "clear" version
|
|
%% The clear version was written against the NIST standard
|
|
|
|
%% https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek.erl#L86-L89
|
|
sha3(OutputBitLength, Message) ->
|
|
Capacity = 2*OutputBitLength,
|
|
%% SHA-3 NIST padding is used in the clear version
|
|
ShaMessage = <<Message/bitstring, (2#01):2>>,
|
|
keccak(Capacity, ShaMessage, OutputBitLength).
|
|
|
|
%% https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek.erl#L131-L134
|
|
shake(ShakeNumber, Message, OutputBitLength) ->
|
|
Capacity = 2*ShakeNumber,
|
|
%% SHAKE has its own padding rule
|
|
ShakeMessage = <<Message/bitstring, (2#1111):4>>,
|
|
keccak(Capacity, ShakeMessage, OutputBitLength).
|
|
```
|
|
|
|
## SHA-s and SHAKE-s
|
|
|
|
These are the "porcelain" functions that we show to the outside world. These
|
|
are just rewrites, in front of Keccak. Another way to think about it is that
|
|
Keccak has tons of settings, and that each of these "algorithms" are just
|
|
different settings presets.
|
|
|
|
```erlang
|
|
%% From: https://github.com/pharpend/kek/blob/8a8a655a80c26ae32763cc25f1e0df8ab0653c82/kek.erl#L26-L134
|
|
|
|
-spec sha3_224(Message) -> Digest
|
|
when Message :: bitstring(),
|
|
Digest :: <<_:224>>.
|
|
%% @doc
|
|
%% SHA-3 with an output bit length of 224 bits.
|
|
%% @end
|
|
|
|
sha3_224(Message) ->
|
|
sha3(224, Message).
|
|
|
|
|
|
|
|
-spec sha3_256(Message) -> Digest
|
|
when Message :: bitstring(),
|
|
Digest :: <<_:256>>.
|
|
%% @doc
|
|
%% SHA-3 with an output bit length of 256 bits.
|
|
%% @end
|
|
|
|
sha3_256(Message) ->
|
|
sha3(256, Message).
|
|
|
|
|
|
|
|
-spec sha3_384(Message) -> Digest
|
|
when Message :: bitstring(),
|
|
Digest :: <<_:384>>.
|
|
%% @doc
|
|
%% SHA-3 with an output bit length of 384 bits.
|
|
%% @end
|
|
|
|
sha3_384(Message) ->
|
|
sha3(384, Message).
|
|
|
|
|
|
|
|
-spec sha3_512(Message) -> Digest
|
|
when Message :: bitstring(),
|
|
Digest :: <<_:512>>.
|
|
%% @doc
|
|
%% SHA-3 with an output bit length of 512 bits.
|
|
%% @end
|
|
|
|
sha3_512(Message) ->
|
|
sha3(512, Message).
|
|
|
|
|
|
|
|
-spec sha3(OutputBitLength, Message) -> Digest
|
|
when OutputBitLength :: pos_integer(),
|
|
Message :: bitstring(),
|
|
Digest :: bitstring().
|
|
%% @doc
|
|
%% SHA-3 with an arbitrary output bit length.
|
|
%%
|
|
%% This means Keccak with Capacity = 2*OutputBitLength. Additionally, SHA3
|
|
%% concatenates the bits 01 onto the end of the input, before sending the
|
|
%% Message to keccak/3.
|
|
%% @end
|
|
|
|
sha3(OutputBitLength, Message) ->
|
|
Capacity = 2*OutputBitLength,
|
|
ShaMessage = <<Message/bitstring, (2#01):2>>,
|
|
keccak(Capacity, ShaMessage, OutputBitLength).
|
|
|
|
|
|
|
|
-spec shake128(Message, OutputBitLength) -> Digest
|
|
when Message :: bitstring(),
|
|
OutputBitLength :: pos_integer(),
|
|
Digest :: bitstring().
|
|
%% @doc
|
|
%% This is the SHAKE variable-length hash with Capacity 256 = 2*128 bits.
|
|
%% @end
|
|
|
|
shake128(Message, OutputBitLength) ->
|
|
shake(128, Message, OutputBitLength).
|
|
|
|
|
|
|
|
-spec shake256(Message, OutputBitLength) -> Digest
|
|
when Message :: bitstring(),
|
|
OutputBitLength :: pos_integer(),
|
|
Digest :: bitstring().
|
|
%% @doc
|
|
%% This is the SHAKE variable-length hash with Capacity 512 = 2*256 bits.
|
|
%% @end
|
|
|
|
shake256(Message, OutputBitLength) ->
|
|
shake(256, Message, OutputBitLength).
|
|
|
|
|
|
|
|
-spec shake(ShakeNumber, Message, OutputBitLength) -> Digest
|
|
when ShakeNumber :: pos_integer(),
|
|
Message :: bitstring(),
|
|
OutputBitLength :: pos_integer(),
|
|
Digest :: bitstring().
|
|
%% @doc
|
|
%% This is the SHAKE variable-length hash with Capacity 512 = 2*ShakeNumber bits.
|
|
%%
|
|
%% This concatenates the bitstring 1111 onto the end of the Message before
|
|
%% sending the message to keccak/3.
|
|
%% @end
|
|
|
|
shake(ShakeNumber, Message, OutputBitLength) ->
|
|
Capacity = 2*ShakeNumber,
|
|
ShakeMessage = <<Message/bitstring, (2#1111):4>>,
|
|
keccak(Capacity, ShakeMessage, OutputBitLength).
|
|
```
|
|
|
|
[nist-standard]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf
|