From 4e934b4d125489d408b447a2001302be0af24907 Mon Sep 17 00:00:00 2001
From: Donald Stufft <donald@stufft.io>
Date: Sun, 14 Apr 2013 00:17:51 -0400
Subject: [PATCH] Include the nonce inside the output of *Box.encrypt

* Provides the ability to pass around a single block of data instead
  of needing to handle transfering the nonce seperately.
* Opens up an avenue for automatic nonce handling in the future.
* Enables passing of nonces manually via explicit nonce parameters.
---
 docs/public.rst      | 13 +++++++++----
 docs/secret.rst      | 12 ++++++++----
 nacl/public.py       | 25 +++++++++++++++++++------
 nacl/secret.py       | 24 +++++++++++++++++++-----
 nacl/utils.py        | 29 +++++++++++++++++++++++++++++
 tests/test_box.py    | 28 +++++++++++++++++++++++++---
 tests/test_secret.py | 25 ++++++++++++++++++++++---
 7 files changed, 131 insertions(+), 25 deletions(-)

diff --git a/docs/public.rst b/docs/public.rst
index 9ba2712a..3090566b 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 3a431bc2..93c6e452 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 ae0c7922..266dd5fa 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 f2d06657..d9e23539 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 3902ee69..f70328f8 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 539fa815..82ffe436 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 44e0ed73..8c0462e2 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
-- 
GitLab