Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chacha20 python bindings #34

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
'src/pycryptopp/hash/sha256module.cpp',
'src/pycryptopp/cipher/aesmodule.cpp',
'src/pycryptopp/cipher/xsalsa20module.cpp',
'src/pycryptopp/cipher/chacha20module.cpp',
]
if ECDSA:
srcs.append('src/pycryptopp/publickey/ecdsamodule.cpp')
Expand Down
3 changes: 3 additions & 0 deletions src/pycryptopp/_pycryptoppmodule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "hash/sha256module.hpp"
#include "cipher/aesmodule.hpp"
#include "cipher/xsalsa20module.hpp"
#include "cipher/chacha20module.hpp"

/* from Crypto++ */
#ifdef DISABLE_EMBEDDED_CRYPTOPP
Expand All @@ -22,6 +23,7 @@ from pycryptopp.publickey import rsa\n\
from pycryptopp import cipher\n\
from pycryptopp.cipher import aes\n\
from pycryptopp.cipher import xsalsa20\n\
from pycryptopp.cipher import chacha20\n\
from pycryptopp import hash\n\
from pycryptopp.hash import sha256");

Expand Down Expand Up @@ -69,4 +71,5 @@ init_pycryptopp(void) {
init_sha256(module);
init_aes(module);
init_xsalsa20(module);
init_chacha20(module);
}
3 changes: 2 additions & 1 deletion src/pycryptopp/cipher/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import aes
import xsalsa20
import chacha20

quiet_pyflakes=[aes, xsalsa20]
quiet_pyflakes=[aes, xsalsa20, chacha20]
31 changes: 31 additions & 0 deletions src/pycryptopp/cipher/chacha20.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pycryptopp import _import_my_names

_import_my_names(globals(), "chacha20_")

del _import_my_names

def selftest():
# pyflakes doesn't know that Chacha20 is made available above
Chacha20 = globals()["ChaCha20"]
from binascii import unhexlify
key = unhexlify("ad5eadf7163b0d36e44c126037a03419"
"fcda2b3a1bb4ab064b6070e61b0fa5ca")
iv = unhexlify("6a059adb8c7d4acb1c537767d541506f" "c5ef0ace9a2a65bd")
encrypted = unhexlify("6c845801d0df33d8aa5ad8c8ff3ebfd5"
"9ab66b64f2157e8c6521e0c34ef0f233"
"baf02fa7a2c289d1b725905667696ac9"
"ba966d72b2d6cac601")
p = Chacha20(key, iv)
decrypted = p.process(encrypted)
expected = "crypto libraries should always test themselves at powerup"
assert decrypted == expected

p = Chacha20(key, iv)
decrypted = ""
offset = 0
for chunksize in [13,11,1,2,3,20,999]:
decrypted += p.process(encrypted[offset:offset+chunksize])
offset += chunksize
assert decrypted == expected

selftest()
180 changes: 180 additions & 0 deletions src/pycryptopp/cipher/chacha20module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* chacha20module.cpp -- Python wrappers around Crypto++'s chacha20
*/

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#if (PY_VERSION_HEX < 0x02050000)
typedef int Py_ssize_t;
#endif

#include "chacha20module.hpp"

#ifdef DISABLE_EMBEDDED_CRYPTOPP
#include <cryptopp/chacha.h>
#else
#include <src-cryptopp/chacha.h>
#endif

static const char* const chacha20__doc__ = "_chacha20 cipher";

static PyObject *chacha20_error;

typedef struct {
PyObject_HEAD

/* internal */
//CryptoPP::CTR_Mode<CryptoPP::ChaCha20>::Encryption *e;
CryptoPP::ChaCha20::Encryption *e;
} ChaCha20;

PyDoc_STRVAR(ChaCha20__doc__,
"An ChaCha20 cipher object.\n\
\n\
This object encrypts/decrypts in CTR mode, using a counter that is initialized\n\
to zero when you instantiate the object. Successive calls to .process() will \n\
use the current counter and increment it.\n\
\n\
");

