diff --git a/nkms/crypto/__init__.py b/nkms/crypto/__init__.py index 397da5dd7..332642fd3 100644 --- a/nkms/crypto/__init__.py +++ b/nkms/crypto/__init__.py @@ -1,20 +1,13 @@ import importlib from nacl.utils import random # noqa +from npre.curves import secp256k1 -# 'Random' parameter g here is derived from Bitcoin's hashMerkleRoot of -# its genesis block: -# bbs98.ec.serialize( -# bbs98.ec.hashEC( -# pre.ecgroup, -# base64.decodebytes(b'4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b') -# bbs98.ec.G)) = b'1:A78WgHh03I38RcZO/FQe9SbmPVzQg+oehzR8QsGXOeqz' default_algorithm = dict( symmetric=dict( cipher='nacl'), pre=dict( cipher='bbs98', # BBS98 is only temporary here, for development - curve=714, # secp256k1 in OpenSSL - g=b'1:A78WgHh03I38RcZO/FQe9SbmPVzQg+oehzR8QsGXOeqz', + curve=secp256k1, m=None, n=None)) diff --git a/nkms/crypto/keypairs.py b/nkms/crypto/keypairs.py new file mode 100644 index 000000000..b06ace4dd --- /dev/null +++ b/nkms/crypto/keypairs.py @@ -0,0 +1,98 @@ +import msgpack +from random import SystemRandom +from py_ecc.secp256k1 import N, privtopub, ecdsa_raw_sign, ecdsa_raw_recover +from nkms.crypto import default_algorithm, pre_from_algorithm + + +class EncryptingKeypair(object): + def __init__(self, privkey_bytes=None): + self.pre = pre_from_algorithm(default_algorithm) + + if not privkey_bytes: + self.priv_key = self.pre.gen_priv(dtype='bytes') + else: + self.priv_key = privkey_bytes + self.pub_key = self.pre.priv2pub(self.priv_key) + + def encrypt(self, data, pubkey=None): + """ + Encrypts the data provided. + + :param bytes data: The data to encrypt + :param bytes pubkey: Pubkey to encrypt for + + :rtype: bytes + :return: Encrypted ciphertext + """ + if not pubkey: + pubkey = self.pub_key + return self.pre.encrypt(pubkey, data) + + def decrypt(self, enc_data): + """ + Decrypts the data provided + + :param bytes enc_data: Decrypts the data provided + + :rtype: bytes + :return: Decrypted plaintext + """ + return self.pre.decrypt(self.priv_key, enc_data) + + +class SigningKeypair(object): + def __init__(self, privkey_bytes=None): + self.secure_rand = SystemRandom() + if privkey_bytes: + self.priv_key = privkey_bytes + else: + # Key generation is random([1, N - 1]) + priv_number = self.secure_rand.randrange(1, N) + self.priv_key = priv_number.to_bytes(32, byteorder='big') + # Get the public component + self.pub_key = privtopub(self.priv_key) + + def _vrs_msgpack_dump(self, v, r, s): + v_bytes = v.to_bytes(1, byteorder='big') + r_bytes = r.to_bytes(32, byteorder='big') + s_bytes = s.to_bytes(32, byteorder='big') + return msgpack.dumps((v_bytes, r_bytes, s_bytes)) + + def _vrs_msgpack_load(self, msgpack_vrs): + sig = msgpack.loads(msgpack_vrs) + v = int.from_bytes(sig[0], byteorder='big') + r = int.from_bytes(sig[1], byteorder='big') + s = int.from_bytes(sig[2], byteorder='big') + return (v, r, s) + + def sign(self, msghash): + """ + Signs a hashed message and returns a msgpack'ed v, r, and s. + + :param bytes msghash: Hash of the message + + :rtype: Bytestring + :return: Msgpacked bytestring of v, r, and s (the signature) + """ + v, r, s = ecdsa_raw_sign(msghash, self.priv_key) + return self._vrs_msgpack_dump(v, r, s) + + def verify(self, msghash, signature, pubkey=None): + """ + Takes a msgpacked signature and verifies the message. + + :param bytes msghash: The hashed message to verify + :param bytes signature: The msgpacked signature (v, r, and s) + :param bytes pubkey: Pubkey to validate signature for + Default is the keypair's pub_key. + + :rtype: Boolean + :return: Is the signature valid or not? + """ + if not pubkey: + pubkey = self.pub_key + sig = self._vrs_msgpack_load(signature) + # Generate the public key from the signature and validate + # TODO: Look into fixed processing time functions for comparison + verify_sig = ecdsa_raw_recover(msghash, sig) + return verify_sig == pubkey diff --git a/nkms/crypto/keyring.py b/nkms/crypto/keyring.py new file mode 100644 index 000000000..d9a5366d4 --- /dev/null +++ b/nkms/crypto/keyring.py @@ -0,0 +1,70 @@ +import sha3 +from nkms.crypto.keypairs import SigningKeypair, EncryptingKeypair + + +class KeyRing(object): + def __init__(self, sig_privkey=None, enc_privkey=None): + """ + Initializes a KeyRing object. Uses the private keys to initialize + their respective objects, if provided. If not, it will generate new + keypairs. + + :param bytes sig_privkey: Private key in bytes of ECDSA signing keypair + :param bytes enc_privkey: Private key in bytes of encrypting keypair + """ + self.sig_keypair = SigningKeypair(sig_privkey) + self.enc_keypair = EncryptingKeypair(enc_privkey) + + def sign(self, message): + """ + Signs a message and returns a signature with the keccak hash. + + :param bytes message: Message to sign in bytes + + :rtype: bytestring + :return: Signature of message + """ + msg_digest = sha3.keccak_256(message).digest() + return self.sig_keypair.sign(msg_digest) + + def verify(self, message, signature, pubkey=None): + """ + Verifies a signature. + + :param bytes message: Message to check signature for + :param bytes signature: Signature to validate + :param bytes pubkey: Pubkey to validate signature with + Default is the sig_keypair's pub_key + + :rtype: Boolean + :return: Is the message signature valid or not? + """ + if not pubkey: + pubkey = self.sig_keypair.pub_key + msg_digest = sha3.keccak_256(message).digest() + return self.sig_keypair.verify(msg_digest, signature, pubkey=pubkey) + + def encrypt(self, plaintext, pubkey=None): + """ + Encrypts the plaintext provided. + + :param bytes plaintext: Plaintext to encrypt w/ EncryptingKeypair + :param bytes pubkey: Pubkey to encrypt for + + :rtype: bytes + :return: Ciphertext of plaintext + """ + if not pubkey: + pubkey = self.enc_keypair.pub_key + return self.enc_keypair.encrypt(plaintext, pubkey=pubkey) + + def decrypt(self, ciphertext): + """ + Decrypts the ciphertext provided. + + :param bytes ciphertext: Ciphertext to decrypt w/ EncryptingKeypair + + :rtype: bytes + :return: Plaintext of Encrypted ciphertext + """ + return self.enc_keypair.decrypt(ciphertext) diff --git a/nkms/crypto/pre/bbs98.py b/nkms/crypto/pre/bbs98.py index 4641fd71f..a172a21b5 100644 --- a/nkms/crypto/pre/bbs98.py +++ b/nkms/crypto/pre/bbs98.py @@ -1,11 +1,10 @@ -import base64 import msgpack from nkms import crypto from npre.bbs98 import PRE as BasePRE def convert_priv(sk): - return b'0:' + base64.encodebytes(sk).strip() + return b'\x00' + sk class PRE(BasePRE): diff --git a/setup.py b/setup.py index 71a9cf6cd..00aa4cde4 100644 --- a/setup.py +++ b/setup.py @@ -21,9 +21,9 @@ TESTS_REQUIRE = [ # should add --process-dependency-links to pip LINKS = [ - 'https://github.com/bmuller/kademlia/archive/kms-dependency.tar.gz#egg=kademlia-1.0', + 'https://github.com/nucypher/kademlia/archive/kms-dependency.tar.gz#egg=kademlia-1.0', 'https://github.com/bmuller/rpcudp/archive/python3.5.tar.gz#egg=rpcudp-3.0.0', - 'https://github.com/nucypher/nucypher-pre-python/archive/master.tar.gz#egg=npre-0.1'] + 'https://github.com/nucypher/nucypher-pre-python/archive/0.3.tar.gz#egg=npre-0.3'] setup(name='nkms', version='0.1', diff --git a/tests/crypto/test_keypairs.py b/tests/crypto/test_keypairs.py new file mode 100644 index 000000000..df51c0ff3 --- /dev/null +++ b/tests/crypto/test_keypairs.py @@ -0,0 +1,53 @@ +import unittest +import sha3 +import msgpack +from nkms.crypto.keypairs import SigningKeypair, EncryptingKeypair + + +class TestSigningKeypair(unittest.TestCase): + def setUp(self): + self.keypair_a = SigningKeypair() + self.keypair_b = SigningKeypair() + self.msg = b'this is a test' + + def test_signing(self): + msg_digest = sha3.keccak_256(self.msg).digest() + signature = self.keypair_a.sign(msg_digest) + + sig = msgpack.loads(signature) + self.assertTrue(1, len(sig[0])) # Check v + self.assertTrue(32, len(sig[1])) # Check r + self.assertTrue(32, len(sig[2])) # Check s + + def test_verification(self): + msg_digest = sha3.keccak_256(self.msg).digest() + signature = self.keypair_a.sign(msg_digest) + + sig = msgpack.loads(signature) + self.assertTrue(1, len(sig[0])) # Check v + self.assertTrue(32, len(sig[1])) # Check r + self.assertTrue(32, len(sig[2])) # Check s + + verify_sig = self.keypair_b.verify(msg_digest, signature, + pubkey=self.keypair_a.pub_key) + self.assertTrue(verify_sig) + + +class TestEncryptingKeypair(unittest.TestCase): + def setUp(self): + self.send_keypair = EncryptingKeypair() + self.recv_keypair = EncryptingKeypair() + self.msg = b'this is a test' + + def test_encryption(self): + ciphertext = self.send_keypair.encrypt(self.msg, + pubkey=self.recv_keypair.pub_key) + self.assertNotEqual(self.msg, ciphertext) + + def test_decryption(self): + ciphertext = self.send_keypair.encrypt(self.msg, + pubkey=self.recv_keypair.pub_key) + self.assertNotEqual(self.msg, ciphertext) + + plaintext = self.recv_keypair.decrypt(ciphertext) + self.assertEqual(self.msg, plaintext) diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py new file mode 100644 index 000000000..0e254dbd1 --- /dev/null +++ b/tests/crypto/test_keyring.py @@ -0,0 +1,44 @@ +import unittest +import msgpack +from nkms.crypto.keyring import KeyRing + + +class TestKeyRing(unittest.TestCase): + def setUp(self): + self.keyring_a = KeyRing() + self.keyring_b = KeyRing() + + self.msg = b'this is a test' + + def test_signing(self): + signature = self.keyring_a.sign(self.msg) + + sig = msgpack.loads(signature) + self.assertTrue(1, len(sig[0])) # Check v + self.assertTrue(32, len(sig[1])) # Check r + self.assertTrue(32, len(sig[2])) # Check s + + def test_verification(self): + signature = self.keyring_a.sign(self.msg) + + sig = msgpack.loads(signature) + self.assertTrue(1, len(sig[0])) # Check v + self.assertTrue(32, len(sig[1])) # Check r + self.assertTrue(32, len(sig[2])) # Check s + + is_valid = self.keyring_b.verify(self.msg, signature, + pubkey=self.keyring_a.sig_keypair.pub_key) + self.assertTrue(is_valid) + + def test_encryption(self): + ciphertext = self.keyring_a.encrypt(self.msg, + pubkey=self.keyring_b.enc_keypair.pub_key) + self.assertNotEqual(self.msg, ciphertext) + + def test_decryption(self): + ciphertext = self.keyring_a.encrypt(self.msg, + pubkey=self.keyring_b.enc_keypair.pub_key) + self.assertNotEqual(self.msg, ciphertext) + + plaintext = self.keyring_b.decrypt(ciphertext) + self.assertEqual(self.msg, plaintext) diff --git a/tests/test_default_crypto.py b/tests/test_default_crypto.py index da371d474..451c0d2c8 100644 --- a/tests/test_default_crypto.py +++ b/tests/test_default_crypto.py @@ -24,6 +24,11 @@ def test_pre(): pk_alice = pre.priv2pub(sk_alice) pk_bob = pre.priv2pub(sk_bob) + assert len(pk_alice) == 34 + assert len(pk_bob) == 34 + assert pk_alice[0] == 1 + assert pk_bob[0] == 1 + cleartext = b'Hello world' cyphertext_for_alice = pre.encrypt(pk_alice, cleartext) diff --git a/tox.ini b/tox.ini index 591694f00..7f03ae065 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py35,py36 deps = .[testing] install_command = pip install --process-dependency-links {opts} {packages} commands = py.test {posargs} +usedevelop = True [pytest] testpaths = tests