diff --git a/docs/public.rst b/docs/public.rst index 9ba2712a8a935b4b1784e0d57d1d9c812b2107c7..3090566bee258db6cdf714e9565be22512c3af43 100644 --- a/docs/public.rst +++ b/docs/public.rst @@ -67,16 +67,16 @@ with equal that from (pkbob, skalice). This is how the system works: # good source of nonce is just 24 random bytes. nonce = nacl.utils.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) + # Encrypt our message, it will be exactly 40 bytes longer than the original + # message as it stores authentication information and nonce alongside it. + encrypted = bob_box.encrypt(message, nonce) # Alice creates a second box with her private key to decrypt the message alice_box = Box(skalice, pkbob) # 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) + plaintext = alice_box.decrypt(encrypted) @@ -85,7 +85,12 @@ Reference .. autoclass:: nacl.public.PublicKey :members: + .. autoclass:: nacl.public.PrivateKey :members: + .. autoclass:: nacl.public.Box :members: + +.. autoclass:: nacl.utils.EncryptedMessage + :members: diff --git a/docs/secret.rst b/docs/secret.rst index 3a431bc22d56b8c5750346ad81011e8c2138afee..93c6e45298255e35b0d1a977396db5ac9420a275 100644 --- a/docs/secret.rst +++ b/docs/secret.rst @@ -33,13 +33,13 @@ Example # good source of nonce is just 24 random bytes. nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) - # Encrypt our message, it will be exactly 16 bytes longer than the original - # message as it stores authentication information alongside it. - ciphertext = box.encrypt(message, nonce) + # Encrypt our message, it will be exactly 40 bytes longer than the original + # message as it stores authentication information and nonce alongside it. + encrypted = box.encrypt(message, nonce) # Decrypt our message, an exception will be raised if the encryption was # tampered with or there was otherwise an error. - plaintext = box.decrypt(ciphertext, nonce) + plaintext = box.decrypt(encrypted) Requirements @@ -89,6 +89,10 @@ Reference .. autoclass:: nacl.secret.SecretBox :members: +.. autoclass:: nacl.utils.EncryptedMessage + :members: + :noindex: + Algorithm details ----------------- diff --git a/nacl/public.py b/nacl/public.py index ae0c792277d07cf2d41fbf533836aa28e3340b82..266dd5faf59d6a8e0b32e16a22242d583a261901 100644 --- a/nacl/public.py +++ b/nacl/public.py @@ -5,7 +5,7 @@ from . import six from . import nacl, encoding from .exceptions import CryptoError -from .utils import random +from .utils import EncryptedMessage, random class PublicKey(encoding.Encodable, six.StringFixer, object): @@ -144,7 +144,7 @@ class Box(encoding.Encodable, six.StringFixer, object): :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`] + :rtype: [:class:`nacl.utils.EncryptedMessage`] """ if len(nonce) != self.NONCE_SIZE: raise ValueError("The nonce must be exactly %s bytes long" % @@ -165,9 +165,16 @@ class Box(encoding.Encodable, six.StringFixer, object): box_zeros = nacl.lib.crypto_box_BOXZEROBYTES ciphertext = nacl.ffi.buffer(ciphertext, len(padded))[box_zeros:] - return encoder.encode(ciphertext) + encoded_nonce = encoder.encode(nonce) + encoded_ciphertext = encoder.encode(ciphertext) + + return EncryptedMessage._from_parts( + encoded_nonce, + encoded_ciphertext, + encoder.encode(nonce + ciphertext), + ) - def decrypt(self, ciphertext, nonce, encoder=encoding.RawEncoder): + def decrypt(self, ciphertext, nonce=None, encoder=encoding.RawEncoder): """ Decrypts the ciphertext using the given nonce and returns the plaintext message. @@ -178,12 +185,18 @@ class Box(encoding.Encodable, six.StringFixer, object): :param encoder: The encoder used to decode the ciphertext. :rtype: [:class:`bytes`] """ + # Decode our ciphertext + ciphertext = encoder.decode(ciphertext) + + if nonce is None: + # If we were given the nonce and ciphertext combined, split them. + nonce = ciphertext[:self.NONCE_SIZE] + ciphertext = ciphertext[self.NONCE_SIZE:] + 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)) diff --git a/nacl/secret.py b/nacl/secret.py index f2d066573bcc61817f501735a3489a41e8c9046f..d9e2353907e20f5f6c5042dcbc65266efb697442 100644 --- a/nacl/secret.py +++ b/nacl/secret.py @@ -5,6 +5,7 @@ from . import six from . import nacl, encoding from .exceptions import CryptoError +from .utils import EncryptedMessage class SecretBox(encoding.Encodable, six.StringFixer, object): @@ -56,7 +57,7 @@ class SecretBox(encoding.Encodable, six.StringFixer, object): :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`] + :rtype: [:class:`nacl.utils.EncryptedMessage`] """ if len(nonce) != self.NONCE_SIZE: raise ValueError("The nonce must be exactly %s bytes long" % @@ -73,9 +74,16 @@ class SecretBox(encoding.Encodable, six.StringFixer, object): box_zeros = nacl.lib.crypto_secretbox_BOXZEROBYTES ciphertext = nacl.ffi.buffer(ciphertext, len(padded))[box_zeros:] - return encoder.encode(ciphertext) + encoded_nonce = encoder.encode(nonce) + encoded_ciphertext = encoder.encode(ciphertext) + + return EncryptedMessage._from_parts( + encoded_nonce, + encoded_ciphertext, + encoder.encode(nonce + ciphertext), + ) - def decrypt(self, ciphertext, nonce, encoder=encoding.RawEncoder): + def decrypt(self, ciphertext, nonce=None, encoder=encoding.RawEncoder): """ Decrypts the ciphertext using the given nonce and returns the plaintext message. @@ -86,12 +94,18 @@ class SecretBox(encoding.Encodable, six.StringFixer, object): :param encoder: The encoder used to decode the ciphertext. :rtype: [:class:`bytes`] """ + # Decode our ciphertext + ciphertext = encoder.decode(ciphertext) + + if nonce is None: + # If we were given the nonce and ciphertext combined, split them. + nonce = ciphertext[:self.NONCE_SIZE] + ciphertext = ciphertext[self.NONCE_SIZE:] + if len(nonce) != self.NONCE_SIZE: raise ValueError("The nonce must be exactly %s bytes long" % nacl.lib.crypto_secretbox_NONCEBYTES) - ciphertext = encoder.decode(ciphertext) - padded = b"\x00" * nacl.lib.crypto_secretbox_BOXZEROBYTES + ciphertext plaintext = nacl.ffi.new("unsigned char[]", len(padded)) diff --git a/nacl/utils.py b/nacl/utils.py index 3902ee69b9c0f31f17e6c66df9dfb0366ed247bf..f70328f8bc72c0303565e78af5b597ed07ab4d7e 100644 --- a/nacl/utils.py +++ b/nacl/utils.py @@ -2,6 +2,35 @@ from __future__ import absolute_import from __future__ import division from . import nacl +from . import six + + +class EncryptedMessage(six.binary_type): + """ + A bytes subclass that holds a messaged that has been encrypted by a + :class:`SecretBox`. + """ + + @classmethod + def _from_parts(cls, nonce, ciphertext, combined): + obj = cls(combined) + obj._nonce = nonce + obj._ciphertext = ciphertext + return obj + + @property + def nonce(self): + """ + The nonce used during the encryption of the :class:`EncryptedMessage`. + """ + return self._nonce + + @property + def ciphertext(self): + """ + The ciphertext contained within the :class:`EncryptedMessage`. + """ + return self._ciphertext def random(size=32): diff --git a/tests/test_box.py b/tests/test_box.py index 539fa815db23341a5f0046ac0fee3afa063fadf3..82ffe436875b3bd94aab3e80da9eb7a1c178058f 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -38,11 +38,19 @@ def test_box_encryption(skalice, pkalice, skbob, pkbob, nonce, plaintext, cipher skbob = PrivateKey(skbob, encoder=HexEncoder) box = Box(skbob, pkalice) + encrypted = box.encrypt( + binascii.unhexlify(plaintext), + binascii.unhexlify(nonce), + encoder=HexEncoder, + ) - plaintext = binascii.unhexlify(plaintext) - nonce = binascii.unhexlify(nonce) + expected = binascii.hexlify( + binascii.unhexlify(nonce) + binascii.unhexlify(ciphertext), + ) - assert box.encrypt(plaintext, nonce, encoder=HexEncoder) == ciphertext + assert encrypted == expected + assert encrypted.nonce == nonce + assert encrypted.ciphertext == ciphertext @pytest.mark.parametrize(("skalice", "pkalice", "skbob", "pkbob", "nonce", "plaintext", "ciphertext"), VECTORS) @@ -59,6 +67,20 @@ def test_box_decryption(skalice, pkalice, skbob, pkbob, nonce, plaintext, cipher assert decrypted == plaintext +@pytest.mark.parametrize(("skalice", "pkalice", "skbob", "pkbob", "nonce", "plaintext", "ciphertext"), VECTORS) +def test_box_decryption_combined(skalice, pkalice, skbob, pkbob, nonce, plaintext, ciphertext): + pkbob = PublicKey(pkbob, encoder=HexEncoder) + skalice = PrivateKey(skalice, encoder=HexEncoder) + + box = Box(skalice, pkbob) + + combined = binascii.hexlify( + binascii.unhexlify(nonce) + binascii.unhexlify(ciphertext)) + decrypted = binascii.hexlify(box.decrypt(combined, 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) diff --git a/tests/test_secret.py b/tests/test_secret.py index 44e0ed7346acfff3308a7e17c8cacc9821e9487e..8c0462e2754af4b4d166dd0850cae3a821f357ca 100644 --- a/tests/test_secret.py +++ b/tests/test_secret.py @@ -26,11 +26,19 @@ def test_secret_box_creation(): @pytest.mark.parametrize(("key", "nonce", "plaintext", "ciphertext"), VECTORS) def test_secret_box_encryption(key, nonce, plaintext, ciphertext): box = SecretBox(key, encoder=HexEncoder) + encrypted = box.encrypt( + binascii.unhexlify(plaintext), + binascii.unhexlify(nonce), + encoder=HexEncoder, + ) - plaintext = binascii.unhexlify(plaintext) - nonce = binascii.unhexlify(nonce) + expected = binascii.hexlify( + binascii.unhexlify(nonce) + binascii.unhexlify(ciphertext), + ) - assert box.encrypt(plaintext, nonce, encoder=HexEncoder) == ciphertext + assert encrypted == expected + assert encrypted.nonce == nonce + assert encrypted.ciphertext == ciphertext @pytest.mark.parametrize(("key", "nonce", "plaintext", "ciphertext"), VECTORS) @@ -42,3 +50,14 @@ def test_secret_box_decryption(key, nonce, plaintext, ciphertext): box.decrypt(ciphertext, nonce, encoder=HexEncoder)) assert decrypted == plaintext + + +@pytest.mark.parametrize(("key", "nonce", "plaintext", "ciphertext"), VECTORS) +def test_secret_box_decryption_combined(key, nonce, plaintext, ciphertext): + box = SecretBox(key, encoder=HexEncoder) + + combined = binascii.hexlify( + binascii.unhexlify(nonce) + binascii.unhexlify(ciphertext)) + decrypted = binascii.hexlify(box.decrypt(combined, encoder=HexEncoder)) + + assert decrypted == plaintext