static PyObject *ChaCha20_process(ChaCha20* self, PyObject* msgobj) {
if(!PyString_CheckExact(msgobj)) {
PyStringObject* typerepr = reinterpret_cast<PyStringObject*>(PyObject_Repr(reinterpret_cast<PyObject*>(msgobj->ob_type)));
if (typerepr) {
PyErr_Format(chacha20_error, "Precondition violation: you are required to pass a Python string object (not a unicode, a subclass of string, or anything else), but you passed %s.", PyString_AS_STRING(reinterpret_cast<PyObject*>(typerepr)));
Py_DECREF(typerepr);
} else
PyErr_Format(chacha20_error, "Precondition violation: you are required to pass a Python string object (not a unicode, a subclass of string, or anything else).");
return NULL;
}

const char* msg;
Py_ssize_t msgsize;
if (PyString_AsStringAndSize(msgobj, const_cast<char**>(&msg), &msgsize))
return NULL;
assert (msgsize >= 0);

PyStringObject* result = reinterpret_cast<PyStringObject*>(PyString_FromStringAndSize(NULL, msgsize));
if (!result)
return NULL;

self->e->ProcessString(reinterpret_cast<byte*>(PyString_AS_STRING(result)), reinterpret_cast<const byte*>(msg), msgsize);
return reinterpret_cast<PyObject*>(result);
}

PyDoc_STRVAR(ChaCha20_process__doc__,
"Encrypt or decrypt the next bytes, returning the result.");

static PyMethodDef ChaCha20_methods[] = {
{"process", reinterpret_cast<PyCFunction>(ChaCha20_process), METH_O, ChaCha20_process__doc__},
{NULL},
};

static PyObject* ChaCha20_new(PyTypeObject* type, PyObject *args, PyObject *kwdict) {
ChaCha20* self = reinterpret_cast<ChaCha20*>(type->tp_alloc(type, 0));
if (!self)
return NULL;
self->e = NULL;
return reinterpret_cast<PyObject*>(self);
}

static void ChaCha20_dealloc(PyObject* self) {
if (reinterpret_cast<ChaCha20*>(self)->e)
delete reinterpret_cast<ChaCha20*>(self)->e;
self->ob_type->tp_free(self);
}

static int ChaCha20_init(PyObject* self, PyObject *args, PyObject *kwdict) {
static const char *kwlist[] = { "key", "iv", NULL};
const char *key = NULL;
Py_ssize_t keysize = 0;
const char *iv = NULL;
const char defaultiv[24] = {0};
Py_ssize_t ivsize = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "t#|t#:ChaCha20.__init__", const_cast<char**>(kwlist), &key, &keysize, &iv, &ivsize))
return -1;
assert (keysize >= 0);
assert (ivsize >= 0);

if (keysize != 32 && keysize != 16 ) {
PyErr_Format(chacha20_error, "Precondition violation: key must be exactly 32 or 16 bytes, not %d", keysize);
return -1;
}
if (!iv)
iv = defaultiv;
else if (ivsize != 24) {
PyErr_Format(chacha20_error, "Precondition violation: if an IV is passed, it must be exactly 24 bytes, not %d", ivsize);
return -1;
}

try {
reinterpret_cast<ChaCha20*>(self)->e = new CryptoPP::ChaCha20::Encryption(reinterpret_cast<const byte*>(key), keysize, reinterpret_cast<const byte*>(iv));
}
catch (CryptoPP::InvalidKeyLength le)
{
PyErr_Format(chacha20_error, "Precondition violation: you are required to pass a valid key size. Crypto++ gave this exception: %s", le.what());
return -1;
}
if (!reinterpret_cast<ChaCha20*>(self)->e)
{
PyErr_NoMemory();
return -1;
}
return 0;
}


