Skip to content

Commit 0eddd4b

Browse files
committed
Merge PR #33 'Added pure-python fallback implementation for ripemd160'
2 parents ed2bc07 + 7654daf commit 0eddd4b

3 files changed

Lines changed: 156 additions & 3 deletions

File tree

bip32/ripemd160.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright (c) 2021 Pieter Wuille
2+
# Distributed under the MIT software license, see the accompanying
3+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
#
5+
# Taken from https://github.com/bitcoin/bitcoin/blob/124e75a41ea0f3f0e90b63b0c41813184ddce2ab/test/functional/test_framework/ripemd160.py
6+
7+
# fmt: off
8+
9+
"""
10+
Pure Python RIPEMD160 implementation.
11+
12+
WARNING: This implementation is NOT constant-time.
13+
Do not use without understanding the implications.
14+
"""
15+
16+
# Message schedule indexes for the left path.
17+
ML = [
18+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
19+
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
20+
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
21+
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
22+
4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
23+
]
24+
25+
# Message schedule indexes for the right path.
26+
MR = [
27+
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
28+
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
29+
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
30+
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
31+
12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
32+
]
33+
34+
# Rotation counts for the left path.
35+
RL = [
36+
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
37+
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
38+
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
39+
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
40+
9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
41+
]
42+
43+
# Rotation counts for the right path.
44+
RR = [
45+
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
46+
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
47+
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
48+
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
49+
8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
50+
]
51+
52+
# K constants for the left path.
53+
KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
54+
55+
# K constants for the right path.
56+
KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0]
57+
58+
59+
def fi(x, y, z, i):
60+
"""The f1, f2, f3, f4, and f5 functions from the specification."""
61+
if i == 0:
62+
return x ^ y ^ z
63+
elif i == 1:
64+
return (x & y) | (~x & z)
65+
elif i == 2:
66+
return (x | ~y) ^ z
67+
elif i == 3:
68+
return (x & z) | (y & ~z)
69+
elif i == 4:
70+
return x ^ (y | ~z)
71+
else:
72+
assert False
73+
74+
75+
def rol(x, i):
76+
"""Rotate the bottom 32 bits of x left by i bits."""
77+
return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff
78+
79+
80+
def compress(h0, h1, h2, h3, h4, block):
81+
"""Compress state (h0, h1, h2, h3, h4) with block."""
82+
# Left path variables.
83+
al, bl, cl, dl, el = h0, h1, h2, h3, h4
84+
# Right path variables.
85+
ar, br, cr, dr, er = h0, h1, h2, h3, h4
86+
# Message variables.
87+
x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)]
88+
89+
# Iterate over the 80 rounds of the compression.
90+
for j in range(80):
91+
rnd = j >> 4
92+
# Perform left side of the transformation.
93+
al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el
94+
al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl
95+
# Perform right side of the transformation.
96+
ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er
97+
ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr
98+
99+
# Compose old state, left transform, and right transform into new state.
100+
return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr
101+
102+
103+
def ripemd160(data):
104+
"""Compute the RIPEMD-160 hash of data."""
105+
# Initialize state.
106+
state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
107+
# Process full 64-byte blocks in the input.
108+
for b in range(len(data) >> 6):
109+
state = compress(*state, data[64*b:64*(b+1)])
110+
# Construct final blocks (with padding and size).
111+
pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63)
112+
fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little')
113+
# Process final blocks.
114+
for b in range(len(fin) >> 6):
115+
state = compress(*state, fin[64*b:64*(b+1)])
116+
# Produce output.
117+
return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)

bip32/utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,22 @@ def _derive_public_child(pubkey, chaincode, index):
125125
return child_pub.format(), payload[32:]
126126

127127

128+
def _ripemd160(data):
129+
try:
130+
rip = hashlib.new("ripemd160")
131+
rip.update(data)
132+
return rip.digest()
133+
except BaseException:
134+
# Implementations may ship hashlib without ripemd160.
135+
# In that case, fallback to custom pure Python implementation.
136+
# WARNING: the implementation in ripemd160.py is not constant-time.
137+
from . import ripemd160
138+
139+
return ripemd160.ripemd160(data)
140+
141+
128142
def _pubkey_to_fingerprint(pubkey):
129-
rip = hashlib.new("ripemd160")
130-
rip.update(hashlib.sha256(pubkey).digest())
131-
return rip.digest()[:4]
143+
return _ripemd160(hashlib.sha256(pubkey).digest())[:4]
132144

133145

134146
def _serialize_extended_key(key, depth, parent, index, chaincode, network="main"):

tests/test_ripemd160.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from bip32.ripemd160 import ripemd160
2+
3+
4+
def test_ripemd():
5+
"""RIPEMD-160 test vectors."""
6+
# See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
7+
for msg, hexout in [
8+
(b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"),
9+
(b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"),
10+
(b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"),
11+
(b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"),
12+
(b"abcdefghijklmnopqrstuvwxyz", "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"),
13+
(
14+
b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
15+
"12a053384a9c0c88e405a06c27dcf49ada62eb2b",
16+
),
17+
(
18+
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
19+
"b0e20b6e3116640286ed3a87a5713079b21f5189",
20+
),
21+
(b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"),
22+
(b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528"),
23+
]:
24+
assert ripemd160(msg).hex() == hexout

0 commit comments

Comments
 (0)