Skip to content

Commit 06bca11

Browse files
committed
keccak: initial implementation of keccak256 and sha3-256 [skip ci]
1 parent 7cffd2f commit 06bca11

File tree

6 files changed

+668
-61
lines changed

6 files changed

+668
-61
lines changed

constantine/ciphers/chacha20.nim

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,14 @@ func chacha20_cipher*(
106106
var keyU{.noInit.}: array[8, uint32]
107107
var nonceU{.noInit.}: array[3, uint32]
108108

109-
var pos = 0'u
109+
var pos = 0
110110
for i in 0 ..< 8:
111-
keyU[i].parseFromBlob(key, pos, littleEndian)
112-
pos = 0'u
111+
keyU[i] = uint32.fromBytes(key, pos, littleEndian)
112+
pos += sizeof(uint32)
113+
pos = 0
113114
for i in 0 ..< 3:
114-
nonceU[i].parseFromBlob(nonce, pos, littleEndian)
115+
nonceU[i] = uint32.fromBytes(nonce, pos, littleEndian)
116+
pos += sizeof(uint32)
115117

116118
var counter = counter
117119
var eaten = 0

constantine/hashes/h_keccak.nim

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# Constantine
2+
# Copyright (c) 2018-2019 Status Research & Development GmbH
3+
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
4+
# Licensed and distributed under either of
5+
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
6+
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
7+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
8+
9+
import ../zoo_exports
10+
11+
import
12+
../platforms/[abstractions, views],
13+
./keccak/keccak_generic
14+
15+
# Keccak, the hash function underlying SHA3
16+
# --------------------------------------------------------------------------------
17+
#
18+
# References:
19+
# - https://keccak.team/keccak_specs_summary.html
20+
# - https://keccak.team/files/Keccak-reference-3.0.pdf
21+
# - https://keccak.team/files/Keccak-implementation-3.2.pdf
22+
# - SHA3 (different padding): https://csrc.nist.gov/publications/detail/fips/202/final
23+
# - https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf
24+
25+
# Sponge API
26+
# --------------------------------------------------------------------------------
27+
#
28+
# References:
29+
# - https://keccak.team/keccak_specs_summary.html
30+
# - https://keccak.team/files/SpongeFunctions.pdf
31+
# - https://keccak.team/files/CSF-0.1.pdf
32+
#
33+
# Keccak[r,c](Mbytes || Mbits) {
34+
# # Padding
35+
# d = 2^|Mbits| + sum for i=0..|Mbits|-1 of 2^i*Mbits[i]
36+
# P = Mbytes || d || 0x00 || … || 0x00
37+
# P = P xor (0x00 || … || 0x00 || 0x80)
38+
#
39+
# # Initialization
40+
# S[x,y] = 0, for (x,y) in (0…4,0…4)
41+
#
42+
# # Absorbing phase
43+
# for each block Pi in P
44+
# S[x,y] = S[x,y] xor Pi[x+5*y], for (x,y) such that x+5*y < r/w
45+
# S = Keccak-f[r+c](S)
46+
#
47+
# # Squeezing phase
48+
# Z = empty string
49+
# while output is requested
50+
# Z = Z || S[x,y], for (x,y) such that x+5*y < r/w
51+
# S = Keccak-f[r+c](S)
52+
#
53+
# return Z
54+
# }
55+
56+
# Duplex construction
57+
# --------------------------------------------------------
58+
# - https://keccak.team/sponge_duplex.html
59+
# - https://keccak.team/files/SpongeDuplex.pdf
60+
# - https://eprint.iacr.org/2011/499.pdf: Duplexing the Sponge
61+
# - https://eprint.iacr.org/2023/522.pdf: SAFE - Sponge API for Field Element
62+
# - https://hackmd.io/@7dpNYqjKQGeYC7wMlPxHtQ/ByIbpfX9c
63+
#
64+
# The original duplex construction described by the Keccak team
65+
# is "absorb-permute-squeeze"
66+
# Paper https://eprint.iacr.org/2022/1340.pdf
67+
# goes over other approaches.
68+
#
69+
# We follow the original intent:
70+
# - permute required when transitioning between absorb->squeeze
71+
# - no permute required when transitioning between squeeze->absorb
72+
# This may change depending on protocol requirement.
73+
# This is inline with the SAFE (Sponge API for FIeld Element) approach
74+
75+
# Types and constants
76+
# ----------------------------------------------------------------
77+
78+
type
79+
KeccakContext*[bits: static int, delimiter: static byte] = object
80+
81+
# Context description
82+
# - `state` is the permutation state, it is update only
83+
# prior to a permutation
84+
# - `buf` is a message buffer to store partial state updates
85+
# - `absorb_offset` tracks how filled the message buffer is
86+
# - `squeeze_offset` tracks the write position in the output buffer
87+
#
88+
# Subtilities:
89+
# Duplex construction requires a state permutation when
90+
# transitioning between absorb and squeezing phase.
91+
# After an absorb, squeeze_offset is incremented by the sponge `rate`
92+
# This signals the need of a permutation before squeeze.
93+
# Similarly after a squeeze, absorb_offset is incremented by the sponge rate.
94+
# The real offset can be recovered with a substraction
95+
# to properly update the state.
96+
97+
H {.align: 64.}: KeccakState
98+
buf {.align: 64.}: array[bits div 8, byte]
99+
absorb_offset: int32
100+
squeeze_offset: int32
101+
102+
keccak256* = KeccakContext[256, 0x01]
103+
sha3_256* = KeccakContext[256, 0x06]
104+
105+
template rate(ctx: KeccakContext): int =
106+
200 - 2*(ctx.bits div 8)
107+
108+
# Internals
109+
# ----------------------------------------------------------------
110+
111+
# No exceptions allowed in core cryptographic operations
112+
{.push raises: [].}
113+
{.push checks: off.}
114+
115+
func absorbBuffer(ctx: var KeccakContext) {.inline.} =
116+
ctx.H.hashMessageBlocks_generic(ctx.buf.asUnchecked(), numBlocks = 1)
117+
ctx.buf.setZero()
118+
# Note: in certain case like authenticated encryption
119+
# we might want to absorb at the same position that have been squeezed
120+
# hence we don't reset the absorb_offset to 0
121+
# The buf is zeroed which is the neutral element for xor.
122+
123+
# Public API
124+
# ----------------------------------------------------------------
125+
126+
template digestSize*(H: type KeccakContext): int =
127+
## Returns the output size in bytes
128+
KeccakContext.bits shr 3
129+
130+
template internalBlockSize*(H: type KeccakContext): int =
131+
## Returns the byte size of the hash function ingested blocks
132+
2 * (KeccakContext.bits shr 3)
133+
134+
func init*(ctx: var KeccakContext) {.inline.} =
135+
## Initialize or reinitialize a Keccak context
136+
ctx.reset()
137+
138+
func absorb*(ctx: var KeccakContext, message: openArray[byte]) =
139+
## Absorb a message in the Keccak sponge state
140+
##
141+
## Security note: the tail of your message might be stored
142+
## in an internal buffer.
143+
## if sensitive content is used, ensure that
144+
## `ctx.finish(...)` and `ctx.clear()` are called as soon as possible.
145+
## Additionally ensure that the message(s) passed were stored
146+
## in memory considered secure for your threat model.
147+
148+
if message.len == 0:
149+
return
150+
151+
var pos = int ctx.absorb_offset
152+
var cur = 0
153+
var bytesLeft = message.len
154+
155+
# We follow the "absorb-permute-squeeze" approach
156+
# originally defined by the Keccak team.
157+
# It is compatible with SHA-3 hash spec.
158+
# See https://eprint.iacr.org/2022/1340.pdf
159+
#
160+
# There are no transition/permutation between squeezing -> absorbing
161+
# And within this `absorb` function
162+
# the state pos == ctx.rate()
163+
# is always followed by a permute and setting `pos = 0`
164+
165+
if (pos mod ctx.rate()) != 0 and pos+bytesLeft >= ctx.rate():
166+
# Previous partial update, fill the state and do one permutation
167+
let free = ctx.rate() - pos
168+
ctx.buf.rawCopy(dStart = pos, message, sStart = 0, len = free)
169+
ctx.absorbBuffer()
170+
pos = 0
171+
cur = free
172+
bytesLeft -= free
173+
174+
if bytesLeft >= ctx.rate():
175+
# Process multiple blocks
176+
let numBlocks = bytesLeft div ctx.rate()
177+
ctx.H.hashMessageBlocks_generic(message.asUnchecked() +% cur, numBlocks)
178+
cur += numBlocks * ctx.rate()
179+
bytesLeft -= numBlocks * ctx.rate()
180+
181+
if bytesLeft != 0:
182+
# Store the tail in buffer
183+
ctx.buf.rawCopy(dStart = pos, message, sStart = cur, len = bytesLeft)
184+
185+
# Epilogue
186+
ctx.absorb_offset = int32 bytesLeft
187+
# Signal that the next squeeze transition needs a permute
188+
ctx.squeeze_offset = int32 ctx.rate()
189+
190+
func squeeze*(ctx: var KeccakContext, digest: var openArray[byte]) =
191+
if digest.len == 0:
192+
return
193+
194+
var pos = ctx.squeeze_offset
195+
var cur = 0
196+
var bytesLeft = digest.len
197+
198+
if pos == ctx.rate():
199+
# Transition from absorbing to squeezing
200+
# This state can only come from `absorb` function
201+
# as within `squeeze`, pos == ctx.rate() is always followed
202+
# by a permute and pos = 0
203+
ctx.H.xorInPartial(ctx.buf.toOpenArray(0, ctx.absorb_offset-1))
204+
ctx.H.pad(ctx.absorb_offset, ctx.delimiter, ctx.rate())
205+
ctx.H.permute_generic(NumRounds = 24)
206+
pos = 0
207+
ctx.absorb_offset = 0
208+
209+
if (pos mod ctx.rate()) != 0 and pos+bytesLeft >= ctx.rate():
210+
# Previous partial squeeze, fill up to rate and do one permutation
211+
let free = ctx.rate() - pos
212+
ctx.H.copyOutPartial(hByteOffset = pos, digest.toOpenArray(0, free-1))
213+
ctx.H.permute_generic(NumRounds = 24)
214+
pos = 0
215+
ctx.absorb_offset = 0
216+
cur = free
217+
bytesLeft -= free
218+
219+
if bytesLeft >= ctx.rate():
220+
# Process multiple blocks
221+
let numBlocks = bytesLeft div ctx.rate()
222+
ctx.H.squeezeDigestBlocks_generic(digest.asUnchecked() +% cur, numBlocks)
223+
ctx.absorb_offset = 0
224+
cur += numBlocks * ctx.rate()
225+
bytesLeft -= numBlocks * ctx.rate()
226+
227+
if bytesLeft != 0:
228+
# Output the tail
229+
ctx.H.copyOutPartial(hByteOffset = pos, digest.toOpenArray(cur, bytesLeft-1))
230+
231+
# Epilogue
232+
ctx.squeeze_offset = int32 bytesLeft
233+
# We don't signal absorb_offset to permute the state if called next
234+
# as per https://eprint.iacr.org/2023/522.pdf
235+
# https://hackmd.io/@7dpNYqjKQGeYC7wMlPxHtQ/ByIbpfX9c#2-SAFE-definition
236+
237+
func update*(ctx: var KeccakContext, message: openArray[byte]) =
238+
## Append a message to a Keccak context
239+
## for incremental Keccak computation
240+
##
241+
## Security note: the tail of your message might be stored
242+
## in an internal buffer.
243+
## if sensitive content is used, ensure that
244+
## `ctx.finish(...)` and `ctx.clear()` are called as soon as possible.
245+
## Additionally ensure that the message(s) passed was(were) stored
246+
## in memory considered secure for your threat model.
247+
ctx.absorb(message)
248+
249+
func finish*[N: static int](ctx: var KeccakContext, digest: var array[N, byte]) =
250+
## Finalize a Keccak computation and output the
251+
## message digest to the `digest` buffer
252+
##
253+
## Security note: this does not clear the internal buffer.
254+
## if sensitive content is used, use "ctx.clear()"
255+
## and also make sure that the message(s) passed were stored
256+
## in memory considered secure for your threat model.
257+
ctx.squeeze(digest)
258+
259+
func clear*(ctx: var KeccakContext) =
260+
## Clear the context internal buffers
261+
# TODO: ensure compiler cannot optimize the code away
262+
ctx.reset()
263+
264+
when isMainModule:
265+
import constantine/serialization/codecs
266+
267+
var msg: array[32, byte]
268+
var digest: array[32, byte]
269+
var ctx: keccak256
270+
271+
ctx.init()
272+
ctx.update(msg)
273+
ctx.finish(digest)
274+
275+
echo digest.toHex()

constantine/hashes/h_sha256.nim

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ when UseASM_X86_32:
3333

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

0 commit comments

Comments
 (0)