Skip to content

Commit 5035703

Browse files
committed
Added codecs: sha0, keccak, md6
1 parent ba9d8b7 commit 5035703

7 files changed

Lines changed: 242 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,9 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
377377
- [X] `blake`: includes BLAKE2b and BLAKE2s (Python 3 only ; relies on `hashlib`)
378378
- [X] `crypt`: Unix's crypt hash for passwords (Python 3 and Unix only ; relies on `crypt`)
379379
- [X] `fnv`: aka Fowler-Noll-Vo ; includes FNV-1 and FNV-1a in 32, 64, 128, 256, 512 and 1024 bits
380-
- [X] `md`: aka Message Digest ; includes MD4 and MD5 (relies on `hashlib`)
381-
- [X] `sha`: aka Secure Hash Algorithms ; includes SHA1, 224, 256, 384, 512 (Python2/3) but also SHA3-224, -256, -384 and -512 (Python 3 only ; relies on `hashlib`)
380+
- [X] `keccak`: original Keccak (pre-SHA-3 padding, as used e.g. by Ethereum) in 224, 256, 384 and 512 bits
381+
- [X] `md`: aka Message Digest ; includes MD2, MD4 and MD5 (relies on `hashlib`), and MD6 (digest sizes multiple of 8, up to 512 bits)
382+
- [X] `sha`: aka Secure Hash Algorithms ; includes SHA0, SHA1, 224, 256, 384, 512 (Python2/3) but also SHA3-224, -256, -384 and -512 (Python 3 only ; relies on `hashlib`)
382383
- [X] `shake`: aka SHAKE hashing (Python 3 only ; relies on `hashlib`)
383384

384385
> :warning: Hash functions are of course definitely **NOT** encoding functions ; they are implemented for convenience with the `.encode(...)` API from `codecs` and useful for chaning codecs.

‎docs/pages/enc/hashing.md‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### BLAKE
66

