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