aboutsummaryrefslogtreecommitdiff
path: root/python-src/curve25519
diff options
context:
space:
mode:
authorMark Haines <mjark@negativecurvature.net>2015-02-26 16:40:56 +0000
committerMark Haines <mjark@negativecurvature.net>2015-02-26 16:40:56 +0000
commite50ac707316ea6d8059f7036322450727773952d (patch)
tree0cd42bcd79e20f54f4944522576a1e35e231ddd5 /python-src/curve25519
Squashed 'lib/curve25519-donna/' content from commit 28772f3
git-subtree-dir: lib/curve25519-donna git-subtree-split: 28772f37a4b8a57ab9439b9e79b19f9abee686da
Diffstat (limited to 'python-src/curve25519')
-rw-r--r--python-src/curve25519/__init__.py4
-rw-r--r--python-src/curve25519/curve25519module.c105
-rw-r--r--python-src/curve25519/keys.py46
-rw-r--r--python-src/curve25519/test/__init__.py0
-rwxr-xr-xpython-src/curve25519/test/test_curve25519.py99
-rwxr-xr-xpython-src/curve25519/test/test_speed.py46
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