static PyTypeObject ChaCha20_type = {
PyObject_HEAD_INIT(NULL)
0, /*ob_size*/
"_chacha20.ChaCha20", /*tp_name*/
sizeof(ChaCha20), /*tp_basicsize*/
0, /*tp_itemsize*/
ChaCha20_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number*/
0, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
ChaCha20__doc__, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
ChaCha20_methods, /*tp_methods*/
0, /*tp_members*/
0, /*tp_getset*/
0, /*tp_base*/
0, /*tp_dict*/
0, /*tp_descr_get*/
0, /*tp_descr_set*/
0, /*tp_dictoffset*/
ChaCha20_init, /*tp_init*/
0, /*tp_alloc*/
ChaCha20_new, /*tp_new*/
};

void init_chacha20(PyObject*const module)
{
if (PyType_Ready(&ChaCha20_type) < 0)
return;
Py_INCREF(&ChaCha20_type);
PyModule_AddObject(module, "chacha20_ChaCha20", (PyObject *)&ChaCha20_type);

chacha20_error = PyErr_NewException(const_cast<char*>("_chacha20.Error"), NULL, NULL);
PyModule_AddObject(module, "chacha20_Error", chacha20_error);

PyModule_AddStringConstant(module, "chacha20__doc__", const_cast<char*>(chacha20__doc__));
}
6 changes: 6 additions & 0 deletions src/pycryptopp/cipher/chacha20module.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef __INCL_CHACHA20MODULE_HPP
#define __INCL_CHACHA20MODULE_HPP

extern void init_chacha20(PyObject* module);

#endif; /*#ifndef __INCL_CHACHA20MODULE_HPP*/
132 changes: 132 additions & 0 deletions src/pycryptopp/test/test_chacha20.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import random, re
import unittest

from binascii import a2b_hex, b2a_hex
from pkg_resources import resource_string

from pycryptopp.cipher import chacha20
TEST_XSALSA_RE=re.compile("\nCOUNT=([0-9]+)\nKEY=([0-9a-f]+)\nIV=([0-9a-f]+)\nPLAINTEXT=([0-9a-f]+)\nCIPHERTEXT=([0-9a-f]+)")

class ChaCha20Test(unittest.TestCase):

enc0="20b800ca9a9247bcc09a6816b123be47ac0681e23749d35fea22c04e1b1b9bb19782ab297a65c80ab1bac2ff2c4beba4c807bf832fdb57e648b3b677d87f8758123fb0f9d39efa95d7dd3f72fb0abe9b96e68514e758e9de31ae1d694c462de537f7e45188f2dcb912c6a0ed1aee5f6b4d2be8c3a1126e928b6fea91d643f9bbe808d984c2517bcb928161"

def test_zero_ChaCha20(self):
key="1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389"
iv="69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37"
computedcipher=chacha20.ChaCha20(a2b_hex(key),a2b_hex(iv)).process('\x00'*139)
self.failUnlessEqual(a2b_hex(self.enc0), computedcipher, "enc0: %s, computedciper: %s" % (self.enc0, b2a_hex(computedcipher)))

cryptor=chacha20.ChaCha20(a2b_hex(key),a2b_hex(iv))

computedcipher1=cryptor.process('\x00'*69)
computedcipher2=cryptor.process('\x00'*69)
computedcipher3=cryptor.process('\x00')
computedcipher12=b2a_hex(computedcipher1)+b2a_hex(computedcipher2)+b2a_hex(computedcipher3)
self.failUnlessEqual(self.enc0, computedcipher12)


def test_ChaCha20(self):
# The test vector is from Crypto++'s TestVectors/chacha.txt, comment
# there is: Source: created by Wei Dai using naclcrypto-20090308 .
# naclcrypto being DJB's crypto library and of course DJB designed
# ChaCha20
s = resource_string("pycryptopp", "testvectors/chacha.txt")
return self._test_ChaCha20(s)

