From c5ebb75b57fe248fea475f0bf1a9d31b9d49611b Mon Sep 17 00:00:00 2001
From: David Fischer <djfische@gmail.com>
Date: Fri, 15 Mar 2013 23:45:13 -0700
Subject: [PATCH] Added public key encryption

The tests are adapted from the libsodium tests.
The docs are adapted from the rbnacl docs.
---
 docs/index.rst    |   1 +
 docs/public.rst   |  91 +++++++++++++++++++++
 nacl/nacl.py      |  20 +++++
 nacl/public.py    | 202 ++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_box.py |  72 +++++++++++++++++
 5 files changed, 386 insertions(+)
 create mode 100644 docs/public.rst
 create mode 100644 nacl/public.py
 create mode 100644 tests/test_box.py

diff --git a/docs/index.rst b/docs/index.rst
index 4730603d..39488ea6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -8,6 +8,7 @@ Contents:
 
    secret
    signing
+   public
 
 
 Support Features
diff --git a/docs/public.rst b/docs/public.rst
new file mode 100644
index 00000000..47af3995
--- /dev/null
+++ b/docs/public.rst
@@ -0,0 +1,91 @@
+Public Key Encryption
+=====================
+
+Imagine Alice wants something valuable shipped to her. Because it's valuable,
+she wants to make sure it arrives securely (i.e. hasn't been opened or
+tampered with) and that it's not a forgery (i.e. it's actually from the sender
+she's expecting it to be from and nobody's pulling the old switcheroo)
+
+One way she can do this is by providing the sender (let's call him Bob) with a
+high-security box of her choosing. She provides Bob with this box, and
+something else: a padlock, but a padlock without a key. Alice is keeping that
+key all to herself. Bob can put items in the box then put the padlock onto it,
+but once the padlock snaps shut, the box cannot be opened by anyone who
+doesn't have Alice's private key.
+
+Here's the twist though, Bob also puts a padlock onto the box. This padlock
+uses a key Bob has published to the world, such that if you have one of Bob's
+keys, you know a box came from him because Bob's keys will open Bob's padlocks
+(let's imagine a world where padlocks cannot be forged even if you know the
+key). Bob then sends the box to Alice.
+
+In order for Alice to open the box, she needs two keys: her private key that
+opens her own padlock, and Bob's well-known key. If Bob's key doesn't open the
+second padlock then Alice knows that this is not the box she was expecting
+from Bob, it's a forgery.
+
+This bidirectional guarantee around identity is known as mutual authentication.
+
+
+Example
+-------
+
+The :class:`~nacl.public.Box` class uses the given public and private (secret)
+keys to derive a shared key, which is used with the nonce given to encrypt the
+given messages and decrypt the given ciphertexts.  The same shared key will
+generated from both pairing of keys, so given two keypairs belonging to alice
+(pkalice, skalice) and bob(pkbob, skbob), the key derived from (pkalice, skbob)
+with equal that from (pkbob, skalice).  This is how the system works:
+
+.. code:: python
+
+    import nacl
+    from nacl.public import PrivateKey, Box
+
+    # generate the private key which must be kept secret
+    skbob = PrivateKey.generate()
+
+    # the public key can be given to anyone wishing to send
+    # Bob an encrypted message
+    pkbob = skbob.public_key
+
+    # Alice does the same and then
+    #     sends her public key to Bob and Bob his public key to Alice
+    skalice = PrivateKey.generate()
+    pkalice = skalice.public_key
+
+    # Bob wishes to send Alice an encrypted message
+    # So Bob must make a Box with his private key and Alice's public key
+    bob_box = Box(pkalice, skbob)
+
+    # This is our message to send, it must be a bytestring as Box will
+    #   treat is as just a binary blob of data.
+    message = b"Kill all humans"
+
+    # This is a nonce, it *MUST* only be used once, but it is not considered
+    #   secret and can be transmitted or stored alongside the ciphertext. A
+    #   good source of nonce is just 24 random bytes.
+    nonce = nacl.random(Box.NONCE_SIZE)
+
+    # Encrypt our message, it will be exactly 16 bytes longer than the original
+    #   message as it stores authentication information alongside it.
+    ciphertext = bob_box.encrypt(message, nonce)
+
+    # Alice creates a second box with her private key to decrypt the message
+    alice_box = Box(pkbob, skalice)
+
+    # Decrypt our message, an exception will be raised if the encryption was
+    #   tampered with or there was otherwise an error.
+    plaintext = alice_box.decrypt(ciphertext, nonce)
+
+
+
+Reference
+---------
+
+.. autoclass:: nacl.public.PublicKey
+    :members:
+.. autoclass:: nacl.public.PrivateKey
+    :members:
+.. autoclass:: nacl.public.Box
+    :members:
diff --git a/nacl/nacl.py b/nacl/nacl.py
index dd3e1f15..aa78d852 100644
--- a/nacl/nacl.py
+++ b/nacl/nacl.py
@@ -36,6 +36,21 @@ ffi.cdef(
         int crypto_sign_open(unsigned char *m, unsigned long long *mlen, const unsigned char *sm, unsigned long long smlen, const unsigned char *pk);
     """
 
+    # Public Key Encryption
+    """
+        static const int crypto_box_PUBLICKEYBYTES;
+        static const int crypto_box_SECRETKEYBYTES;
+        static const int crypto_box_BEFORENMBYTES;
+        static const int crypto_box_NONCEBYTES;
+        static const int crypto_box_ZEROBYTES;
+        static const int crypto_box_BOXZEROBYTES;
+
+        int crypto_box_keypair(unsigned char *pk, unsigned char *sk);
+        int crypto_box_afternm(unsigned char *c, const unsigned char *m, unsigned long long mlen, const unsigned char *n, const unsigned char *k);
+        int crypto_box_open_afternm(unsigned char *m, const unsigned char *c, unsigned long long clen, const unsigned char *n, const unsigned char *k);
+        int crypto_box_beforenm(unsigned char *k, const unsigned char *pk, const unsigned char *sk);
+    """
+
     # Hashing
     """
         static const int crypto_hash_BYTES;
@@ -75,6 +90,11 @@ lib.crypto_sign_seed_keypair = wrap_nacl_function(lib.crypto_sign_seed_keypair)
 lib.crypto_sign = wrap_nacl_function(lib.crypto_sign)
 lib.crypto_sign_open = wrap_nacl_function(lib.crypto_sign_open)
 
+lib.crypto_box_keypair = wrap_nacl_function(lib.crypto_box_keypair)
+lib.crypto_box_afternm = wrap_nacl_function(lib.crypto_box_afternm)
+lib.crypto_box_open_afternm = wrap_nacl_function(lib.crypto_box_open_afternm)
+lib.crypto_box_beforenm = wrap_nacl_function(lib.crypto_box_beforenm)
+
 lib.crypto_hash = wrap_nacl_function(lib.crypto_hash)
 lib.crypto_hash_sha256 = wrap_nacl_function(lib.crypto_hash_sha256)
 lib.crypto_hash_sha512 = wrap_nacl_function(lib.crypto_hash_sha512)
diff --git a/nacl/public.py b/nacl/public.py
new file mode 100644
index 00000000..43449e36
--- /dev/null
+++ b/nacl/public.py
@@ -0,0 +1,202 @@
+from __future__ import absolute_import
+from __future__ import division
+
+from . import six
+
+from . import nacl, encoding
+from .exceptions import CryptoError
+
+
+class PublicKey(encoding.Encodable, six.StringFixer, object):
+    """
+    The public key counterpart to an Ed25519 :class:`nacl.public.PrivateKey`
+    for encrypting messages.
+
+    :param public_key: [:class:`bytes`] Encoded Ed25519 public key
+    :param encoder: A class that is able to decode the `public_key`
+    """
+
+    PUBLICKEY_SIZE = nacl.lib.crypto_box_PUBLICKEYBYTES
+
+    def __init__(self, public_key, encoder=encoding.RawEncoder):
+        self._public_key = encoder.decode(public_key)
+
+        if len(self._public_key) != self.PUBLICKEY_SIZE:
+            raise ValueError('The public key must be exactly %s bytes long' %
+                             self.PUBLICKEY_SIZE)
+
+    def __bytes__(self):
+        return self._public_key
+
+
+class PrivateKey(encoding.Encodable, six.StringFixer, object):
+    """
+    Private key for decrypting messages using the Ed25519 algorithm.
+
+    .. warning:: This **must** be protected and remain secret. Anyone who
+        knows the value of your :class:`~nacl.public.PrivateKey` can decrypt
+        any message encrypted by the corresponding
+        :class:`~nacl.public.PublicKey`
+
+    :param private_key: The private key used to decrypt messages
+    :param encoder: The encoder class used to decode the given keys
+
+    :cvar PRIVATEKEY_SIZE: The size that the private key is required to be
+    """
+
+    PRIVATEKEY_SIZE = nacl.lib.crypto_box_SECRETKEYBYTES
+
+    def __init__(self, private_key, encoder=encoding.RawEncoder):
+        self._private_key = encoder.decode(private_key)
+        self._public_key = None
+
+        if len(self._private_key) != self.PRIVATEKEY_SIZE:
+            raise ValueError('The private key must be exactly %s bytes long' %
+                             self.PRIVATEKEY_SIZE)
+
+    def __bytes__(self):
+        return self._private_key
+
+    @classmethod
+    def generate(cls):
+        """
+        Generates a random :class:`~nacl.public.PrivateKey` object
+
+        :rtype: :class:`~nacl.public.PrivateKey`
+        """
+        pk = nacl.ffi.new("unsigned char[]", PublicKey.PUBLICKEY_SIZE)
+        sk = nacl.ffi.new("unsigned char[]", cls.PRIVATEKEY_SIZE)
+
+        public_key = nacl.ffi.buffer(pk, PublicKey.PUBLICKEY_SIZE)[:]
+        private_key = nacl.ffi.buffer(sk, cls.PRIVATEKEY_SIZE)[:]
+
+        if not nacl.lib.crypto_box_keypair(public_key, private_key):
+            raise CryptoError("Failed to generate key pair")
+
+        sk = cls(private_key)
+        sk.public_key = public_key
+
+        return sk
+
+    @property
+    def public_key(self):
+        return self._public_key
+
+    @public_key.setter
+    def public_key(self, value):
+        self._public_key = value
+
+
+class Box(encoding.Encodable, six.StringFixer, object):
+    """
+    The Box class boxes and unboxes messages between a pair of keys
+
+    The ciphertexts generated by :class:`~nacl.public.Box` include a 16
+    byte authenticator which is checked as part of the decryption. An invalid
+    authenticator will cause the decrypt function to raise an exception. The
+    authenticator is not a signature. Once you've decrypted the message you've
+    demonstrated the ability to create arbitrary valid message, so messages you
+    send are repudiable. For non-repudiable messages, sign them after
+    encryption.
+
+    :param public_key: :class:`~nacl.public.PublicKey` used to encrypt and
+        decrypt messages
+    :param private_key: :class:`~nacl.public.PrivateKey` used to encrypt and
+        decrypt messages
+
+    :cvar NONCE_SIZE: The size that the nonce is required to be.
+    """
+
+    NONCE_SIZE = nacl.lib.crypto_box_NONCEBYTES
+
+    def __init__(self, public_key, private_key):
+        self._pk = str(public_key)
+        self._sk = str(private_key)
+        self._shared_key = None
+
+    def __bytes__(self):
+        return self._sk
+
+    def encrypt(self, plaintext, nonce, encoder=encoding.RawEncoder):
+        """
+        Encrypts the plaintext message using the given `nonce` and returns
+        the ciphertext encoded with the encoder.
+
+        .. warning:: It is **VITALLY** important that the nonce is a nonce,
+            i.e. it is a number used only once for any given key. If you fail
+            to do this, you compromise the privacy of the messages encrypted.
+
+        :param plaintext: [:class:`bytes`] The plaintext message to encrypt
+        :param nonce: [:class:`bytes`] The nonce to use in the encryption
+        :param encoder: The encoder to use to encode the ciphertext
+        :rtype: [:class:`bytes`]
+        """
+        if len(nonce) != self.NONCE_SIZE:
+            raise ValueError('The nonce must be exactly %s bytes long' %
+                             self.NONCE_SIZE)
+
+        padded = b"\x00" * nacl.lib.crypto_box_ZEROBYTES + plaintext
+        ciphertext = nacl.ffi.new("unsigned char[]", len(padded))
+
+        if not nacl.lib.crypto_box_afternm(
+                    ciphertext,
+                    padded,
+                    len(padded),
+                    nonce,
+                    self._beforenm()
+                ):
+            raise CryptoError("Encryption failed")
+
+        box_zeros = nacl.lib.crypto_box_BOXZEROBYTES
+        ciphertext = nacl.ffi.buffer(ciphertext, len(padded))[box_zeros:]
+
+        return encoder.encode(ciphertext)
+
+    def decrypt(self, ciphertext, nonce, encoder=encoding.RawEncoder):
+        """
+        Decrypts the ciphertext using the given nonce and returns the
+        plaintext message.
+
+        :param ciphertext: [:class:`bytes`] The encrypted message to decrypt
+        :param nonce: [:class:`bytes`] The nonce used when encrypting the
+            ciphertext
+        :param encoder: The encoder used to decode the ciphertext.
+        :rtype: [:class:`bytes`]
+        """
+        if len(nonce) != self.NONCE_SIZE:
+            raise ValueError('The nonce must be exactly %s bytes long' %
+                             self.NONCE_SIZE)
+
+        ciphertext = encoder.decode(ciphertext)
+
+        padded = b"\x00" * nacl.lib.crypto_box_BOXZEROBYTES + ciphertext
+        plaintext = nacl.ffi.new("unsigned char[]", len(padded))
+
+        if not nacl.lib.crypto_box_open_afternm(
+                    plaintext,
+                    padded,
+                    len(padded),
+                    nonce,
+                    self._beforenm()
+                ):
+            raise CryptoError(
+                        'Decryption failed. Ciphertext failed verification')
+
+        box_zeros = nacl.lib.crypto_box_ZEROBYTES
+        plaintext = nacl.ffi.buffer(plaintext, len(padded))[box_zeros:]
+
+        return plaintext
+
+    def _beforenm(self):
+        if self._shared_key is not None:
+            return self._shared_key
+
+        sharedkey_size = nacl.lib.crypto_box_BEFORENMBYTES
+
+        k = nacl.ffi.new("unsigned char[]", sharedkey_size)
+
+        if not nacl.lib.crypto_box_beforenm(k, self._pk, self._sk):
+            raise CryptoError("Failed to derive shared key")
+
+        self._shared_key = nacl.ffi.buffer(k, sharedkey_size)[:]
+        return self._shared_key
diff --git a/tests/test_box.py b/tests/test_box.py
new file mode 100644
index 00000000..25e2ef39
--- /dev/null
+++ b/tests/test_box.py
@@ -0,0 +1,72 @@
+import binascii
+import pytest
+
+from nacl.encoding import HexEncoder
+from nacl.public import PrivateKey, PublicKey, Box
+from nacl.exceptions import CryptoError
+
+
+VECTORS = [
+    # skalice, pkalice, skbob, pkbob, nonce, plaintext, ciphertext
+    (
+        b"77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a",
+        b"8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a",
+        b"5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb",
+        b"de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f",
+        b"69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37",
+        b"be075fc53c81f2d5cf141316ebeb0c7b5228c52a4c62cbd44b66849b64244ffce5ecbaaf33bd751a1ac728d45e6c61296cdc3c01233561f41db66cce314adb310e3be8250c46f06dceea3a7fa1348057e2f6556ad6b1318a024a838f21af1fde048977eb48f59ffd4924ca1c60902e52f0a089bc76897040e082f937763848645e0705",
+        b"f3ffc7703f9400e52a7dfb4b3d3305d98e993b9f48681273c29650ba32fc76ce48332ea7164d96a4476fb8c531a1186ac0dfc17c98dce87b4da7f011ec48c97271d2c20f9b928fe2270d6fb863d51738b48eeee314a7cc8ab932164548e526ae90224368517acfeabd6bb3732bc0e9da99832b61ca01b6de56244a9e88d5f9b37973f622a43d14a6599b1f654cb45a74e355a5",
+    ),
+]
+
+
+def test_box_creation():
+    pk = PublicKey(
+        b"ec2bee2d5be613ca82e377c96a0bf2220d823ce980cdff6279473edc52862798",
+        encoder=HexEncoder,
+    )
+    sk = PrivateKey(
+        b"5c2bee2d5be613ca82e377c96a0bf2220d823ce980cdff6279473edc52862798",
+        encoder=HexEncoder,
+    )
+    Box(pk, sk)
+
+
+@pytest.mark.parametrize(("skalice", "pkalice", "skbob", "pkbob", "nonce", "plaintext", "ciphertext"), VECTORS)
+def test_box_encryption(skalice, pkalice, skbob, pkbob, nonce, plaintext, ciphertext):
+    pkalice = PublicKey(pkalice, encoder=HexEncoder)
+    skbob = PrivateKey(skbob, encoder=HexEncoder)
+
+    box = Box(pkalice, skbob)
+
+    plaintext = binascii.unhexlify(plaintext)
+    nonce = binascii.unhexlify(nonce)
+
+    assert box.encrypt(plaintext, nonce, encoder=HexEncoder) == ciphertext
+
+
+@pytest.mark.parametrize(("skalice", "pkalice", "skbob", "pkbob", "nonce", "plaintext", "ciphertext"), VECTORS)
+def test_box_decryption(skalice, pkalice, skbob, pkbob, nonce, plaintext, ciphertext):
+    pkbob = PublicKey(pkbob, encoder=HexEncoder)
+    skalice = PrivateKey(skalice, encoder=HexEncoder)
+
+    box = Box(pkbob, skalice)
+
+    nonce = binascii.unhexlify(nonce)
+    decrypted = binascii.hexlify(
+                    box.decrypt(ciphertext, nonce, encoder=HexEncoder))
+
+    assert decrypted == plaintext
+
+
+@pytest.mark.parametrize(("skalice", "pkalice", "skbob", "pkbob", "nonce", "plaintext", "ciphertext"), VECTORS)
+def test_box_failed_decryption(skalice, pkalice, skbob, pkbob, nonce, plaintext, ciphertext):
+    pkbob = PublicKey(pkbob, encoder=HexEncoder)
+    skbob = PrivateKey(skbob, encoder=HexEncoder)
+
+    # this cannot decrypt the ciphertext!
+    # the ciphertext must be decrypted by (pkbob, skalice) or (pkalice, skbob)
+    box = Box(pkbob, skbob)
+
+    with pytest.raises(CryptoError):
+        box.decrypt(ciphertext, binascii.unhexlify(nonce), encoder=HexEncoder)
-- 
GitLab