Skip to content

Commit 067e094

Browse files
committed
Merge branch 'hkdf' (COMPATIBILITY BREAK)
This changes password_to_scalar() and arbitrary_element() to a new derivation function, using HKDF and matching the proposed functions in bitwiseshiftleft/sjcl#273 . Users of this library from after this commit will not interoperate with those who use the library from before this commit: they will get "WrongPasswordError" all the time. closes #5 (rebased the original commits)
2 parents 94b8009 + eb15e64 commit 067e094

9 files changed

+288
-60
lines changed

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ python:
1010
- "3.4"
1111
- "3.5"
1212
- "pypy"
13-
- "pypy3"
1413
install:
1514
- pip install -U tox virtualenv coverage python-coveralls
1615
script:

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
* License: MIT
55
* Dependencies: none (pure-python)
6-
* Compatible With: Python 2.6, 2.7, 3.3, 3.4, 3.5, pypy2, pypy3
6+
* Compatible With: Python 2.6, 2.7, 3.3, 3.4, 3.5, pypy2
77
* [![Build Status](https://travis-ci.org/warner/python-spake2.png?branch=master)](https://travis-ci.org/warner/python-spake2) [![Coverage Status](https://coveralls.io/repos/warner/python-spake2/badge.svg)](https://coveralls.io/r/warner/python-spake2)
88

99
This library implements the SPAKE2 password-authenticated key exchange

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,5 @@ def abbrev(t):
7979
"Programming Language :: Python :: 3.5",
8080
"Topic :: Security :: Cryptography",
8181
],
82+
install_requires=["hkdf"],
8283
)

src/spake2/ed25519_basic.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import binascii, hashlib, itertools
2+
from .groups import expand_arbitrary_element_seed
23

34
Q = 2**255 - 19
45
L = 2**252 + 27742317777372353535851937790883648493
@@ -267,8 +268,11 @@ def subtract(self, other):
267268

268269

269270
def arbitrary_element(seed): # unknown DL
270-
# TODO: if we don't need uniformity, maybe use just sha256 here?
271-
hseed = hashlib.sha512(seed).digest()
271+
# We don't strictly need the uniformity provided by hashing to an
272+
# oversized string (128 bits more than the field size), then reducing
273+
# down to Q. But it's comforting, and it's the same technique we use for
274+
# converting passwords/seeds to scalars (which *does* need uniformity).
275+
hseed = expand_arbitrary_element_seed(seed, (256/8)+16)
272276
y = int(binascii.hexlify(hseed), 16) % Q
273277

274278
# we try successive Y values until we find a valid point

src/spake2/ed25519_group.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from . import ed25519_basic
2+
from .groups import password_to_scalar
23

34
class _Ed25519Group:
45
def random_scalar(self, entropy_f):
@@ -8,7 +9,7 @@ def scalar_to_bytes(self, i):
89
def bytes_to_scalar(self, b):
910
return ed25519_basic.bytes_to_scalar(b)
1011
def password_to_scalar(self, pw):
11-
return ed25519_basic.password_to_scalar(pw)
12+
return password_to_scalar(pw, self.scalar_size_bytes, self.order())
1213
def arbitrary_element(self, seed):
1314
return ed25519_basic.arbitrary_element(seed)
1415
def bytes_to_element(self, b):

src/spake2/groups.py