def _test_ChaCha20(self, vects_str):
for mo in TEST_XSALSA_RE.finditer(vects_str):
#count = int(mo.group(1))
key = a2b_hex(mo.group(2))
iv = a2b_hex(mo.group(3))
#plaintext = a2b_hex(mo.group(4))
#ciphertext= a2b_hex(mo.group(5))
plaintext = mo.group(4)
ciphertext = mo.group(5)
computedcipher=chacha20.ChaCha20(key,iv).process(a2b_hex(plaintext))
#print "ciphertext", b2a_hex(computedcipher), '\n'
#print "computedtext", ciphertext, '\n'
#print count, ": \n"
self.failUnlessEqual(computedcipher,a2b_hex(ciphertext),"computedcipher: %s, ciphertext: %s" % (b2a_hex(computedcipher), ciphertext))

#the random decomposing
plaintext1 = ""
plaintext2 = ""
length = len(plaintext)
rccipher = ""
cryptor = chacha20.ChaCha20(key,iv)
if length > 2:
point = random.randint(0,length-3)
if (point%2) !=0:
point -= 1
plaintext1 += plaintext[:point+2]
plaintext2 += plaintext[point+2:]
rccipher += b2a_hex(cryptor.process(a2b_hex(plaintext1)))
rccipher += b2a_hex(cryptor.process(a2b_hex(plaintext2)))
self.failUnlessEqual(rccipher, ciphertext, "random computed cipher: %s, ciphertext: %s" % (rccipher, ciphertext))

#every byte encrypted
cryptor = chacha20.ChaCha20(key,iv)
eccipher=""
l = 0
while l<=(length-2):
eccipher += b2a_hex(cryptor.process(a2b_hex(plaintext[l:l+2])))
l += 2
self.failUnlessEqual(eccipher, ciphertext, "every byte computed cipher: %s, ciphertext: %s" % (eccipher, ciphertext))


def test_types_and_lengths(self):

# the key= argument must be a bytestring exactly 32 bytes long
self.failUnlessRaises(TypeError, chacha20.ChaCha20, None)
for i in range(70):
key = "a"*i
if i != 32 and i != 16:
self.failUnlessRaises(chacha20.Error, chacha20.ChaCha20, key)
else:
self.failUnless(chacha20.ChaCha20(key))

# likewise, iv= (if provided) must be exactly 24 bytes long. Passing
# None is not treated the same as not passing the argument at all.
key = "a"*32
self.failUnlessRaises(TypeError, chacha20.ChaCha20, key, None)
for i in range(70):
iv = "i"*i
if i != 24:
self.failUnlessRaises(chacha20.Error, chacha20.ChaCha20, key, iv)
else:
self.failUnless(chacha20.ChaCha20(key, iv))

def test_recursive(self):
# Try to use the same technique as:
# http://blogs.msdn.com/si_team/archive/2006/05/19/aes-test-vectors.aspx
# It's not exactly the same, though, because ChaCha20 is a stream
# cipher, whereas the Ferguson code is exercising a block cipher. But
# we try to do something similar.

# the ChaCha20 internal function uses a 32-byte block. We want to
# exercise it twice for each key, to guard against
# clobbering-after-key-setup errors. Just doing enc(enc(p)) could let
# XOR errors slip through. So to be safe, use B=64.
B=64
N=24
K=32
s = "\x00"*(B+N+K)
def enc(key, nonce, plaintext):
p = chacha20.ChaCha20(key=key, iv=nonce)
return p.process(plaintext)
for i in range(1000):
plaintext = s[-K-N-B:-K-N]
nonce = s[-K-N:-K]
key = s[-K:]
ciphertext = enc(key, nonce, plaintext)
s += ciphertext
s = s[-K-N-B:]
output = b2a_hex(s[-B:])
# I compared test output against pysodium - David
self.failUnlessEqual(output, "2a84ffcd163b683daafd51b2d10af7338ac5fb25716d5f7b6e6af9cdbe6abd6e63c00d4e8eff8c306b17d71691b6ce1e6e0577c0e8e719abb3dfa7f7b21d955e")



if __name__ == "__main__":
unittest.main()
Loading