7-
These one-way transformation functions all rely on the native [`hashlib`](https://docs.python.org/3/library/hashlib.html) library.
7+
These one-way transformation functions rely on the native [`hashlib`](https://docs.python.org/3/library/hashlib.html) library, except SHA-0 (withdrawn shortly after its publication and replaced by SHA-1), which is implemented in pure Python.
88

99
**Codec** | **Conversions** | **Aliases** | **Comment**
1010
:---: | :---: | --- | ---
@@ -94,13 +94,31 @@ These one-way transformation functions are non-cryptographic hashes, used e.g. i
9494

9595
-----
9696

97+
### Keccak
98+
99+
This one-way transformation function is the original Keccak, as submitted to the SHA-3 competition: it only differs from SHA-3 by the padding of the data (e.g. Ethereum uses Keccak-256).
100+
101+
**Codec** | **Conversions** | **Aliases** | **Comment**
102+
:---: | :---: | --- | ---
103+
`keccak` | data --> Keccak(data, size) | `keccak224`, `keccak-256`, `keccak_512`, ... | parametrized ; *size* belongs to {224, 256, 384, 512} (default: 256)
104+
105+
```python
106+
>>> codext.encode("This is a test string !", "keccak")
107+
'cee1238ea4e44163d88c21979e42fedb5451d60c1f97b0221f757c1e77b37883'
108+
>>> codext.encode("This is a test string !", "keccak-512")
109+
'd5cbccbd447103a06c2fd3fb3b566373395ce9c573015d83d5b75f518c92ca013fc73130bf0e941d29d2283176f1816f658064ff2ee09b470e314e1d61bda761'
110+
```
111+
112+
-----
113+
97114
### Message Digest
98115

99116
**Codec** | **Conversions** | **Aliases** | **Comment**
100117
:---: | :---: | --- | ---
101118
`md2` | data --> MD2(data) | |
102119
`md4` | data --> MD4(data) | | relies on [`hashlib`](https://docs.python.org/3/library/hashlib.html)
103120
`md5` | data --> MD5(data) | | relies on [`hashlib`](https://docs.python.org/3/library/hashlib.html)
121+
`md6` | data --> MD6(data, size) | `md6-128`, `md6_512`, ... | parametrized ; *size* is a multiple of 8 in [8, 512] (default: 256) ; unkeyed, with the default tree height (64)
104122

105123
```python
106124
>>> codext.encode("This is a test string !", "md2")
@@ -109,16 +127,21 @@ These one-way transformation functions are non-cryptographic hashes, used e.g. i
109127
'ee4170b214eaac5be6a13d64a31b60b3'
110128
>>> codext.encode("This is a test string !", "md5")
111129
'5ba93d5b8e8efd9135f0030c978dd64e'
130+
>>> codext.encode("This is a test string !", "md6")
131+
'dc29b9f3d9cd16cf27b71f915b386bdd8fc3daa5eb7476a3408442d52bf8d869'
132+
>>> codext.encode("This is a test string !", "md6-128")
133+
'091d9f116b997823999ac64cfe3bfbad'
112134
```
113135

114136
-----
115137

116138
### Secure Hash Algorithm
117139

118-
These one-way transformation functions all rely on the native [`hashlib`](https://docs.python.org/3/library/hashlib.html) library.
140+
These one-way transformation functions rely on the native [`hashlib`](https://docs.python.org/3/library/hashlib.html) library, except SHA-0 (withdrawn shortly after its publication and replaced by SHA-1), which is implemented in pure Python.
119141

120142
**Codec** | **Conversions** | **Aliases** | **Comment**
121143
:---: | :---: | --- | ---
144+
`sha0` | data --> SHA0(data) | |
122145
`sha1` | data --> SHA1(data) | |
123146
`sha224` | data --> SHA224(data) | |
124147
`sha256` | data --> SHA256(data) | |
@@ -130,6 +153,8 @@ These one-way transformation functions all rely on the native [`hashlib`](https:
130153
`sha512` | data --> SHA512(data) | |
131154

132155
```python
156+
>>> codext.encode("This is a test string !", "sha0")
157+
'2cffee460320a8c226b9a62819f9409367d97e1b'
133158
>>> codext.encode("This is a test string !", "shake_256_64")
134159
'01a14a746d7c1d28927fe6078fdb9dcc8fabc45da58b3d1af13175b6278a6e824241927c47b4c5ced2ff629833574c9d985410d97c5c3d54d0f15b548cf2713d'
135160
>>> codext.encode("This is a test string !", "sha224")

‎src/codext/hashing/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .blake import *
33
from .crypt import *
44
from .fnv import *
5+
from .keccak import *
56
from .md import *
67
from .mmh3 import *
78
from .sha import *

‎src/codext/hashing/keccak.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Keccak Codecs - string hashing with Keccak.
3+
4+
Keccak was designed by Guido Bertoni, Joan Daemen, Michaël Peeters and Gilles Van Assche, and won the SHA-3 competition
5+
in 2012. It is a sponge construction over the Keccak-f[1600] permutation, whose capacity is twice the digest size. The
6+
SHA-3 standard (FIPS 202, provided by hashlib) differs from the original submission only in the padding of the data
7+
(domain separation bits "01" added before the "10*1" padding). The original Keccak, implemented here, is still used
8+
e.g. by Ethereum (Keccak-256).
9+
10+
The digest size (224, 256, 384 or 512 bits) is given with the codec name (e.g. "keccak-512") ; it defaults to 256. These
11+
codecs return the hexadecimal digest (one-way), for use with other codecs in encoding chains.
12+
13+
These codecs:
14+
- transform strings from str to str
15+
- transform strings from bytes to bytes
16+
- transform file content from str to bytes (write)
17+
18+
Reference: https://keccak.team/files/Keccak-reference-3.0.pdf
19+
"""
20+
from ..__common__ import *
21+
22+
23+
M64 = (1 << 64) - 1
24+
25+
26+
def __rc(t):
27+
# output bit of the LFSR x^8 + x^6 + x^5 + x^4 + 1 (see the reference, section 1.2)
28+
r = 1
29+
for _ in range(t % 255):
30+
r <<= 1
31+
if r & 0x100:
32+
r ^= 0x171
33+
return r & 1
34+
35+
36+
def __rot():
37+
r, x, y = [0] * 25, 1, 0
38+
for t in range(24):
39+
r[x + 5 * y], x, y = (t + 1) * (t + 2) // 2 % 64, y, (2 * x + 3 * y) % 5
40+
return r
41+
42+
43+
# round constants and rotation offsets (see the reference, sections 1.2 and 2.3)
44+
RC = [sum(__rc(j + 7 * i) << (2 ** j - 1) for j in range(7)) for i in range(24)]
45+
ROT = __rot()
46+
47+
48+
def __keccak_f(a):
49+
rol = lambda v, n: (v << n | v >> (64 - n)) & M64 if n else v
50+
for rc in RC:
51+
# theta
52+
c = [a[x] ^ a[x+5] ^ a[x+10] ^ a[x+15] ^ a[x+20] for x in range(5)]
53+
d = [c[(x - 1) % 5] ^ rol(c[(x + 1) % 5], 1) for x in range(5)]
54+
a = [v ^ d[i % 5] for i, v in enumerate(a)]
55+
# rho and pi
56+
r = [0] * 25
57+
for i, v in enumerate(a):
58+
x, y = i % 5, i // 5
59+
r[y + 5 * ((2 * x + 3 * y) % 5)] = rol(v, ROT[i])
60+
# chi and iota
61+
a = [r[i] ^ (~r[(i + 1) % 5 + i - i % 5] & r[(i + 2) % 5 + i - i % 5]) for i in range(25)]
62+
a[0] ^= rc
63+
return a
64+
65+
66+
def keccak(n=""):
67+
# a bad digest size is only reported when coding, as the lookup of the bare codec name must succeed (e.g. when
68+
# listing the encodings)
69+
n = n or 256
70+
def _hash(data, errors="strict"):
71+
if n not in (224, 256, 384, 512):
72+
raise LookupError("Bad parameter for encoding 'keccak': digest size must be 224, 256, 384 or 512")
73+
# rate in bytes, then pad10*1 (without the SHA-3 domain separation bits)
74+
d, rate = b(data), 200 - n // 4
75+
d += b"\x01" + b"\x00" * (-(len(d) + 1) % rate)
76+
d = d[:-1] + bytes([d[-1] | 0x80])
77+
a = [0] * 25
78+
for i in range(0, len(d), rate):
79+
for j in range(rate // 8):
80+
a[j] ^= int.from_bytes(d[i+8*j:i+8*j+8], "little")
81+
a = __keccak_f(a)
82+
# the digest is always shorter than the rate, hence a single squeeze
83+
return b"".join(v.to_bytes(8, "little") for v in a)[:n // 8].hex(), len(data)
84+
return _hash
85+
86+
87+
add("keccak", keccak, pattern=r"^keccak(?:[-_]?(\d+))?$", guess=None)

‎src/codext/hashing/md.py‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
44
The Message Digest algorithms were designed by Ronald Rivest: MD2 (1989, implemented here as it is not provided by
55
hashlib anymore), MD4 (1990, only when available in hashlib) and MD5 (1991), all producing 128-bit digests ; they are
6-
now considered broken for cryptographic use. These codecs return the hexadecimal digest (one-way), for use with other
7-
codecs in encoding chains.
6+
now considered broken for cryptographic use. MD6 (2008, submitted to the SHA-3 competition) is unrelated to them: it
7+
compresses blocks of 512 bytes into chaining values of 128 bytes along a 4-ary tree, which the last compression
8+
truncates to the digest size (from 8 to 512 bits, given with the codec name, e.g. "md6-512" ; it defaults to 256). It
9+
is implemented here in its default mode, unkeyed and with a tree height of 64. These codecs return the hexadecimal
10+
digest (one-way), for use with other codecs in encoding chains.
811
912
These codecs:
1013
- transform strings from str to str
@@ -14,7 +17,10 @@
1417
Reference: https://datatracker.ietf.org/doc/html/rfc1319
1518
https://datatracker.ietf.org/doc/html/rfc1320
1619
https://datatracker.ietf.org/doc/html/rfc1321
20+
https://people.csail.mit.edu/rivest/pubs/RABCx08.pdf
1721
"""
22+
from math import isqrt
23+
1824
from ..__common__ import *
1925

2026

@@ -59,8 +65,59 @@ def md2(data):
5965
return "".join("{:02x}".format(x) for x in digest[:16])
6066

6167

68+
# MD6 constants (see the specification, section 9): Q (15 words of the fractional part of sqrt(6)), the tap positions,
69+
# the shift amounts and the round constants (initial value and mask)
70+
M64 = (1 << 64) - 1
71+
MD6_Q = [isqrt(6 << 1920) >> (64 * (14 - i)) & M64 for i in range(15)]
72+
MD6_T = (17, 18, 21, 31, 67, 89)
73+
MD6_R = (10, 5, 13, 10, 11, 12, 2, 7, 14, 15, 7, 13, 11, 7, 6, 12)
74+
MD6_L = (11, 24, 9, 16, 15, 9, 27, 15, 6, 2, 29, 8, 15, 5, 31, 9)
75+
MD6_S0, MD6_SM = 0x0123456789abcdef, 0x7311c2812425cfa0
76+
77+
78+
def __md6_compress(n, rounds):
79+
a, s, (t0, t1, t2, t3, t4, t5) = n[:], MD6_S0, MD6_T
80+
for i in range(89, 89 + 16 * rounds):
81+
j = (i - 89) % 16
82+
if j == 0 and i > 89:
83+
s = (s << 1 | s >> 63) & M64 ^ s & MD6_SM
84+
x = s ^ a[i-t5] ^ a[i-t0] ^ (a[i-t1] & a[i-t2]) ^ (a[i-t3] & a[i-t4])
85+
x ^= x >> MD6_R[j]
86+
a.append((x ^ x << MD6_L[j]) & M64)
87+
return a[-16:]
88+
89+
90+
def md6(n=""):
91+
# a bad digest size is only reported when coding, as the lookup of the bare codec name must succeed (e.g. when
92+
# listing the encodings)
93+
n = n or 256
94+
def _hash(data, errors="strict"):
95+
if n not in range(8, 513, 8):
96+
raise LookupError("Bad parameter for encoding 'md6': digest size must be a multiple of 8 from 8 to 512")
97+
# default mode (see the specification, section 2.4): no key, tree of height L=64 and r=40+d/4 rounds ; with
98+
# L=64, the sequential mode at level L+1 is never reached
99+
m, rounds, lvl, key = b(data), 40 + n // 4, 0, [0] * 8
100+
while True:
101+
lvl += 1
102+
nb, out = max(1, -(-len(m) // 512)), b""
103+
for i in range(nb):
104+
blk = m[512*i:512*(i+1)]
105+
# control word: rounds, L, final compression flag (z), padding bits (p), key length and digest size
106+
p = 8 * (512 - len(blk))
107+
v = rounds << 48 | 64 << 40 | (nb == 1) << 36 | p << 20 | n
108+
blk += b"\x00" * (p // 8)
109+
w = [int.from_bytes(blk[k:k+8], "big") for k in range(0, 512, 8)]
110+
out += b"".join(x.to_bytes(8, "big") for x in __md6_compress(MD6_Q + key + [lvl << 56 | i, v] + w,
111+
rounds))
112+
m = out
113+
if nb == 1:
114+
return m[-n // 8:].hex(), len(data)
115+
return _hash
116+
117+
62118
add("md2", lambda s, error="strict": (md2(s), len(s)), guess=None)
63119
add("md5", lambda s, error="strict": (hashlib.new("md5", b(s)).hexdigest(), len(s)), guess=None)
64120
if "md4" in hashlib.algorithms_available:
65121
add("md4", lambda s, error="strict": (hashlib.new("md4", b(s)).hexdigest(), len(s)), guess=None)
66122

123+
add("md6", md6, pattern=r"^md6(?:[-_]?(\d+))?$", guess=None)

‎src/codext/hashing/sha.py‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
11
# -*- coding: UTF-8 -*-
22
"""SHA Hashing Codecs - string hashing with Secure Hash Algorithms.
33
4-
The Secure Hash Algorithms are published by the NIST: SHA-1 (1995, 160 bits, now broken), the SHA-2 family designed by
5-
the NSA (SHA-224, SHA-256, SHA-384 and SHA-512) and the SHA-3 family (SHA3-224 to SHA3-512, based on Keccak,
6-
standardised in 2015). These codecs return the hexadecimal digest (one-way), for use with other codecs in encoding
7-
chains.
4+
The Secure Hash Algorithms are published by the NIST: SHA-0 (1993, 160 bits, withdrawn soon after its publication and
5+
implemented here as it is not provided by hashlib), SHA-1 (1995, which only adds a one-bit rotation in the message
6+
schedule of SHA-0, now broken), the SHA-2 family designed by the NSA (SHA-224, SHA-256, SHA-384 and SHA-512) and the
7+
SHA-3 family (SHA3-224 to SHA3-512, based on Keccak, standardised in 2015). These codecs return the hexadecimal digest
8+
(one-way), for use with other codecs in encoding chains.
89
910
These codecs:
1011
- transform strings from str to str
1112
- transform strings from bytes to bytes
1213
- transform file content from str to bytes (write)
1314
14-
Reference: https://csrc.nist.gov/pubs/fips/180-4/upd1/final
15+
Reference: https://en.wikipedia.org/wiki/SHA-1#SHA-0
16+
https://csrc.nist.gov/pubs/fips/180-4/upd1/final
1517
https://csrc.nist.gov/pubs/fips/202/final
1618
"""
1719
from ..__common__ import *
1820

1921

22+
def sha0(data):
23+
# same as SHA-1 (see FIPS 180-4, section 6.1.2), without the 1-bit left rotation in the message schedule
24+
d, m = b(data), 0xffffffff
25+
d += b"\x80" + b"\x00" * (-(len(d) + 9) % 64) + (8 * len(d)).to_bytes(8, "big")
26+
h = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0]
27+
for i in range(0, len(d), 64):
28+
w = [int.from_bytes(d[i+j:i+j+4], "big") for j in range(0, 64, 4)]
29+
for t in range(16, 80):
30+
w.append(w[t-3] ^ w[t-8] ^ w[t-14] ^ w[t-16])
31+
a, b_, c, d_, e = h
32+
for t in range(80):
33+
if t < 20:
34+
f, k = (b_ & c) | (~b_ & d_), 0x5a827999
35+
elif t < 40:
36+
f, k = b_ ^ c ^ d_, 0x6ed9eba1
37+
elif t < 60:
38+
f, k = (b_ & c) | (b_ & d_) | (c & d_), 0x8f1bbcdc
39+
else:
40+
f, k = b_ ^ c ^ d_, 0xca62c1d6
41+
a, b_, c, d_, e = ((a << 5 | a >> 27) + f + e + k + w[t]) & m, a, (b_ << 30 | b_ >> 2) & m, c, d_
42+
h = [(x + y) & m for x, y in zip(h, [a, b_, c, d_, e])]
43+
return "".join(f"{x:08x}" for x in h)
44+
45+
46+
add("sha0", lambda s, error="strict": (sha0(s), len(s)), guess=None)
2047
add("sha1", lambda s, error="strict": (hashlib.sha1(b(s)).hexdigest(), len(s)), guess=None)
2148
add("sha224", lambda s, error="strict": (hashlib.sha224(b(s)).hexdigest(), len(s)), guess=None)
2249
add("sha256", lambda s, error="strict": (hashlib.sha256(b(s)).hexdigest(), len(s)), guess=None)

‎tests/test_manual.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,39 @@ def test_codec_hash_functions(self):
208208
self.assertRaises(LookupError, codecs.encode, STR, "fnv1-16")
209209
self.assertRaises(LookupError, codecs.encode, STR, "fnv1a-2048")
210210
self.assertRaises(NotImplementedError, codecs.decode, STR, "fnv1a")
211+
# SHA-0, Keccak (original padding) and MD6, with test vectors of the specifications and reference
212+
# implementations
213+
for enc, vectors in {
214+
'sha0': ["f96cea198ad1dd5617ac084a3d92c6107708c0ef", "0164b8a914cd2a5e74c4f7ff082c4d97f1edf880"],
215+
'keccak|keccak256|keccak-256|keccak_256': [
216+
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
217+
"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"],
218+
'keccak224': ["f71837502ba8e10837bdd8d365adb85591895602fc552b48b7390abd"],
219+
'keccak-384': ["2c23146a63a29acf99e73b88f8c24eaa7dc60aa771780ccc006afbfa8fe2479b"
220+
"2dd2b21362337441ac12b515911957ff"],
221+
'keccak_512': ["0eab42de4c3ceb9235fc91acffe746b29c29a8c366b7c60e4e67c466f36a4304"
222+
"c00fa9caf9d87976ba469bcbe06713b435f091ef2769fb160cdab33d3670680e"],
223+
'md6|md6256|md6-256|md6_256': ["bca38b24a804aa37d821d31af00f5598230122c5bbfc4c4ad5ed40e4258f04ca",
224+
"230637d4e6845cf0d092b558e87625f03881dd53a7439da34cf3b94ed0d8b2c5"],
225+
'md6-224': ["d2091aa2ad17f38c51ade2697f24cafc3894c617c77ffe10fdc7abcb",
226+
"510c30e4202a5cdd8a4f2ae9beebb6f5988128897937615d52e6d228"],
227+
'md6-512': ["6b7f33821a2c060ecdd81aefddea2fd3c4720270e18654f4cb08ece49ccb469f"
228+
"8beeee7c831206bd577f9f2630d9177979203a9489e47e04df4e6deaa0f8e0c0",
229+
"00918245271e377a7ffb202b90f3bda5477d8feab12d8a3a8994ebc55fe6e74c"
230+
"a8341520032eeea3fdef892f2882378f636212af4b2683ccf80bf025b7d9b457"],
231+
}.items():
232+
for e in enc.split("|"):
233+
for s, r in zip(["", "abc"], vectors):
234+
self.assertEqual(codecs.encode(s, e), r)
235+
self.assertEqual(codecs.encode(b(s), e), b(r))
236+
# MD6 over several levels of its tree (8192 bytes, i.e. 16 blocks, then 4, then 1)
237+
self.assertEqual(codecs.encode(bytes(i & 0xff for i in range(8192)), "md6"),
238+
b"2a40338156df221b18b20e4003f51f61284cacd01935e9e87414e6ae40a6bd25")
239+
self.assertEqual(len(codecs.encode(STR, "md6-8")), 2)
240+
for e in ["keccak-128", "keccak_1024", "md6-0", "md6-12", "md6-520"]:
241+
self.assertRaises(LookupError, codecs.encode, STR, e)
242+
for e in ["sha0", "keccak", "md6"]:
243+
self.assertRaises(NotImplementedError, codecs.decode, STR, e)
211244
if UNIX:
212245
try:
213246
import crypt

0 commit comments

Comments
 (0)