+26-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import division
22
import hashlib
3+
from hkdf import Hkdf
34
from .six import integer_types
45
from .util import (size_bits, size_bytes, unbiased_randrange,
56
bytes_to_number, number_to_bytes)
@@ -61,6 +62,24 @@
6162
"""
6263

6364

65+
def expand_password(data, num_bytes):
66+
h = Hkdf(salt=b"", input_key_material=data, hash=hashlib.sha256)
67+
info = b"SPAKE2 pw"
68+
return h.expand(info, num_bytes)
69+
70+
def password_to_scalar(pw, scalar_size_bytes, q):
71+
assert isinstance(pw, bytes)
72+
# the oversized hash reduces bias in the result, so
73+
# uniformly-random passwords give nearly-uniform scalars
74+
oversized = expand_password(pw, scalar_size_bytes+16)
75+
assert len(oversized) >= scalar_size_bytes
76+
i = bytes_to_number(oversized)
77+
return i % q
78+
79+
def expand_arbitrary_element_seed(data, num_bytes):
80+
h = Hkdf(salt=b"", input_key_material=data, hash=hashlib.sha256)
81+
info = b"SPAKE2 arbitrary element"
82+
return h.expand(info, num_bytes)
6483

6584
class _Element:
6685
def __init__(self, group, e):
@@ -76,7 +95,7 @@ def to_bytes(self):
7695
return self._group._element_to_bytes(self)
7796

7897
class IntegerGroup:
79-
def __init__(self, p, q, g, element_hasher):
98+
def __init__(self, p, q, g):
8099
self.q = q # the subgroup order, used for scalars
81100
self.scalar_size_bytes = size_bytes(self.q)
82101
_s = self.scalar_to_bytes(self.password_to_scalar(b""))
@@ -89,10 +108,6 @@ def __init__(self, p, q, g, element_hasher):
89108
self.p = p # the field size
90109
self.element_size_bits = size_bits(self.p)
91110
self.element_size_bytes = size_bytes(self.p)
92-
_e = element_hasher(b"")
93-
assert isinstance(_e, bytes)
94-
assert len(_e) >= self.element_size_bytes
95-
self.element_hasher = element_hasher
96111

97112
# double-check that the generator has the right order
98113
assert pow(g, self.q, self.p) == 1
@@ -119,19 +134,14 @@ def bytes_to_scalar(self, b):
119134
return i
120135

121136
def password_to_scalar(self, pw):
122-
assert isinstance(pw, bytes)
123-
# the oversized hash reduces bias in the result, so
124-
# uniformly-random passwords give nearly-uniform scalars
125-
oversized = hashlib.sha512(pw).digest()
126-
assert len(oversized) >= self.scalar_size_bytes
127-
i = bytes_to_number(oversized)
128-
return i % self.q
137+
return password_to_scalar(pw, self.scalar_size_bytes, self.q)
129138

130139

131140
def arbitrary_element(self, seed):
132141
# we do *not* know the discrete log of this one. Nobody should.
133142
assert isinstance(seed, bytes)
134-
processed_seed = self.element_hasher(seed)[:self.element_size_bytes]
143+
processed_seed = expand_arbitrary_element_seed(seed,
144+
self.element_size_bytes)
135145
assert isinstance(processed_seed, bytes)
136146
assert len(processed_seed) == self.element_size_bytes
137147
# The larger (non-prime-order) group (Zp*) we're using has order
@@ -192,21 +202,6 @@ def _add(self, e1, e2):
192202
return _Element(self, (e1._e * e2._e) % self.p)
193203

194204

195-
def sha256(b):
196-
return hashlib.sha256(b).digest()
197-
def sha512(b):
198-
return hashlib.sha512(b).digest()
199-
def hash1024(b):
200-
return b"".join([sha512(b"0:"+b), sha512(b"1:"+b)])
201-
def hash2048(b):
202-
return b"".join([sha512(b"0:"+b), sha512(b"1:"+b),
203-
sha512(b"2:"+b), sha512(b"3:"+b)])
204-
def hash3072(b):
205-
return b"".join([sha512(b"0:"+b), sha512(b"1:"+b),
206-
sha512(b"2:"+b), sha512(b"3:"+b),
207-
sha512(b"4:"+b), sha512(b"5:"+b)])
208-
209-
210205
# This 1024-bit group originally came from the J-PAKE demo code,
211206
# http://haofeng66.googlepages.com/JPAKEDemo.java . That java code
212207
# recommended these 2048 and 3072 bit groups from this NIST document:
@@ -217,18 +212,18 @@ def hash3072(b):
217212
p=0xE0A67598CD1B763BC98C8ABB333E5DDA0CD3AA0E5E1FB5BA8A7B4EABC10BA338FAE06DD4B90FDA70D7CF0CB0C638BE3341BEC0AF8A7330A3307DED2299A0EE606DF035177A239C34A912C202AA5F83B9C4A7CF0235B5316BFC6EFB9A248411258B30B839AF172440F32563056CB67A861158DDD90E6A894C72A5BBEF9E286C6B,
218213
q=0xE950511EAB424B9A19A2AEB4E159B7844C589C4F,
219214
g=0xD29D5121B0423C2769AB21843E5A3240FF19CACC792264E3BB6BE4F78EDD1B15C4DFF7F1D905431F0AB16790E1F773B5CE01C804E509066A9919F5195F4ABC58189FD9FF987389CB5BEDF21B4DAB4F8B76A055FFE2770988FE2EC2DE11AD92219F0B351869AC24DA3D7BA87011A701CE8EE7BFE49486ED4527B7186CA4610A75,
220-
element_hasher = hash1024)
215+
)
221216

222217
# L=2048, N=224
223218
I2048 = IntegerGroup(
224219
p=0xC196BA05AC29E1F9C3C72D56DFFC6154A033F1477AC88EC37F09BE6C5BB95F51C296DD20D1A28A067CCC4D4316A4BD1DCA55ED1066D438C35AEBAABF57E7DAE428782A95ECA1C143DB701FD48533A3C18F0FE23557EA7AE619ECACC7E0B51652A8776D02A425567DED36EABD90CA33A1E8D988F0BBB92D02D1D20290113BB562CE1FC856EEB7CDD92D33EEA6F410859B179E7E789A8F75F645FAE2E136D252BFFAFF89528945C1ABE705A38DBC2D364AADE99BE0D0AAD82E5320121496DC65B3930E38047294FF877831A16D5228418DE8AB275D7D75651CEFED65F78AFC3EA7FE4D79B35F62A0402A1117599ADAC7B269A59F353CF450E6982D3B1702D9CA83,
225220
q=0x90EAF4D1AF0708B1B612FF35E0A2997EB9E9D263C9CE659528945C0D,
226221
g=0xA59A749A11242C58C894E9E5A91804E8FA0AC64B56288F8D47D51B1EDC4D65444FECA0111D78F35FC9FDD4CB1F1B79A3BA9CBEE83A3F811012503C8117F98E5048B089E387AF6949BF8784EBD9EF45876F2E6A5A495BE64B6E770409494B7FEE1DBB1E4B2BC2A53D4F893D418B7159592E4FFFDF6969E91D770DAEBD0B5CB14C00AD68EC7DC1E5745EA55C706C4A1C5C88964E34D09DEB753AD418C1AD0F4FDFD049A955E5D78491C0B7A2F1575A008CCD727AB376DB6E695515B05BD412F5B8C2F4C77EE10DA48ABD53F5DD498927EE7B692BBBCDA2FB23A516C5B4533D73980B2A3B60E384ED200AE21B40D273651AD6060C13D97FD69AA13C5611A51B9085,
227-
element_hasher=hash2048)
222+
)
228223

229224
# L=3072, N=256
230225
I3072 = IntegerGroup(
231226
p=0x90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA129F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D15939487E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773EBE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941DAD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504FB0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328EC22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB73,
232227
q=0xCFA0478A54717B08CE64805B76E5B14249A77A4838469DF7F7DC987EFCCFB11D,
233228
g=0x5E5CBA992E0A680D885EB903AEA78E4A45A469103D448EDE3B7ACCC54D521E37F84A4BDD5B06B0970CC2D2BBB715F7B82846F9A0C393914C792E6A923E2117AB805276A975AADB5261D91673EA9AAFFEECBFA6183DFCB5D3B7332AA19275AFA1F8EC0B60FB6F66CC23AE4870791D5982AAD1AA9485FD8F4A60126FEB2CF05DB8A7F0F09B3397F3937F2E90B9E5B9C9B6EFEF642BC48351C46FB171B9BFA9EF17A961CE96C7E7A7CC3D3D03DFAD1078BA21DA425198F07D2481622BCE45969D9C4D6063D72AB7A0F08B2F49A7CC6AF335E08C4720E31476B67299E231F8BD90B39AC3AE3BE0C6B6CACEF8289A2E2873D58E51E029CAFBD55E6841489AB66B5B4B9BA6E2F784660896AFF387D92844CCB8B69475496DE19DA2E58259B090489AC8E62363CDF82CFD8EF2A427ABCD65750B506F56DDE3B988567A88126B914D7828E2B63A6D7ED0747EC59E0E0A23CE7D8A74C1D2C2A7AFB6A29799620F00E11C33787F7DED3B30E1A22D09F1FBDA1ABBBFBF25CAE05A13F812E34563F99410E73B,
234-
element_hasher=hash3072)
229+
)

src/spake2/test/myhkdf.py

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
from hashlib import sha256, sha1
2+
from binascii import unhexlify
3+
import hmac
4+
5+
def HKDF(IKM, dkLen, salt=None, info=b"", digest=sha256,
6+
_test_expected_PRK=None):
7+
assert isinstance(IKM, bytes)
8+
assert isinstance(salt, (bytes,type(None)))
9+
assert isinstance(info, bytes)
10+
hlen = len(digest(b"").digest())
11+
assert dkLen <= hlen*255
12+
if salt is None:
13+
salt = b"\x00"*hlen
14+
# extract
15+
PRK = hmac.new(salt, IKM, digest).digest()
16+
if _test_expected_PRK and _test_expected_PRK != PRK:
17+
raise ValueError("test failed")
18+
# expand
19+
blocks = []
20+
counter = 1
21+
t = b""
22+
while hlen*len(blocks) < dkLen:
23+
t = hmac.new(PRK, t+info+unhexlify("%02x"%counter), digest).digest()
24+
blocks.append(t)
25+
counter += 1
26+
return b"".join(blocks)[:dkLen]
27+
28+
def power_on_self_test():
29+
from binascii import hexlify, unhexlify
30+
31+
def _test(IKM, salt, info, L, PRK, OKM, digest=sha256):
32+
def remove_prefix(prefix, s):
33+
assert s.startswith(prefix)
34+
return s[len(prefix):]
35+
ikm = unhexlify(remove_prefix("0x", IKM))
36+
salt = unhexlify(remove_prefix("0x", salt))
37+
info = unhexlify(remove_prefix("0x", info))
38+
prk = unhexlify(remove_prefix("0x", PRK))
39+
okm = unhexlify(remove_prefix("0x", OKM))
40+
assert isinstance(ikm, bytes)
41+
assert isinstance(salt, bytes)
42+
assert isinstance(info, bytes)
43+
assert isinstance(prk, bytes)
44+
assert isinstance(okm, bytes)
45+
if digest is None:
46+
out = HKDF(ikm, L, salt, info, _test_expected_PRK=prk)
47+
else:
48+
out = HKDF(ikm, L, salt, info, digest=digest,
49+
_test_expected_PRK=prk)
50+
if okm != out:
51+
raise ValueError("got %s, expected %s" % (hexlify(out), hexlify(okm)))
52+
53+
# test vectors from RFC5869
54+
_test(IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
55+
salt="0x000102030405060708090a0b0c",
56+
info="0xf0f1f2f3f4f5f6f7f8f9",
57+
L=42,
58+
PRK=("0x077709362c2e32df0ddc3f0dc47bba63"
59+
"90b6c73bb50f9c3122ec844ad7c2b3e5"),
60+
OKM=("0x3cb25f25faacd57a90434f64d0362f2a"
61+
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
62+
"34007208d5b887185865"))
63+
64+
_test(IKM=("0x000102030405060708090a0b0c0d0e0f"
65+
"101112131415161718191a1b1c1d1e1f"
66+
"202122232425262728292a2b2c2d2e2f"
67+
"303132333435363738393a3b3c3d3e3f"
68+
"404142434445464748494a4b4c4d4e4f"),
69+
salt=("0x606162636465666768696a6b6c6d6e6f"
70+
"707172737475767778797a7b7c7d7e7f"
71+
"808182838485868788898a8b8c8d8e8f"
72+
"909192939495969798999a9b9c9d9e9f"
73+
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"),
74+
info=("0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
75+
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
76+
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
77+
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
78+
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"),
79+
L=82,
80+
PRK=("0x06a6b88c5853361a06104c9ceb35b45c"
81+
"ef760014904671014a193f40c15fc244"),
82+
OKM=("0xb11e398dc80327a1c8e7f78c596a4934"
83+
"4f012eda2d4efad8a050cc4c19afa97c"
84+
"59045a99cac7827271cb41c65e590e09"
85+
"da3275600c2f09b8367793a9aca3db71"
86+
"cc30c58179ec3e87c14c01d5c1f3434f"
87+
"1d87"))
88+
89+
_test(IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
90+
salt="0x",
91+
info="0x",
92+
L=42,
93+
PRK=("0x19ef24a32c717b167f33a91d6f648bdf"
94+
"96596776afdb6377ac434c1c293ccb04"),
95+
OKM=("0x8da4e775a563c18f715f802a063c5a31"
96+
"b8a11f5c5ee1879ec3454e5f3c738d2d"
97+
"9d201395faa4b61a96c8"))
98+
99+
_test(digest=sha1,
100+
IKM="0x0b0b0b0b0b0b0b0b0b0b0b",
101+
salt="0x000102030405060708090a0b0c",
102+
info="0xf0f1f2f3f4f5f6f7f8f9",
103+
L=42,
104+
PRK="0x9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243",
105+
OKM=("0x085a01ea1b10f36933068b56efa5ad81"
106+
"a4f14b822f5b091568a9cdd4f155fda2"
107+
"c22e422478d305f3f896"))
108+
_test(digest=sha1,
109+
IKM=("0x000102030405060708090a0b0c0d0e0f"
110+
"101112131415161718191a1b1c1d1e1f"
111+
"202122232425262728292a2b2c2d2e2f"
112+
"303132333435363738393a3b3c3d3e3f"
113+
"404142434445464748494a4b4c4d4e4f"),
114+
salt=("0x606162636465666768696a6b6c6d6e6f"
115+
"707172737475767778797a7b7c7d7e7f"
116+
"808182838485868788898a8b8c8d8e8f"
117+
"909192939495969798999a9b9c9d9e9f"
118+
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"),
119+
info=("0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
120+
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
121+
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
122+
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
123+
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"),
124+
L=82,
125+
PRK="0x8adae09a2a307059478d309b26c4115a224cfaf6",
126+
OKM=("0x0bd770a74d1160f7c9f12cd5912a06eb"
127+
"ff6adcae899d92191fe4305673ba2ffe"
128+
"8fa3f1a4e5ad79f3f334b3b202b2173c"
129+
"486ea37ce3d397ed034c7f9dfeb15c5e"
130+
"927336d0441f4c4300e2cff0d0900b52"
131+
"d3b4"))
132+
_test(digest=sha1,
133+
IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
134+
salt="0x",
135+
info="0x",
136+
L=42,
137+
PRK="0xda8c8a73c7fa77288ec6f5e7c297786aa0d32d01",
138+
OKM=("0x0ac1af7002b3d761d1e55298da9d0506"
139+
"b9ae52057220a306e07b6b87e8df21d0"
140+
"ea00033de03984d34918"))
141+
142+
_test(digest=sha1,
143+
IKM="0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
144+
salt="0x",
145+
info="0x",
146+
L=42,
147+
PRK="0x2adccada18779e7c2077ad2eb19d3f3e731385dd",
148+
OKM=("0x2c91117204d745f3500d636a62f64f0a"
149+
"b3bae548aa53d423b0d1f27ebba6f5e5"
150+
"673a081d70cce7acfc48"))
151+
152+
# finally test that HKDF() without a digest= uses SHA256
153+
154+
_test(digest=None,
155+
IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
156+
salt="0x",
157+
info="0x",
158+
L=42,
159+
PRK=("0x19ef24a32c717b167f33a91d6f648bdf"
160+
"96596776afdb6377ac434c1c293ccb04"),
161+
OKM=("0x8da4e775a563c18f715f802a063c5a31"
162+
"b8a11f5c5ee1879ec3454e5f3c738d2d"
163+
"9d201395faa4b61a96c8"))
164+
#print "all test passed"
165+
166+
power_on_self_test()

0 commit comments

Comments
 (0)