Skip to content

Commit

Permalink
Merge branch 'hkdf' (COMPATIBILITY BREAK)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
warner committed May 12, 2016
2 parents 94b8009 + eb15e64 commit 067e094
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 60 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ python:
- "3.4"
- "3.5"
- "pypy"
- "pypy3"
install:
- pip install -U tox virtualenv coverage python-coveralls
script:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

* License: MIT
* Dependencies: none (pure-python)
* Compatible With: Python 2.6, 2.7, 3.3, 3.4, 3.5, pypy2, pypy3
* Compatible With: Python 2.6, 2.7, 3.3, 3.4, 3.5, pypy2
* [![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)

This library implements the SPAKE2 password-authenticated key exchange
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ def abbrev(t):
"Programming Language :: Python :: 3.5",
"Topic :: Security :: Cryptography",
],
install_requires=["hkdf"],
)
8 changes: 6 additions & 2 deletions src/spake2/ed25519_basic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import binascii, hashlib, itertools
from .groups import expand_arbitrary_element_seed

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


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

# we try successive Y values until we find a valid point
Expand Down
3 changes: 2 additions & 1 deletion src/spake2/ed25519_group.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import ed25519_basic
from .groups import password_to_scalar

class _Ed25519Group:
def random_scalar(self, entropy_f):
Expand All @@ -8,7 +9,7 @@ def scalar_to_bytes(self, i):
def bytes_to_scalar(self, b):
return ed25519_basic.bytes_to_scalar(b)
def password_to_scalar(self, pw):
return ed25519_basic.password_to_scalar(pw)
return password_to_scalar(pw, self.scalar_size_bytes, self.order())
def arbitrary_element(self, seed):
return ed25519_basic.arbitrary_element(seed)
def bytes_to_element(self, b):
Expand Down
57 changes: 26 additions & 31 deletions src/spake2/groups.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import division
import hashlib
from hkdf import Hkdf
from .six import integer_types
from .util import (size_bits, size_bytes, unbiased_randrange,
bytes_to_number, number_to_bytes)
Expand Down Expand Up @@ -61,6 +62,24 @@
"""


def expand_password(data, num_bytes):
h = Hkdf(salt=b"", input_key_material=data, hash=hashlib.sha256)
info = b"SPAKE2 pw"
return h.expand(info, num_bytes)

def password_to_scalar(pw, scalar_size_bytes, q):
assert isinstance(pw, bytes)
# the oversized hash reduces bias in the result, so
# uniformly-random passwords give nearly-uniform scalars
oversized = expand_password(pw, scalar_size_bytes+16)
assert len(oversized) >= scalar_size_bytes
i = bytes_to_number(oversized)
return i % q

def expand_arbitrary_element_seed(data, num_bytes):
h = Hkdf(salt=b"", input_key_material=data, hash=hashlib.sha256)
info = b"SPAKE2 arbitrary element"
return h.expand(info, num_bytes)

class _Element:
def __init__(self, group, e):
Expand All @@ -76,7 +95,7 @@ def to_bytes(self):
return self._group._element_to_bytes(self)

class IntegerGroup:
def __init__(self, p, q, g, element_hasher):
def __init__(self, p, q, g):
self.q = q # the subgroup order, used for scalars
self.scalar_size_bytes = size_bytes(self.q)
_s = self.scalar_to_bytes(self.password_to_scalar(b""))
Expand All @@ -89,10 +108,6 @@ def __init__(self, p, q, g, element_hasher):
self.p = p # the field size
self.element_size_bits = size_bits(self.p)
self.element_size_bytes = size_bytes(self.p)
_e = element_hasher(b"")
assert isinstance(_e, bytes)
assert len(_e) >= self.element_size_bytes
self.element_hasher = element_hasher

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

def password_to_scalar(self, pw):
assert isinstance(pw, bytes)
# the oversized hash reduces bias in the result, so
# uniformly-random passwords give nearly-uniform scalars
oversized = hashlib.sha512(pw).digest()
assert len(oversized) >= self.scalar_size_bytes
i = bytes_to_number(oversized)
return i % self.q
return password_to_scalar(pw, self.scalar_size_bytes, self.q)


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


def sha256(b):
return hashlib.sha256(b).digest()
def sha512(b):
return hashlib.sha512(b).digest()
def hash1024(b):
return b"".join([sha512(b"0:"+b), sha512(b"1:"+b)])
def hash2048(b):
return b"".join([sha512(b"0:"+b), sha512(b"1:"+b),
sha512(b"2:"+b), sha512(b"3:"+b)])
def hash3072(b):
return b"".join([sha512(b"0:"+b), sha512(b"1:"+b),
sha512(b"2:"+b), sha512(b"3:"+b),
sha512(b"4:"+b), sha512(b"5:"+b)])


# This 1024-bit group originally came from the J-PAKE demo code,
# http://haofeng66.googlepages.com/JPAKEDemo.java . That java code
# recommended these 2048 and 3072 bit groups from this NIST document:
Expand All @@ -217,18 +212,18 @@ def hash3072(b):
p=0xE0A67598CD1B763BC98C8ABB333E5DDA0CD3AA0E5E1FB5BA8A7B4EABC10BA338FAE06DD4B90FDA70D7CF0CB0C638BE3341BEC0AF8A7330A3307DED2299A0EE606DF035177A239C34A912C202AA5F83B9C4A7CF0235B5316BFC6EFB9A248411258B30B839AF172440F32563056CB67A861158DDD90E6A894C72A5BBEF9E286C6B,
q=0xE950511EAB424B9A19A2AEB4E159B7844C589C4F,
g=0xD29D5121B0423C2769AB21843E5A3240FF19CACC792264E3BB6BE4F78EDD1B15C4DFF7F1D905431F0AB16790E1F773B5CE01C804E509066A9919F5195F4ABC58189FD9FF987389CB5BEDF21B4DAB4F8B76A055FFE2770988FE2EC2DE11AD92219F0B351869AC24DA3D7BA87011A701CE8EE7BFE49486ED4527B7186CA4610A75,
element_hasher = hash1024)
)

# L=2048, N=224
I2048 = IntegerGroup(
p=0xC196BA05AC29E1F9C3C72D56DFFC6154A033F1477AC88EC37F09BE6C5BB95F51C296DD20D1A28A067CCC4D4316A4BD1DCA55ED1066D438C35AEBAABF57E7DAE428782A95ECA1C143DB701FD48533A3C18F0FE23557EA7AE619ECACC7E0B51652A8776D02A425567DED36EABD90CA33A1E8D988F0BBB92D02D1D20290113BB562CE1FC856EEB7CDD92D33EEA6F410859B179E7E789A8F75F645FAE2E136D252BFFAFF89528945C1ABE705A38DBC2D364AADE99BE0D0AAD82E5320121496DC65B3930E38047294FF877831A16D5228418DE8AB275D7D75651CEFED65F78AFC3EA7FE4D79B35F62A0402A1117599ADAC7B269A59F353CF450E6982D3B1702D9CA83,
q=0x90EAF4D1AF0708B1B612FF35E0A2997EB9E9D263C9CE659528945C0D,
g=0xA59A749A11242C58C894E9E5A91804E8FA0AC64B56288F8D47D51B1EDC4D65444FECA0111D78F35FC9FDD4CB1F1B79A3BA9CBEE83A3F811012503C8117F98E5048B089E387AF6949BF8784EBD9EF45876F2E6A5A495BE64B6E770409494B7FEE1DBB1E4B2BC2A53D4F893D418B7159592E4FFFDF6969E91D770DAEBD0B5CB14C00AD68EC7DC1E5745EA55C706C4A1C5C88964E34D09DEB753AD418C1AD0F4FDFD049A955E5D78491C0B7A2F1575A008CCD727AB376DB6E695515B05BD412F5B8C2F4C77EE10DA48ABD53F5DD498927EE7B692BBBCDA2FB23A516C5B4533D73980B2A3B60E384ED200AE21B40D273651AD6060C13D97FD69AA13C5611A51B9085,
element_hasher=hash2048)
)

# L=3072, N=256
I3072 = IntegerGroup(
p=0x90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA129F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D15939487E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773EBE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941DAD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504FB0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328EC22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB73,
q=0xCFA0478A54717B08CE64805B76E5B14249A77A4838469DF7F7DC987EFCCFB11D,
g=0x5E5CBA992E0A680D885EB903AEA78E4A45A469103D448EDE3B7ACCC54D521E37F84A4BDD5B06B0970CC2D2BBB715F7B82846F9A0C393914C792E6A923E2117AB805276A975AADB5261D91673EA9AAFFEECBFA6183DFCB5D3B7332AA19275AFA1F8EC0B60FB6F66CC23AE4870791D5982AAD1AA9485FD8F4A60126FEB2CF05DB8A7F0F09B3397F3937F2E90B9E5B9C9B6EFEF642BC48351C46FB171B9BFA9EF17A961CE96C7E7A7CC3D3D03DFAD1078BA21DA425198F07D2481622BCE45969D9C4D6063D72AB7A0F08B2F49A7CC6AF335E08C4720E31476B67299E231F8BD90B39AC3AE3BE0C6B6CACEF8289A2E2873D58E51E029CAFBD55E6841489AB66B5B4B9BA6E2F784660896AFF387D92844CCB8B69475496DE19DA2E58259B090489AC8E62363CDF82CFD8EF2A427ABCD65750B506F56DDE3B988567A88126B914D7828E2B63A6D7ED0747EC59E0E0A23CE7D8A74C1D2C2A7AFB6A29799620F00E11C33787F7DED3B30E1A22D09F1FBDA1ABBBFBF25CAE05A13F812E34563F99410E73B,
element_hasher=hash3072)
)
166 changes: 166 additions & 0 deletions src/spake2/test/myhkdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from hashlib import sha256, sha1
from binascii import unhexlify
import hmac

def HKDF(IKM, dkLen, salt=None, info=b"", digest=sha256,
_test_expected_PRK=None):
assert isinstance(IKM, bytes)
assert isinstance(salt, (bytes,type(None)))
assert isinstance(info, bytes)
hlen = len(digest(b"").digest())
assert dkLen <= hlen*255
if salt is None:
salt = b"\x00"*hlen
# extract
PRK = hmac.new(salt, IKM, digest).digest()
if _test_expected_PRK and _test_expected_PRK != PRK:
raise ValueError("test failed")
# expand
blocks = []
counter = 1
t = b""
while hlen*len(blocks) < dkLen:
t = hmac.new(PRK, t+info+unhexlify("%02x"%counter), digest).digest()
blocks.append(t)
counter += 1
return b"".join(blocks)[:dkLen]

def power_on_self_test():
from binascii import hexlify, unhexlify

def _test(IKM, salt, info, L, PRK, OKM, digest=sha256):
def remove_prefix(prefix, s):
assert s.startswith(prefix)
return s[len(prefix):]
ikm = unhexlify(remove_prefix("0x", IKM))
salt = unhexlify(remove_prefix("0x", salt))
info = unhexlify(remove_prefix("0x", info))
prk = unhexlify(remove_prefix("0x", PRK))
okm = unhexlify(remove_prefix("0x", OKM))
assert isinstance(ikm, bytes)
assert isinstance(salt, bytes)
assert isinstance(info, bytes)
assert isinstance(prk, bytes)
assert isinstance(okm, bytes)
if digest is None:
out = HKDF(ikm, L, salt, info, _test_expected_PRK=prk)
else:
out = HKDF(ikm, L, salt, info, digest=digest,
_test_expected_PRK=prk)
if okm != out:
raise ValueError("got %s, expected %s" % (hexlify(out), hexlify(okm)))

# test vectors from RFC5869
_test(IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt="0x000102030405060708090a0b0c",
info="0xf0f1f2f3f4f5f6f7f8f9",
L=42,
PRK=("0x077709362c2e32df0ddc3f0dc47bba63"
"90b6c73bb50f9c3122ec844ad7c2b3e5"),
OKM=("0x3cb25f25faacd57a90434f64d0362f2a"
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
"34007208d5b887185865"))

_test(IKM=("0x000102030405060708090a0b0c0d0e0f"
"101112131415161718191a1b1c1d1e1f"
"202122232425262728292a2b2c2d2e2f"
"303132333435363738393a3b3c3d3e3f"
"404142434445464748494a4b4c4d4e4f"),
salt=("0x606162636465666768696a6b6c6d6e6f"
"707172737475767778797a7b7c7d7e7f"
"808182838485868788898a8b8c8d8e8f"
"909192939495969798999a9b9c9d9e9f"
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"),
info=("0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"),
L=82,
PRK=("0x06a6b88c5853361a06104c9ceb35b45c"
"ef760014904671014a193f40c15fc244"),
OKM=("0xb11e398dc80327a1c8e7f78c596a4934"
"4f012eda2d4efad8a050cc4c19afa97c"
"59045a99cac7827271cb41c65e590e09"
"da3275600c2f09b8367793a9aca3db71"
"cc30c58179ec3e87c14c01d5c1f3434f"
"1d87"))

_test(IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt="0x",
info="0x",
L=42,
PRK=("0x19ef24a32c717b167f33a91d6f648bdf"
"96596776afdb6377ac434c1c293ccb04"),
OKM=("0x8da4e775a563c18f715f802a063c5a31"
"b8a11f5c5ee1879ec3454e5f3c738d2d"
"9d201395faa4b61a96c8"))

_test(digest=sha1,
IKM="0x0b0b0b0b0b0b0b0b0b0b0b",
salt="0x000102030405060708090a0b0c",
info="0xf0f1f2f3f4f5f6f7f8f9",
L=42,
PRK="0x9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243",
OKM=("0x085a01ea1b10f36933068b56efa5ad81"
"a4f14b822f5b091568a9cdd4f155fda2"
"c22e422478d305f3f896"))
_test(digest=sha1,
IKM=("0x000102030405060708090a0b0c0d0e0f"
"101112131415161718191a1b1c1d1e1f"
"202122232425262728292a2b2c2d2e2f"
"303132333435363738393a3b3c3d3e3f"
"404142434445464748494a4b4c4d4e4f"),
salt=("0x606162636465666768696a6b6c6d6e6f"
"707172737475767778797a7b7c7d7e7f"
"808182838485868788898a8b8c8d8e8f"
"909192939495969798999a9b9c9d9e9f"
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"),
info=("0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"),
L=82,
PRK="0x8adae09a2a307059478d309b26c4115a224cfaf6",
OKM=("0x0bd770a74d1160f7c9f12cd5912a06eb"
"ff6adcae899d92191fe4305673ba2ffe"
"8fa3f1a4e5ad79f3f334b3b202b2173c"
"486ea37ce3d397ed034c7f9dfeb15c5e"
"927336d0441f4c4300e2cff0d0900b52"
"d3b4"))
_test(digest=sha1,
IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt="0x",
info="0x",
L=42,
PRK="0xda8c8a73c7fa77288ec6f5e7c297786aa0d32d01",
OKM=("0x0ac1af7002b3d761d1e55298da9d0506"
"b9ae52057220a306e07b6b87e8df21d0"
"ea00033de03984d34918"))

_test(digest=sha1,
IKM="0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
salt="0x",
info="0x",
L=42,
PRK="0x2adccada18779e7c2077ad2eb19d3f3e731385dd",
OKM=("0x2c91117204d745f3500d636a62f64f0a"
"b3bae548aa53d423b0d1f27ebba6f5e5"
"673a081d70cce7acfc48"))

# finally test that HKDF() without a digest= uses SHA256

_test(digest=None,
IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt="0x",
info="0x",
L=42,
PRK=("0x19ef24a32c717b167f33a91d6f648bdf"
"96596776afdb6377ac434c1c293ccb04"),
OKM=("0x8da4e775a563c18f715f802a063c5a31"
"b8a11f5c5ee1879ec3454e5f3c738d2d"
"9d201395faa4b61a96c8"))
#print "all test passed"

power_on_self_test()
Loading

0 comments on commit 067e094

Please sign in to comment.