diff options
Diffstat (limited to 'python-src/curve25519')
-rw-r--r-- | python-src/curve25519/__init__.py | 4 | ||||
-rw-r--r-- | python-src/curve25519/curve25519module.c | 105 | ||||
-rw-r--r-- | python-src/curve25519/keys.py | 46 | ||||
-rw-r--r-- | python-src/curve25519/test/__init__.py | 0 | ||||
-rwxr-xr-x | python-src/curve25519/test/test_curve25519.py | 99 | ||||
-rwxr-xr-x | python-src/curve25519/test/test_speed.py | 46 |
6 files changed, 300 insertions, 0 deletions
diff --git a/python-src/curve25519/__init__.py b/python-src/curve25519/__init__.py new file mode 100644 index 0000000..873ff57 --- /dev/null +++ b/python-src/curve25519/__init__.py @@ -0,0 +1,4 @@ + +from .keys import Private, Public + +hush_pyflakes = [Private, Public]; del hush_pyflakes diff --git a/python-src/curve25519/curve25519module.c b/python-src/curve25519/curve25519module.c new file mode 100644 index 0000000..e309ec0 --- /dev/null +++ b/python-src/curve25519/curve25519module.c @@ -0,0 +1,105 @@ +/* tell python that PyArg_ParseTuple(t#) means Py_ssize_t, not int */ +#define PY_SSIZE_T_CLEAN +#include <Python.h> +#if (PY_VERSION_HEX < 0x02050000) + typedef int Py_ssize_t; +#endif + +/* This is required for compatibility with Python 2. */ +#if PY_MAJOR_VERSION >= 3 + #include <bytesobject.h> + #define y "y" +#else + #define PyBytes_FromStringAndSize PyString_FromStringAndSize + #define y "t" +#endif + +int curve25519_donna(char *mypublic, + const char *secret, const char *basepoint); + +static PyObject * +pycurve25519_makeprivate(PyObject *self, PyObject *args) +{ + char *in1; + Py_ssize_t in1len; + if (!PyArg_ParseTuple(args, y"#:clamp", &in1, &in1len)) + return NULL; + if (in1len != 32) { + PyErr_SetString(PyExc_ValueError, "input must be 32-byte string"); + return NULL; + } + in1[0] &= 248; + in1[31] &= 127; + in1[31] |= 64; + return PyBytes_FromStringAndSize((char *)in1, 32); +} + +static PyObject * +pycurve25519_makepublic(PyObject *self, PyObject *args) +{ + const char *private; + char mypublic[32]; + char basepoint[32] = {9}; + Py_ssize_t privatelen; + if (!PyArg_ParseTuple(args, y"#:makepublic", &private, &privatelen)) + return NULL; + if (privatelen != 32) { + PyErr_SetString(PyExc_ValueError, "input must be 32-byte string"); + return NULL; + } + curve25519_donna(mypublic, private, basepoint); + return PyBytes_FromStringAndSize((char *)mypublic, 32); +} + +static PyObject * +pycurve25519_makeshared(PyObject *self, PyObject *args) +{ + const char *myprivate, *theirpublic; + char shared_key[32]; + Py_ssize_t myprivatelen, theirpubliclen; + if (!PyArg_ParseTuple(args, y"#"y"#:generate", + &myprivate, &myprivatelen, &theirpublic, &theirpubliclen)) + return NULL; + if (myprivatelen != 32) { + PyErr_SetString(PyExc_ValueError, "input must be 32-byte string"); + return NULL; + } + if (theirpubliclen != 32) { + PyErr_SetString(PyExc_ValueError, "input must be 32-byte string"); + return NULL; + } + curve25519_donna(shared_key, myprivate, theirpublic); + return PyBytes_FromStringAndSize((char *)shared_key, 32); +} + + +static PyMethodDef +curve25519_functions[] = { + {"make_private", pycurve25519_makeprivate, METH_VARARGS, "data->private"}, + {"make_public", pycurve25519_makepublic, METH_VARARGS, "private->public"}, + {"make_shared", pycurve25519_makeshared, METH_VARARGS, "private+public->shared"}, + {NULL, NULL, 0, NULL}, +}; + +#if PY_MAJOR_VERSION >= 3 + static struct PyModuleDef + curve25519_module = { + PyModuleDef_HEAD_INIT, + "_curve25519", + NULL, + NULL, + curve25519_functions, + }; + + PyObject * + PyInit__curve25519(void) + { + return PyModule_Create(&curve25519_module); + } +#else + PyMODINIT_FUNC + init_curve25519(void) + { + (void)Py_InitModule("_curve25519", curve25519_functions); + } +#endif
\ No newline at end of file diff --git a/python-src/curve25519/keys.py b/python-src/curve25519/keys.py new file mode 100644 index 0000000..e131dac --- /dev/null +++ b/python-src/curve25519/keys.py @@ -0,0 +1,46 @@ +from . import _curve25519 +from hashlib import sha256 +import os + +# the curve25519 functions are really simple, and could be used without an +# OOP layer, but it's a bit too easy to accidentally swap the private and +# public keys that way. + +def _hash_shared(shared): + return sha256(b"curve25519-shared:"+shared).digest() + +class Private: + def __init__(self, secret=None, seed=None): + if secret is None: + if seed is None: + secret = os.urandom(32) + else: + secret = sha256(b"curve25519-private:"+seed).digest() + else: + assert seed is None, "provide secret, seed, or neither, not both" + if not isinstance(secret, bytes) or len(secret) != 32: + raise TypeError("secret= must be 32-byte string") + self.private = _curve25519.make_private(secret) + + def serialize(self): + return self.private + + def get_public(self): + return Public(_curve25519.make_public(self.private)) + + def get_shared_key(self, public, hashfunc=None): + if not isinstance(public, Public): + raise ValueError("'public' must be an instance of Public") + if hashfunc is None: + hashfunc = _hash_shared + shared = _curve25519.make_shared(self.private, public.public) + return hashfunc(shared) + +class Public: + def __init__(self, public): + assert isinstance(public, bytes) + assert len(public) == 32 + self.public = public + + def serialize(self): + return self.public diff --git a/python-src/curve25519/test/__init__.py b/python-src/curve25519/test/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/python-src/curve25519/test/__init__.py diff --git a/python-src/curve25519/test/test_curve25519.py b/python-src/curve25519/test/test_curve25519.py new file mode 100755 index 0000000..2ecbd47 --- /dev/null +++ b/python-src/curve25519/test/test_curve25519.py @@ -0,0 +1,99 @@ +#! /usr/bin/python + +import unittest + +from curve25519 import Private, Public +from hashlib import sha1, sha256 +from binascii import hexlify + +class Basic(unittest.TestCase): + def test_basic(self): + secret1 = b"abcdefghijklmnopqrstuvwxyz123456" + self.assertEqual(len(secret1), 32) + + secret2 = b"654321zyxwvutsrqponmlkjihgfedcba" + self.assertEqual(len(secret2), 32) + priv1 = Private(secret=secret1) + pub1 = priv1.get_public() + priv2 = Private(secret=secret2) + pub2 = priv2.get_public() + shared12 = priv1.get_shared_key(pub2) + e = b"b0818125eab42a8ac1af5e8b9b9c15ed2605c2bbe9675de89e5e6e7f442b9598" + self.assertEqual(hexlify(shared12), e) + shared21 = priv2.get_shared_key(pub1) + self.assertEqual(shared12, shared21) + + pub2a = Public(pub2.serialize()) + shared12a = priv1.get_shared_key(pub2a) + self.assertEqual(hexlify(shared12a), e) + + def test_errors(self): + priv1 = Private() + self.assertRaises(ValueError, priv1.get_shared_key, priv1) + + def test_seed(self): + # use 32-byte secret + self.assertRaises(TypeError, Private, secret=123) + self.assertRaises(TypeError, Private, secret=b"too short") + secret1 = b"abcdefghijklmnopqrstuvwxyz123456" + assert len(secret1) == 32 + priv1 = Private(secret=secret1) + priv1a = Private(secret=secret1) + priv1b = Private(priv1.serialize()) + self.assertEqual(priv1.serialize(), priv1a.serialize()) + self.assertEqual(priv1.serialize(), priv1b.serialize()) + e = b"6062636465666768696a6b6c6d6e6f707172737475767778797a313233343576" + self.assertEqual(hexlify(priv1.serialize()), e) + + # the private key is a clamped form of the secret, so they won't + # quite be the same + p = Private(secret=b"\x00"*32) + self.assertEqual(hexlify(p.serialize()), b"00"*31+b"40") + p = Private(secret=b"\xff"*32) + self.assertEqual(hexlify(p.serialize()), b"f8"+b"ff"*30+b"7f") + + # use arbitrary-length seed + self.assertRaises(TypeError, Private, seed=123) + priv1 = Private(seed=b"abc") + priv1a = Private(seed=b"abc") + priv1b = Private(priv1.serialize()) + self.assertEqual(priv1.serialize(), priv1a.serialize()) + self.assertEqual(priv1.serialize(), priv1b.serialize()) + self.assertRaises(AssertionError, Private, seed=b"abc", secret=b"no") + + priv1 = Private(seed=b"abc") + priv1a = Private(priv1.serialize()) + self.assertEqual(priv1.serialize(), priv1a.serialize()) + self.assertRaises(AssertionError, Private, seed=b"abc", secret=b"no") + + # use built-in os.urandom + priv2 = Private() + priv2a = Private(priv2.private) + self.assertEqual(priv2.serialize(), priv2a.serialize()) + + # attempt to use both secret= and seed=, not allowed + self.assertRaises(AssertionError, Private, seed=b"abc", secret=b"no") + + def test_hashfunc(self): + priv1 = Private(seed=b"abc") + priv2 = Private(seed=b"def") + shared_sha256 = priv1.get_shared_key(priv2.get_public()) + e = b"da959ffe77ebeb4757fe5ba310e28ede425ae0d0ff5ec9c884e2d08f311cf5e5" + self.assertEqual(hexlify(shared_sha256), e) + + # confirm the hash function remains what we think it is + def myhash(shared_key): + return sha256(b"curve25519-shared:"+shared_key).digest() + shared_myhash = priv1.get_shared_key(priv2.get_public(), myhash) + self.assertEqual(hexlify(shared_myhash), e) + + def hexhash(shared_key): + return sha1(shared_key).hexdigest().encode() + shared_hexhash = priv1.get_shared_key(priv2.get_public(), hexhash) + self.assertEqual(shared_hexhash, + b"80eec98222c8edc4324fb9477a3c775ce7c6c93a") + + +if __name__ == "__main__": + unittest.main() + diff --git a/python-src/curve25519/test/test_speed.py b/python-src/curve25519/test/test_speed.py new file mode 100755 index 0000000..87952fa --- /dev/null +++ b/python-src/curve25519/test/test_speed.py @@ -0,0 +1,46 @@ +#! /usr/bin/python + +from time import time +from curve25519 import Private + +count = 10000 +elapsed_get_public = 0.0 +elapsed_get_shared = 0.0 + +def abbreviate_time(data): + # 1.23s, 790ms, 132us + if data is None: + return "" + s = float(data) + if s >= 10: + #return abbreviate.abbreviate_time(data) + return "%d" % s + if s >= 1.0: + return "%.2fs" % s + if s >= 0.01: + return "%dms" % (1000*s) + if s >= 0.001: + return "%.1fms" % (1000*s) + if s >= 0.000001: + return "%.1fus" % (1000000*s) + return "%dns" % (1000000000*s) + +def nohash(key): return key + +for i in range(count): + p = Private() + start = time() + pub = p.get_public() + elapsed_get_public += time() - start + pub2 = Private().get_public() + start = time() + shared = p.get_shared_key(pub2) #, hashfunc=nohash) + elapsed_get_shared += time() - start + +print("get_public: %s" % abbreviate_time(elapsed_get_public / count)) +print("get_shared: %s" % abbreviate_time(elapsed_get_shared / count)) + +# these take about 560us-570us each (with the default compiler settings, -Os) +# on my laptop, same with -O2 +# of which the python overhead is about 5us +# and the get_shared_key() hash step adds about 5us |