diff --git a/docs/encoding.rst b/docs/encoding.rst new file mode 100644 index 0000000000000000000000000000000000000000..d698cd3e17aa5e257fb8a5d4caf4065fb4f84acf --- /dev/null +++ b/docs/encoding.rst @@ -0,0 +1,58 @@ +Encoders +======== + +PyNaCl supports a simple method of encoding and decoding messages in different +formats. Encoders are simple classes with staticmethods that encode/decode and +are typically passed as a keyword argument `encoder` to various methods. + +For example you can generate a signing key and encode it in hex with: + +.. code:: python + + hex_key = nacl.signing.SigningKey.generate().encode(encoder=nacl.encoding.HexEncoder) + +Then you can later decode it from hex: + +.. code:: python + + signing_key = nacl.signing.SigningKey(hex_key, encoder=nacl.encoding.HexEncoder) + + +Built in Encoders +----------------- + +.. autoclass:: nacl.encoding.RawEncoder + :members: + +.. autoclass:: nacl.encoding.HexEncoder + :members: + +.. autoclass:: nacl.encoding.Base16Encoder + :members: + +.. autoclass:: nacl.encoding.Base32Encoder + :members: + +.. autoclass:: nacl.encoding.Base64Encoder + :members: + + +Defining your own Encoder +------------------------- + +Defining your own encoder is easy. Each encoder is simply a class with 2 static +methods. For example here is the hex encoder: + +.. code:: python + + import binascii + + class HexEncoder(object): + + @staticmethod + def encode(data): + return binascii.hexlify(data) + + @staticmethod + def decode(data): + return binascii.unhexlify(data) diff --git a/docs/index.rst b/docs/index.rst index c987ffbf29cde87ca443be70fc9d622fbfa8f050..6be9babe475bb5974449dd8b13fb582799513859 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,15 @@ Contents: signing +Support Features +---------------- + +.. toctree:: + :maxdepth: 2 + + encoding + + Api Documentation ----------------- diff --git a/docs/signing.rst b/docs/signing.rst index 8a67a8ca5f25e7de0e9c4e58e2824f7d8f22617b..f4e24950c24f5b6d05fe55871295632d8d8ce63a 100644 --- a/docs/signing.rst +++ b/docs/signing.rst @@ -19,7 +19,7 @@ Signer's perspective (:class:`~nacl.signing.SigningKey`) .. code:: python - import binascii + import nacl.encoding import nacl.signing # Generate a new random signing key @@ -32,17 +32,16 @@ Signer's perspective (:class:`~nacl.signing.SigningKey`) verify_key = signing_key.verify_key # Serialize the verify key to send it to a third party - binascii.hexlify(bytes(verify_key)) + verify_key_hex = verify_key.encode(encoder=nacl.encoding.HexEncoder) Verifier's perspective (:class:`~nacl.signing.VerifyKey`) .. code:: python - import binascii import nacl.signing # Create a VerifyKey object from a hex serialized public key - verify_key = nacl.signing.VerifyKey(binascii.unhexlify(verify_key_hex)) + verify_key = nacl.signing.VerifyKey(verify_key_hex, encoder=nacl.encoding.HexEncoder) # Check the validity of a message's signature # Will raise nacl.signing.BadSignatureError if the signature check fails diff --git a/nacl/encoding.py b/nacl/encoding.py new file mode 100644 index 0000000000000000000000000000000000000000..e885c9b7e62cd6b60c106058d4375fdc408e3737 --- /dev/null +++ b/nacl/encoding.py @@ -0,0 +1,63 @@ +import base64 +import binascii + + +class RawEncoder(object): + + @staticmethod + def encode(data): + return data + + @staticmethod + def decode(data): + return data + + +class HexEncoder(object): + + @staticmethod + def encode(data): + return binascii.hexlify(data) + + @staticmethod + def decode(data): + return binascii.unhexlify(data) + + +class Base16Encoder(object): + + @staticmethod + def encode(data): + return base64.b16encode(data) + + @staticmethod + def decode(data): + return base64.b16decode(data) + + +class Base32Encoder(object): + + @staticmethod + def encode(data): + return base64.b32encode(data) + + @staticmethod + def decode(data): + return base64.b32decode(data) + + +class Base64Encoder(object): + + @staticmethod + def encode(data): + return base64.b64encode(data) + + @staticmethod + def decode(data): + return base64.b64decode(data) + + +class Encodable(object): + + def encode(self, encoder=RawEncoder): + return encoder.encode(bytes(self)) diff --git a/nacl/hash.py b/nacl/hash.py index c2ef41124708b8c56fd818e3f7fa4ef6a5877bd3..a82c3ec1e7b48471b838c92902fff9a16b0a3f9e 100644 --- a/nacl/hash.py +++ b/nacl/hash.py @@ -1,29 +1,23 @@ from __future__ import absolute_import from __future__ import division -import binascii - -from . import nacl +from . import nacl, encoding from .exceptions import CryptoError -def sha256(message, binary=False): +def sha256(message, encoder=encoding.HexEncoder): digest = nacl.ffi.new("unsigned char[]", nacl.lib.crypto_hash_sha256_BYTES) if not nacl.lib.crypto_hash_sha256(digest, message, len(message)): raise CryptoError("Hashing failed") digest = nacl.ffi.buffer(digest, nacl.lib.crypto_hash_sha256_BYTES)[:] - if binary: - return digest - return binascii.hexlify(digest) + return encoder.encode(digest) -def sha512(message, binary=False): +def sha512(message, encoder=encoding.HexEncoder): digest = nacl.ffi.new("unsigned char[]", nacl.lib.crypto_hash_sha512_BYTES) if not nacl.lib.crypto_hash_sha512(digest, message, len(message)): raise CryptoError("Hashing failed") digest = nacl.ffi.buffer(digest, nacl.lib.crypto_hash_sha512_BYTES)[:] - if binary: - return digest - return binascii.hexlify(digest) + return encoder.encode(digest) diff --git a/nacl/signing.py b/nacl/signing.py index 53a33112462be63ddfa3e6e899c493cb710a890d..f08e3abf6164e92b7fa733cf34a950875cc3b1ba 100644 --- a/nacl/signing.py +++ b/nacl/signing.py @@ -3,7 +3,7 @@ from __future__ import division from . import six -from . import nacl +from . import nacl, encoding from .exceptions import CryptoError from .random import random @@ -19,37 +19,51 @@ class SignedMessage(six.binary_type): A bytes subclass that holds a messaged that has been signed by a :class:`SigningKey`. """ + @classmethod + def _from_parts(cls, signature, message, combined): + obj = cls(combined) + obj._signature = signature + obj._message = message + return obj + @property def signature(self): """ The signature contained within the :class:`SignedMessage`. """ - return self[:nacl.lib.crypto_sign_BYTES] + return self._signature @property def message(self): """ The message contained within the :class:`SignedMessage`. """ - return self[nacl.lib.crypto_sign_BYTES:] + return self._message -class VerifyKey(object): +class VerifyKey(encoding.Encodable, six.StringFixer, object): """ The public key counterpart to an Ed25519 SigningKey for producing digital signatures. :param key: [:class:`bytes`] Serialized Ed25519 public key + :param encoder: A class that is able to decode the `key` """ - def __init__(self, key): + def __init__(self, key, encoder=encoding.RawEncoder): + # Decode the key + key = encoder.decode(key) + if len(key) != nacl.lib.crypto_sign_PUBLICKEYBYTES: raise ValueError("The key must be exactly %s bytes long" % nacl.lib.crypto_sign_PUBLICKEYBYTES) self._key = key - def verify(self, smessage, signature=None): + def __bytes__(self): + return self._key + + def verify(self, smessage, signature=None, encoder=encoding.RawEncoder): """ Verifies the signature of a signed message, returning the message if it has not been tampered with else raising @@ -59,6 +73,8 @@ class VerifyKey(object): signature and message concated together. :param signature: [:class:`bytes`] If an unsigned message is given for smessage then the detached signature must be provded. + :param encoder: A class that is able to decode the secret message and + signature. :rtype: :class:`bytes` """ if signature is not None: @@ -66,6 +82,9 @@ class VerifyKey(object): # them. smessage = signature + smessage + # Decode the signed message + smessage = encoder.decode(smessage) + message = nacl.ffi.new("unsigned char[]", len(smessage)) message_len = nacl.ffi.new("unsigned long long *") @@ -75,7 +94,7 @@ class VerifyKey(object): return nacl.ffi.buffer(message, message_len[0])[:] -class SigningKey(object): +class SigningKey(encoding.Encodable, six.StringFixer, object): """ Private key for producing digital signatures using the Ed25519 algorithm. @@ -88,12 +107,16 @@ class SigningKey(object): masquerade as you. :param seed: [:class:`bytes`] Random 32-byte value (i.e. private key) + :param encoder: A class that is able to decode the seed :ivar: verify_key: [:class:`~nacl.signing.VerifyKey`] The verify (i.e. public) key that corresponds with this signing key. """ - def __init__(self, seed): + def __init__(self, seed, encoder=encoding.RawEncoder): + # Decode the seed + seed = encoder.decode(seed) + # Verify that our seed is the proper size seed_size = nacl.lib.crypto_sign_SECRETKEYBYTES // 2 if len(seed) != seed_size: @@ -113,6 +136,9 @@ class SigningKey(object): # Public values self.verify_key = VerifyKey(nacl.ffi.buffer(pk, nacl.lib.crypto_sign_PUBLICKEYBYTES)[:]) + def __bytes__(self): + return self._seed + @classmethod def generate(cls): """ @@ -120,13 +146,16 @@ class SigningKey(object): :rtype: :class:`~nacl.signing.SigningKey` """ - return cls(random(nacl.lib.crypto_sign_SECRETKEYBYTES // 2)) + return cls(random(nacl.lib.crypto_sign_SECRETKEYBYTES // 2), + encoder=encoding.RawEncoder, + ) - def sign(self, message): + def sign(self, message, encoder=encoding.RawEncoder): """ Sign a message using this key. :param message: [:class:`bytes`] The data to be signed. + :param encoder: A class that is used to encode the signed message. :rtype: :class:`~nacl.signing.SignedMessage` """ sm = nacl.ffi.new("unsigned char[]", len(message) + nacl.lib.crypto_sign_BYTES) @@ -135,4 +164,10 @@ class SigningKey(object): if not nacl.lib.crypto_sign(sm, smlen, message, len(message), self._signing_key): raise CryptoError("Failed to sign the message") - return SignedMessage(nacl.ffi.buffer(sm, smlen[0])[:]) + raw_signed = nacl.ffi.buffer(sm, smlen[0])[:] + + signature = encoder.encode(raw_signed[:nacl.lib.crypto_sign_BYTES]) + message = encoder.encode(raw_signed[nacl.lib.crypto_sign_BYTES:]) + signed = encoder.encode(raw_signed) + + return SignedMessage._from_parts(signature, message, signed) diff --git a/nacl/six.py b/nacl/six.py index ab45e3841ac5b373ba0d155e3c71030105a5691c..0838cf7472b5f1dffa6cab336395a13a5bd30484 100644 --- a/nacl/six.py +++ b/nacl/six.py @@ -390,3 +390,13 @@ _add_doc(reraise, """Reraise an exception.""") def with_metaclass(meta, base=object): """Create a base class with a metaclass.""" return meta("NewBase", (base,), {}) + + +# PyNaCl additions +class StringFixer(object): + + def __str__(self): + if PY3: + return self.__unicode__() + else: + return self.__bytes__() diff --git a/tests/test_hash.py b/tests/test_hash.py index 700c3bd041240d93d34158b6be9fa6a902cf1e1d..b24b25bd31392705e06035dc842d7f2468ed7392 100644 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -1,6 +1,8 @@ -import nacl import pytest +import nacl +import nacl.encoding + @pytest.mark.parametrize(("inp", "expected"), [ ( @@ -27,7 +29,7 @@ def test_sha256_hex(inp, expected): ) ]) def test_sha256_binary(inp, expected): - assert nacl.hash.sha256(inp, binary=True) == expected + assert nacl.hash.sha256(inp, encoder=nacl.encoding.RawEncoder) == expected @pytest.mark.parametrize(("inp", "expected"), [ @@ -55,4 +57,4 @@ def test_sha512_hex(inp, expected): ) ]) def test_sha512_binary(inp, expected): - assert nacl.hash.sha512(inp, binary=True) == expected + assert nacl.hash.sha512(inp, encoder=nacl.encoding.RawEncoder) == expected diff --git a/tests/test_signing.py b/tests/test_signing.py index fe57c970f15eb44e7fea66e00968578c9e259d84..d22e2944320d1eadc84a1d51c8b6d37fcfcaf8f7 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -7,6 +7,7 @@ import os import pytest import nacl +import nacl.encoding import nacl.nacl @@ -38,19 +39,19 @@ class TestSigningKey: b"77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", ]) def test_initialization_with_seed(self, seed): - nacl.signing.SigningKey(binascii.unhexlify(seed)) + nacl.signing.SigningKey(seed, encoder=nacl.encoding.HexEncoder) @pytest.mark.parametrize(("seed", "message", "signature", "expected"), [(x["seed"], x["message"], x["signature"], x["signed"]) for x in ed25519_known_answers()] ) def test_message_signing(self, seed, message, signature, expected): - signing_key = nacl.signing.SigningKey(binascii.unhexlify(seed)) - signed = signing_key.sign(binascii.unhexlify(message)) + signing_key = nacl.signing.SigningKey(seed, encoder=nacl.encoding.HexEncoder) + signed = signing_key.sign(binascii.unhexlify(message), encoder=nacl.encoding.HexEncoder) - assert binascii.hexlify(signed) == expected - assert binascii.hexlify(signed.message) == message - assert binascii.hexlify(signed.signature) == signature + assert signed == expected + assert signed.message == message + assert signed.signature == signature class TestVerifyKey: @@ -59,13 +60,10 @@ class TestVerifyKey: [(x["public_key"], x["signed"], x["message"], x["signature"]) for x in ed25519_known_answers()] ) def test_valid_signed_message(self, public_key, signed, message, signature): - key = nacl.signing.VerifyKey(binascii.unhexlify(public_key)) - signedb = binascii.unhexlify(signed) - messageb = binascii.unhexlify(message) - signatureb = binascii.unhexlify(signature) + key = nacl.signing.VerifyKey(public_key, encoder=nacl.encoding.HexEncoder) - assert binascii.hexlify(key.verify(signedb)) == message - assert binascii.hexlify(key.verify(messageb, signatureb)) == message + assert binascii.hexlify(key.verify(signed, encoder=nacl.encoding.HexEncoder)) == message + assert binascii.hexlify(key.verify(message, signature, encoder=nacl.encoding.HexEncoder)) == message def test_invalid_signed_message(self): skey = nacl.signing.SigningKey.generate()