Skip to content

Commit

Permalink
keccak: initial implementation of keccak256 and sha3-256 [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
mratsim committed Dec 22, 2024
1 parent 7cffd2f commit 06bca11
Show file tree
Hide file tree
Showing 6 changed files with 668 additions and 61 deletions.
10 changes: 6 additions & 4 deletions constantine/ciphers/chacha20.nim
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,14 @@ func chacha20_cipher*(
var keyU{.noInit.}: array[8, uint32]
var nonceU{.noInit.}: array[3, uint32]

var pos = 0'u
var pos = 0
for i in 0 ..< 8:
keyU[i].parseFromBlob(key, pos, littleEndian)
pos = 0'u
keyU[i] = uint32.fromBytes(key, pos, littleEndian)
pos += sizeof(uint32)
pos = 0
for i in 0 ..< 3:
nonceU[i].parseFromBlob(nonce, pos, littleEndian)
nonceU[i] = uint32.fromBytes(nonce, pos, littleEndian)
pos += sizeof(uint32)

var counter = counter
var eaten = 0
Expand Down
275 changes: 275 additions & 0 deletions constantine/hashes/h_keccak.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import ../zoo_exports

import
../platforms/[abstractions, views],
./keccak/keccak_generic

# Keccak, the hash function underlying SHA3
# --------------------------------------------------------------------------------
#
# References:
# - https://keccak.team/keccak_specs_summary.html
# - https://keccak.team/files/Keccak-reference-3.0.pdf
# - https://keccak.team/files/Keccak-implementation-3.2.pdf
# - SHA3 (different padding): https://csrc.nist.gov/publications/detail/fips/202/final
# - https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf

# Sponge API
# --------------------------------------------------------------------------------
#
# References:
# - https://keccak.team/keccak_specs_summary.html
# - https://keccak.team/files/SpongeFunctions.pdf
# - https://keccak.team/files/CSF-0.1.pdf
#
# Keccak[r,c](Mbytes || Mbits) {
# # Padding
# d = 2^|Mbits| + sum for i=0..|Mbits|-1 of 2^i*Mbits[i]
# P = Mbytes || d || 0x00 || … || 0x00
# P = P xor (0x00 || … || 0x00 || 0x80)
#
# # Initialization
# S[x,y] = 0, for (x,y) in (0…4,0…4)
#
# # Absorbing phase
# for each block Pi in P
# S[x,y] = S[x,y] xor Pi[x+5*y], for (x,y) such that x+5*y < r/w
# S = Keccak-f[r+c](S)
#
# # Squeezing phase
# Z = empty string
# while output is requested
# Z = Z || S[x,y], for (x,y) such that x+5*y < r/w
# S = Keccak-f[r+c](S)
#
# return Z
# }

# Duplex construction
# --------------------------------------------------------
# - https://keccak.team/sponge_duplex.html
# - https://keccak.team/files/SpongeDuplex.pdf
# - https://eprint.iacr.org/2011/499.pdf: Duplexing the Sponge
# - https://eprint.iacr.org/2023/522.pdf: SAFE - Sponge API for Field Element
# - https://hackmd.io/@7dpNYqjKQGeYC7wMlPxHtQ/ByIbpfX9c
#
# The original duplex construction described by the Keccak team
# is "absorb-permute-squeeze"
# Paper https://eprint.iacr.org/2022/1340.pdf
# goes over other approaches.
#
# We follow the original intent:
# - permute required when transitioning between absorb->squeeze
# - no permute required when transitioning between squeeze->absorb
# This may change depending on protocol requirement.
# This is inline with the SAFE (Sponge API for FIeld Element) approach

# Types and constants
# ----------------------------------------------------------------

type
KeccakContext*[bits: static int, delimiter: static byte] = object

# Context description
# - `state` is the permutation state, it is update only
# prior to a permutation
# - `buf` is a message buffer to store partial state updates
# - `absorb_offset` tracks how filled the message buffer is
# - `squeeze_offset` tracks the write position in the output buffer
#
# Subtilities:
# Duplex construction requires a state permutation when
# transitioning between absorb and squeezing phase.
# After an absorb, squeeze_offset is incremented by the sponge `rate`
# This signals the need of a permutation before squeeze.
# Similarly after a squeeze, absorb_offset is incremented by the sponge rate.
# The real offset can be recovered with a substraction
# to properly update the state.

H {.align: 64.}: KeccakState
buf {.align: 64.}: array[bits div 8, byte]
absorb_offset: int32
squeeze_offset: int32

keccak256* = KeccakContext[256, 0x01]
sha3_256* = KeccakContext[256, 0x06]

template rate(ctx: KeccakContext): int =
200 - 2*(ctx.bits div 8)

# Internals
# ----------------------------------------------------------------

# No exceptions allowed in core cryptographic operations
{.push raises: [].}
{.push checks: off.}

func absorbBuffer(ctx: var KeccakContext) {.inline.} =
ctx.H.hashMessageBlocks_generic(ctx.buf.asUnchecked(), numBlocks = 1)
ctx.buf.setZero()
# Note: in certain case like authenticated encryption
# we might want to absorb at the same position that have been squeezed
# hence we don't reset the absorb_offset to 0
# The buf is zeroed which is the neutral element for xor.

# Public API
# ----------------------------------------------------------------

template digestSize*(H: type KeccakContext): int =
## Returns the output size in bytes
KeccakContext.bits shr 3

template internalBlockSize*(H: type KeccakContext): int =
## Returns the byte size of the hash function ingested blocks
2 * (KeccakContext.bits shr 3)

func init*(ctx: var KeccakContext) {.inline.} =
## Initialize or reinitialize a Keccak context
ctx.reset()

func absorb*(ctx: var KeccakContext, message: openArray[byte]) =
## Absorb a message in the Keccak sponge state
##
## Security note: the tail of your message might be stored
## in an internal buffer.
## if sensitive content is used, ensure that
## `ctx.finish(...)` and `ctx.clear()` are called as soon as possible.
## Additionally ensure that the message(s) passed were stored
## in memory considered secure for your threat model.

if message.len == 0:
return

var pos = int ctx.absorb_offset
var cur = 0
var bytesLeft = message.len

# We follow the "absorb-permute-squeeze" approach
# originally defined by the Keccak team.
# It is compatible with SHA-3 hash spec.
# See https://eprint.iacr.org/2022/1340.pdf
#
# There are no transition/permutation between squeezing -> absorbing
# And within this `absorb` function
# the state pos == ctx.rate()
# is always followed by a permute and setting `pos = 0`

if (pos mod ctx.rate()) != 0 and pos+bytesLeft >= ctx.rate():
# Previous partial update, fill the state and do one permutation
let free = ctx.rate() - pos
ctx.buf.rawCopy(dStart = pos, message, sStart = 0, len = free)
ctx.absorbBuffer()
pos = 0
cur = free
bytesLeft -= free

if bytesLeft >= ctx.rate():
# Process multiple blocks
let numBlocks = bytesLeft div ctx.rate()
ctx.H.hashMessageBlocks_generic(message.asUnchecked() +% cur, numBlocks)
cur += numBlocks * ctx.rate()
bytesLeft -= numBlocks * ctx.rate()

if bytesLeft != 0:
# Store the tail in buffer
ctx.buf.rawCopy(dStart = pos, message, sStart = cur, len = bytesLeft)

# Epilogue
ctx.absorb_offset = int32 bytesLeft
# Signal that the next squeeze transition needs a permute
ctx.squeeze_offset = int32 ctx.rate()

func squeeze*(ctx: var KeccakContext, digest: var openArray[byte]) =
if digest.len == 0:
return

var pos = ctx.squeeze_offset
var cur = 0
var bytesLeft = digest.len

if pos == ctx.rate():
# Transition from absorbing to squeezing
# This state can only come from `absorb` function
# as within `squeeze`, pos == ctx.rate() is always followed
# by a permute and pos = 0
ctx.H.xorInPartial(ctx.buf.toOpenArray(0, ctx.absorb_offset-1))
ctx.H.pad(ctx.absorb_offset, ctx.delimiter, ctx.rate())
ctx.H.permute_generic(NumRounds = 24)
pos = 0
ctx.absorb_offset = 0

if (pos mod ctx.rate()) != 0 and pos+bytesLeft >= ctx.rate():
# Previous partial squeeze, fill up to rate and do one permutation
let free = ctx.rate() - pos
ctx.H.copyOutPartial(hByteOffset = pos, digest.toOpenArray(0, free-1))
ctx.H.permute_generic(NumRounds = 24)
pos = 0
ctx.absorb_offset = 0
cur = free
bytesLeft -= free

if bytesLeft >= ctx.rate():
# Process multiple blocks
let numBlocks = bytesLeft div ctx.rate()
ctx.H.squeezeDigestBlocks_generic(digest.asUnchecked() +% cur, numBlocks)
ctx.absorb_offset = 0
cur += numBlocks * ctx.rate()
bytesLeft -= numBlocks * ctx.rate()

if bytesLeft != 0:
# Output the tail
ctx.H.copyOutPartial(hByteOffset = pos, digest.toOpenArray(cur, bytesLeft-1))

# Epilogue
ctx.squeeze_offset = int32 bytesLeft
# We don't signal absorb_offset to permute the state if called next
# as per https://eprint.iacr.org/2023/522.pdf
# https://hackmd.io/@7dpNYqjKQGeYC7wMlPxHtQ/ByIbpfX9c#2-SAFE-definition

func update*(ctx: var KeccakContext, message: openArray[byte]) =
## Append a message to a Keccak context
## for incremental Keccak computation
##
## Security note: the tail of your message might be stored
## in an internal buffer.
## if sensitive content is used, ensure that
## `ctx.finish(...)` and `ctx.clear()` are called as soon as possible.
## Additionally ensure that the message(s) passed was(were) stored
## in memory considered secure for your threat model.
ctx.absorb(message)

func finish*[N: static int](ctx: var KeccakContext, digest: var array[N, byte]) =
## Finalize a Keccak computation and output the
## message digest to the `digest` buffer
##
## Security note: this does not clear the internal buffer.
## if sensitive content is used, use "ctx.clear()"
## and also make sure that the message(s) passed were stored
## in memory considered secure for your threat model.
ctx.squeeze(digest)

func clear*(ctx: var KeccakContext) =
## Clear the context internal buffers
# TODO: ensure compiler cannot optimize the code away
ctx.reset()

when isMainModule:
import constantine/serialization/codecs

var msg: array[32, byte]
var digest: array[32, byte]
var ctx: keccak256

ctx.init()
ctx.update(msg)
ctx.finish(digest)

echo digest.toHex()
4 changes: 2 additions & 2 deletions constantine/hashes/h_sha256.nim
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ when UseASM_X86_32:

type
Sha256Context* = object
## Align to 64 for cache line and SIMD friendliness
# Align to 64 for cache line and SIMD friendliness
s{.align: 64}: Sha256_state
buf{.align: 64}: array[BlockSize, byte]
msgLen: uint64
Expand Down Expand Up @@ -130,7 +130,7 @@ func update*(ctx: var Sha256Context, message: openarray[byte]) {.libPrefix: pref
## in an internal buffer.
## if sensitive content is used, ensure that
## `ctx.finish(...)` and `ctx.clear()` are called as soon as possible.
## Additionally ensure that the message(s) passed were stored
## Additionally ensure that the message(s) passed was(were) stored
## in memory considered secure for your threat model.
##
## For passwords and secret keys, you MUST NOT use raw SHA-256
Expand Down
Loading

0 comments on commit 06bca11

Please sign in to comment.