diff --git a/.travis.yml b/.travis.yml index 41e4cb7..a77f25a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ language: python notifications: email: false python: -# - "3.5" + - "3.5" - "3.6" -# - "3.5-dev" # 3.5 development branch + - "3.5-dev" # 3.5 development branch - "3.6-dev" # 3.6 development branch # - "3.7-dev" # 3.7 development branch # command to install dependencies diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..66a31ff --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,22 @@ +Changelog +========= + +.. _v0-2-0: + +0.2.0 - `trunk` +~~~~~~~~~~~~~~~~ + +.. note:: This version is not yet released and is under active development. + +* Compatible with revision 33 (doesn't break compatibility with revision 32). +* Cryptography requirement updated to the newest version (2.1.1) - **Python 3.5** is supported again. +* Adding sphinx documentation for Read the Docs publication. +* Minor fixes for better performance. + + +.. _v0-1-0: + +0.1.1 - 2017-09-12 +~~~~~~~~~~~~~~~~~~ + +Initial release. \ No newline at end of file diff --git a/README.md b/README.md index 3d649b1..b1612ed 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,21 @@ noiseprotocol ============= [![Build Status](https://travis-ci.org/plizonczyk/noiseprotocol.svg?branch=master)](https://travis-ci.org/plizonczyk/noiseprotocol) [![PyPI](https://img.shields.io/pypi/v/noiseprotocol.svg)](https://pypi.python.org/pypi/noiseprotocol) +[![Documentation Status](https://readthedocs.org/projects/noiseprotocol/badge/)](http://noiseprotocol.readthedocs.io/) This repository contains source code of **noiseprotocol** - a Python 3 implementation of [Noise Protocol Framework](http://www.noiseprotocol.org/). +Compatible with revisions 32 and 33. ### Warning This package shall not be used (yet) for production purposes. There was little to none peer review done so far. Use common sense while using - until this package becomes stable. +## Documentation +Available on [Read the Docs](https://noiseprotocol.readthedocs.io). For now it provides basic documentation on +HandshakeState, CipherState and SymmetricState. Refer to the rest of the README below for more information. + ## Installation and prerequisites -For now, only Python 3.6 is supported. +For now, only Python 3.5+ is supported. Install via pip: ``` @@ -25,11 +31,16 @@ NoiseBuilder class provides highest level of abstraction for the package. You ca through this class' interfaces. An example for setting up NoiseBuilder could look like this: ```python +import socket + from noise.builder import NoiseBuilder +sock = socket.socket() +sock.connect(('localhost', 2000)) + # Create instance of NoiseBuilder, set up to use NN handshake pattern, Curve25519 for # elliptic curve keypair, ChaCha20Poly1305 as cipher function and SHA256 for hashing. -proto = NoiseBuilder.from_name('Noise_NN_25519_ChaChaPoly_SHA256') +proto = NoiseBuilder.from_name(b'Noise_NN_25519_ChaChaPoly_SHA256') # Set role in this connection as initiator proto.set_as_initiator() @@ -41,20 +52,62 @@ proto.start_handshake() message = proto.write_message() # Send the message to the responder - you may simply use sockets or any other way # to exchange bytes between communicating parties. -# For clarity - we omit socket creation in this example. -sock.send(message) +sock.sendall(message) # Receive the message from the responder -received = sock.recv() +received = sock.recv(2048) # Feed the received message into noise payload = proto.read_message(received) # As of now, the handshake should be finished (as we are using NN pattern). # Any further calls to write_message or read_message would raise NoiseHandshakeError exception. # We can use encrypt/decrypt methods of NoiseBuilder now for encryption and decryption of messages. -encrypted_message = proto.encrypt('This is an example payload') +encrypted_message = proto.encrypt(b'This is an example payload') +sock.sendall(encrypted_message) -ciphertext = sock.recv() +ciphertext = sock.recv(2048) plaintext = proto.decrypt(ciphertext) +print(plaintext) +``` + +The example above covers the connection from the initiator's ("client") point of view. The snippet below is an example of responder's code ("server") using a socket connection to send and receive ciphertext. + +```python +import socket +from itertools import cycle + +from noise.builder import NoiseBuilder + +if __name__ == '__main__': + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('localhost', 2000)) + s.listen(1) + + conn, addr = s.accept() + print('Accepted connection from', addr) + + noise = NoiseBuilder.from_name(b'Noise_NN_25519_ChaChaPoly_SHA256') + noise.set_as_responder() + noise.start_handshake() + + # Perform handshake. Break when finished + for action in cycle(['receive', 'send']): + if noise.handshake_finished: + break + elif action == 'send': + ciphertext = noise.write_message() + conn.sendall(ciphertext) + elif action == 'receive': + data = conn.recv(2048) + plaintext = noise.read_message(data) + + # Endless loop "echoing" received data + while True: + data = conn.recv(2048) + if not data: + break + received = noise.decrypt(data) + conn.sendall(noise.encrypt(received)) ``` #### Wireguard integration example @@ -82,11 +135,9 @@ pytest ### Todo-list for the project: - [ ] fallback patterns support -- [ ] documentation on Read the Docs and more extensive readme - [ ] scripts for keypair generation (+ console entry points) - [ ] "echo" (noise-c like) example - [ ] extensive logging -- [ ] bringing back Python 3.5 support and supporting Python 3.7 (dependent on Cryptography package updates) - [ ] move away from custom ed448 implementation - [ ] implement countermeasures for side-channel attacks - [ ] **get peer review of the code** diff --git a/dev_requirements.txt b/dev_requirements.txt new file mode 100644 index 0000000..b893bd7 --- /dev/null +++ b/dev_requirements.txt @@ -0,0 +1,4 @@ +pytest>=3.2.0 +sphinx>=1.6.4 +sphinx-autobuild>=0.7.1 +sphinx_rtd_theme>=0.2.4 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..36de6b0 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = noiseprotocol +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..bfc8e94 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# noiseprotocol documentation build configuration file, created by +# sphinx-quickstart on Sun Oct 8 01:15:26 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'noiseprotocol' +copyright = '2017, Piotr Lizonczyk' +author = 'Piotr Lizonczyk' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.2' +# The full version, including alpha/beta/rc tags. +release = '0.2.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +import sphinx_rtd_theme +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'noiseprotocoldoc' + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7cec576 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +.. noiseprotocol documentation master file, created by + sphinx-quickstart on Sun Oct 8 01:15:26 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to noiseprotocol's documentation! +========================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + +Documentation for the Code +************************** + +.. automodule:: noise.state + :members: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/noise/builder.py b/noise/builder.py index 4bc853e..fc6ca61 100644 --- a/noise/builder.py +++ b/noise/builder.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum from typing import Union, List from cryptography.exceptions import InvalidTag @@ -9,10 +9,10 @@ from .noise_protocol import NoiseProtocol class Keypair(Enum): - STATIC = auto() - REMOTE_STATIC = auto() - EPHEMERAL = auto() - REMOTE_EPHEMERAL = auto() + STATIC = 1 + REMOTE_STATIC = 2 + EPHEMERAL = 3 + REMOTE_EPHEMERAL = 4 _keypairs = {Keypair.STATIC: 's', Keypair.REMOTE_STATIC: 'rs', diff --git a/noise/crypto.py b/noise/crypto.py index 39cca26..c47f522 100644 --- a/noise/crypto.py +++ b/noise/crypto.py @@ -61,8 +61,8 @@ class X448(object): d = (x3 - z3) % P da = (d * a) % P cb = (c * b) % P - x3 = (((da + cb) % P) ** 2) % P - z3 = (x1 * (((da - cb) % P) ** 2) % P) % P + x3 = pow((da + cb) % P, 2, P) + z3 = (x1 * pow((da - cb) % P, 2, P)) % P x2 = (aa * bb) % P z2 = (e * ((aa + (A24 * e) % P) % P)) % P @@ -82,31 +82,29 @@ class X448(object): # Self-test # Test vectors taken from RFC 7748 section 5.2 and 6.2 -scalar1 = bytes.fromhex('203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b437' - '1282dd2c8d5be3095f') -u1 = bytes.fromhex('0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfb' - 'de71ce8d157db') -assert X448.mul(scalar1, u1) == bytes.fromhex('884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df3' - '4321d62077e63633c575c1c954514e99da7c179d') +scalar1 = bytes.fromhex( + '203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f') +u1 = bytes.fromhex( + '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db') +assert X448.mul(scalar1, u1) == bytes.fromhex( + '884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d') -scalar2 = bytes.fromhex('3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f5' - '4b29259a4f1c600ad3') -u2 = bytes.fromhex('06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa' - '7a7fb4ef8a086') -assert X448.mul(scalar2, u2) == bytes.fromhex('ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc6' - '6a01b0779d98223961111e21766282f73dd96b6f') +scalar2 = bytes.fromhex( + '3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3') +u2 = bytes.fromhex( + '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086') +assert X448.mul(scalar2, u2) == bytes.fromhex( + 'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f') -alice_priv = bytes.fromhex('9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f12' - '7ab1d9ac2d8c0a598726b') -alice_pub = bytes.fromhex('9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da8812' - '0d53177f80e532c41fa0') -bob_priv = bytes.fromhex('1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d' - '1b0366f10b65173992d') -bob_pub = bytes.fromhex('3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3d' - 'ae07bdc1c67bf33609') +alice_priv = bytes.fromhex( + '9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b') +alice_pub = bytes.fromhex( + '9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0') +bob_priv = bytes.fromhex( + '1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d') +bob_pub = bytes.fromhex( + '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609') assert alice_pub == X448.mul_5(alice_priv) assert bob_pub == X448.mul_5(bob_priv) -assert X448.mul(alice_priv, bob_pub) == X448.mul(bob_priv, alice_pub) == bytes.fromhex('07fff4181ac6cc95ec1c16a94a0f74d' - '12da232ce40a77552281d282bb60c0b' - '56fd2464c335543936521c24403085d' - '59a449a5037514a879d') +assert X448.mul(alice_priv, bob_pub) == X448.mul(bob_priv, alice_pub) == bytes.fromhex( + '07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d') diff --git a/noise/functions.py b/noise/functions.py index 0b86d31..4294778 100644 --- a/noise/functions.py +++ b/noise/functions.py @@ -1,15 +1,13 @@ import abc import warnings -from functools import partial # Turn back on when Cryptography gets fixed -import hashlib -import hmac +from functools import partial import os from cryptography.hazmat.backends import default_backend -# from cryptography.hazmat.primitives import hashes # Turn back on when Cryptography gets fixed +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 -# from cryptography.hazmat.primitives.hmac import HMAC # Turn back on when Cryptography gets fixed +from cryptography.hazmat.primitives.hmac import HMAC from noise.constants import MAX_NONCE from noise.exceptions import NoiseValueError from .crypto import X448 @@ -67,28 +65,22 @@ class Cipher(object): self.rekey = self._default_rekey else: raise NotImplementedError('Cipher method: {}'.format(method)) + self.cipher = None def _aesgcm_encrypt(self, k, n, ad, plaintext): - # Might be expensive to initialise AESGCM with the same key every time. The key should be (as per spec) kept in - # CipherState, but we may as well hold an initialised AESGCM and manage reinitialisation on CipherState.rekey - cipher = self._cipher(k) - return cipher.encrypt(nonce=self._aesgcm_nonce(n), data=plaintext, associated_data=ad) + return self.cipher.encrypt(nonce=self._aesgcm_nonce(n), data=plaintext, associated_data=ad) def _aesgcm_decrypt(self, k, n, ad, ciphertext): - cipher = self._cipher(k) - return cipher.decrypt(nonce=self._aesgcm_nonce(n), data=ciphertext, associated_data=ad) + return self.cipher.decrypt(nonce=self._aesgcm_nonce(n), data=ciphertext, associated_data=ad) def _aesgcm_nonce(self, n): return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='big') def _chacha20_encrypt(self, k, n, ad, plaintext): - # Same comment as with AESGCM - cipher = self._cipher(k) - return cipher.encrypt(nonce=self._chacha20_nonce(n), data=plaintext, associated_data=ad) + return self.cipher.encrypt(nonce=self._chacha20_nonce(n), data=plaintext, associated_data=ad) def _chacha20_decrypt(self, k, n, ad, ciphertext): - cipher = self._cipher(k) - return cipher.decrypt(nonce=self._chacha20_nonce(n), data=ciphertext, associated_data=ad) + return self.cipher.decrypt(nonce=self._chacha20_nonce(n), data=ciphertext, associated_data=ad) def _chacha20_nonce(self, n): return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='little') @@ -96,6 +88,9 @@ class Cipher(object): def _default_rekey(self, k): return self.encrypt(k, MAX_NONCE, b'', b'\x00' * 32)[:32] + def initialize(self, key): + self.cipher = self._cipher(key) + class Hash(object): def __init__(self, method): @@ -103,60 +98,44 @@ class Hash(object): self.hashlen = 32 self.blocklen = 64 self.hash = self._hash_sha256 - # self.fn = hashes.SHA256 # Turn back on when Cryptography gets fixed - self.fn = 'SHA256' + self.fn = hashes.SHA256 elif method == 'SHA512': self.hashlen = 64 self.blocklen = 128 self.hash = self._hash_sha512 - # self.fn = hashes.SHA512 # Turn back on when Cryptography gets fixed - self.fn = 'SHA512' + self.fn = hashes.SHA512 elif method == 'BLAKE2s': self.hashlen = 32 self.blocklen = 64 self.hash = self._hash_blake2s - # self.fn = partial(hashes.BLAKE2s, digest_size=self.hashlen) # Turn back on when Cryptography gets fixed - self.fn = 'blake2s' + self.fn = partial(hashes.BLAKE2s, digest_size=self.hashlen) elif method == 'BLAKE2b': self.hashlen = 64 self.blocklen = 128 self.hash = self._hash_blake2b - # self.fn = partial(hashes.BLAKE2b, digest_size=self.hashlen) # Turn back on when Cryptography gets fixed - self.fn = 'blake2b' + self.fn = partial(hashes.BLAKE2b, digest_size=self.hashlen) else: raise NotImplementedError('Hash method: {}'.format(method)) def _hash_sha256(self, data): - return hashlib.sha256(data).digest() + digest = hashes.Hash(hashes.SHA256(), backend) + digest.update(data) + return digest.finalize() def _hash_sha512(self, data): - return hashlib.sha512(data).digest() + digest = hashes.Hash(hashes.SHA512(), backend) + digest.update(data) + return digest.finalize() def _hash_blake2s(self, data): - return hashlib.blake2s(data).digest() + digest = hashes.Hash(hashes.BLAKE2s(digest_size=self.hashlen), backend) + digest.update(data) + return digest.finalize() def _hash_blake2b(self, data): - return hashlib.blake2b(data).digest() - - # def _hash_sha256(self, data): # Turn back on when Cryptography gets fixed - # digest = hashes.Hash(hashes.SHA256(), backend) - # digest.update(data) - # return digest.finalize() - # - # def _hash_sha512(self, data): # Turn back on when Cryptography gets fixed - # digest = hashes.Hash(hashes.SHA512(), backend) - # digest.update(data) - # return digest.finalize() - # - # def _hash_blake2s(self, data): # Turn back on when Cryptography gets fixed - # digest = hashes.Hash(hashes.BLAKE2s(digest_size=self.hashlen), backend) - # digest.update(data) - # return digest.finalize() - # - # def _hash_blake2b(self, data): # Turn back on when Cryptography gets fixed - # digest = hashes.Hash(hashes.BLAKE2b(digest_size=self.hashlen), backend) - # digest.update(data) - # return digest.finalize() + digest = hashes.Hash(hashes.BLAKE2b(digest_size=self.hashlen), backend) + digest.update(data) + return digest.finalize() class _KeyPair(object): @@ -227,8 +206,8 @@ dh_map = { } cipher_map = { - 'AESGCM': Cipher('AESGCM'), - 'ChaChaPoly': Cipher('ChaCha20') + 'AESGCM': partial(Cipher, 'AESGCM'), + 'ChaChaPoly': partial(Cipher, 'ChaCha20') } hash_map = { @@ -244,15 +223,11 @@ keypair_map = { } -# def hmac_hash(key, data, algorithm): # Turn back on when Cryptography gets fixed -# # Applies HMAC using the HASH() function. -# hmac = HMAC(key=key, algorithm=algorithm(), backend=backend) -# hmac.update(data=data) -# return hmac.finalize() - def hmac_hash(key, data, algorithm): # Applies HMAC using the HASH() function. - return hmac.new(key, data, algorithm).digest() + hmac = HMAC(key=key, algorithm=algorithm(), backend=backend) + hmac.update(data=data) + return hmac.finalize() def hkdf(chaining_key, input_key_material, num_outputs, hmac_hash_fn): diff --git a/noise/noise_protocol.py b/noise/noise_protocol.py index 7491f60..e067c00 100644 --- a/noise/noise_protocol.py +++ b/noise/noise_protocol.py @@ -102,7 +102,7 @@ class NoiseProtocol(object): self.cipher_state_decrypt = None else: self.cipher_state_encrypt = None - self.handshake_hash = self.symmetric_state.h + self.handshake_hash = self.symmetric_state.get_handshake_hash() del self.handshake_state del self.symmetric_state del self.cipher_state_handshake diff --git a/noise/patterns.py b/noise/patterns.py index 978766b..cca285a 100644 --- a/noise/patterns.py +++ b/noise/patterns.py @@ -60,7 +60,7 @@ class Pattern(object): def get_required_keypairs(self, initiator: bool) -> list: required = [] if initiator: - if self.name[0] in ['K', 'X', 'I']: + if self.name[0] in ('K', 'X', 'I'): required.append('s') if self.one_way or self.name[1] == 'K': required.append('rs') diff --git a/noise/state.py b/noise/state.py index 52317cd..63e53b6 100644 --- a/noise/state.py +++ b/noise/state.py @@ -6,31 +6,42 @@ from .constants import Empty, TOKEN_E, TOKEN_S, TOKEN_EE, TOKEN_ES, TOKEN_SE, TO class CipherState(object): """ - Implemented as per Noise Protocol specification (rev 32) - paragraph 5.1. + Implemented as per Noise Protocol specification - paragraph 5.1. The initialize_key() function takes additional required argument - noise_protocol. + + This class holds an instance of Cipher wrapper. It manages initialisation of underlying cipher function + with appropriate key in initialize_key() and rekey() methods. """ def __init__(self, noise_protocol): self.k = Empty() self.n = None - self.noise_protocol = noise_protocol + self.cipher = noise_protocol.cipher_fn() def initialize_key(self, key): """ + :param key: Key to set within CipherState """ self.k = key self.n = 0 + if self.has_key(): + self.cipher.initialize(key) def has_key(self): """ + :return: True if self.k is not an instance of Empty """ return not isinstance(self.k, Empty) + def set_nonce(self, nonce): + self.n = nonce + def encrypt_with_ad(self, ad: bytes, plaintext: bytes) -> bytes: """ If k is non-empty returns ENCRYPT(k, n++, ad, plaintext). Otherwise returns plaintext. + :param ad: bytes sequence :param plaintext: bytes sequence :return: ciphertext bytes sequence @@ -41,7 +52,7 @@ class CipherState(object): if not self.has_key(): return plaintext - ciphertext = self.noise_protocol.cipher_fn.encrypt(self.k, self.n, ad, plaintext) + ciphertext = self.cipher.encrypt(self.k, self.n, ad, plaintext) self.n = self.n + 1 return ciphertext @@ -49,6 +60,7 @@ class CipherState(object): """ If k is non-empty returns DECRYPT(k, n++, ad, ciphertext). Otherwise returns ciphertext. If an authentication failure occurs in DECRYPT() then n is not incremented and an error is signaled to the caller. + :param ad: bytes sequence :param ciphertext: bytes sequence :return: plaintext bytes sequence @@ -59,17 +71,18 @@ class CipherState(object): if not self.has_key(): return ciphertext - plaintext = self.noise_protocol.cipher_fn.decrypt(self.k, self.n, ad, ciphertext) + plaintext = self.cipher.decrypt(self.k, self.n, ad, ciphertext) self.n = self.n + 1 return plaintext def rekey(self): - self.k = self.noise_protocol.cipher_fn.rekey(self.k) + self.k = self.cipher.rekey(self.k) + self.cipher.initialize(self.k) class SymmetricState(object): """ - Implemented as per Noise Protocol specification (rev 32) - paragraph 5.2. + Implemented as per Noise Protocol specification - paragraph 5.2. The initialize_symmetric function takes different required argument - noise_protocol, which contains protocol_name. """ @@ -86,6 +99,7 @@ class SymmetricState(object): protocol name and crypto functions Comments below are mostly copied from specification. + :param noise_protocol: a valid NoiseProtocol instance :return: initialised SymmetricState instance """ @@ -112,6 +126,7 @@ class SymmetricState(object): def mix_key(self, input_key_material: bytes): """ + :param input_key_material: :return: """ @@ -127,6 +142,7 @@ class SymmetricState(object): def mix_hash(self, data: bytes): """ Sets h = HASH(h + data). + :param data: bytes sequence """ self.h = self.noise_protocol.hash_fn.hash(self.h + data) @@ -142,10 +158,14 @@ class SymmetricState(object): # Calls InitializeKey(temp_k). self.cipher_state.initialize_key(temp_k) + def get_handshake_hash(self): + return self.h + def encrypt_and_hash(self, plaintext: bytes) -> bytes: """ Sets ciphertext = EncryptWithAd(h, plaintext), calls MixHash(ciphertext), and returns ciphertext. Note that if k is empty, the EncryptWithAd() call will set ciphertext equal to plaintext. + :param plaintext: bytes sequence :return: ciphertext bytes sequence """ @@ -157,6 +177,7 @@ class SymmetricState(object): """ Sets plaintext = DecryptWithAd(h, ciphertext), calls MixHash(ciphertext), and returns plaintext. Note that if k is empty, the DecryptWithAd() call will set plaintext equal to ciphertext. + :param ciphertext: bytes sequence :return: plaintext bytes sequence """ @@ -167,6 +188,7 @@ class SymmetricState(object): def split(self): """ Returns a pair of CipherState objects for encrypting/decrypting transport messages. + :return: tuple (CipherState, CipherState) """ # Sets temp_k1, temp_k2 = HKDF(ck, b'', 2). @@ -197,7 +219,7 @@ class SymmetricState(object): class HandshakeState(object): """ - Implemented as per Noise Protocol specification (rev 32) - paragraph 5.3. + Implemented as per Noise Protocol specification - paragraph 5.3. The initialize() function takes different required argument - noise_protocol, which contains handshake_pattern. """ @@ -223,7 +245,7 @@ class HandshakeState(object): :param noise_protocol: a valid NoiseProtocol instance :param initiator: boolean indicating the initiator or responder role :param prologue: byte sequence which may be zero-length, or which may contain context information that both - parties want to confirm is identical + parties want to confirm is identical :param s: local static key pair :param e: local ephemeral key pair :param rs: remote party’s static public key @@ -270,6 +292,7 @@ class HandshakeState(object): def write_message(self, payload: Union[bytes, bytearray], message_buffer: bytearray): """ Comments below are mostly copied from specification. + :param payload: byte sequence which may be zero-length :param message_buffer: buffer-like object :return: None or result of SymmetricState.split() - tuple (CipherState, CipherState) @@ -328,6 +351,7 @@ class HandshakeState(object): def read_message(self, message: Union[bytes, bytearray], payload_buffer: bytearray): """ Comments below are mostly copied from specification. + :param message: byte sequence containing a Noise handshake message :param payload_buffer: buffer-like object :return: None or result of SymmetricState.split() - tuple (CipherState, CipherState) diff --git a/requirements.txt b/requirements.txt index aa49793..64d75b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -cryptography==2.0.3 +cryptography==2.1.1 diff --git a/setup.py b/setup.py index 2f1aaa8..297fbaa 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ except (IOError, ImportError): setup( name='noiseprotocol', - version='0.1.1', + version='0.2.0', description='Implementation of Noise Protocol Framework', long_description=long_description, url='https://github.com/plizonczyk/noiseprotocol', @@ -26,12 +26,12 @@ setup( 'Topic :: Security :: Cryptography', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - # 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', # 'Programming Language :: Python :: 3.7', ], keywords='cryptography noiseprotocol noise security', packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']), - install_requires=['cryptography==2.0.3'], - python_requires='~=3.6', + install_requires=['cryptography==2.1.1'], + python_requires='~=3.5,~=3.6', ) diff --git a/tests/test_rev33_compat.py b/tests/test_rev33_compat.py new file mode 100644 index 0000000..13eeb21 --- /dev/null +++ b/tests/test_rev33_compat.py @@ -0,0 +1,26 @@ +from noise.noise_protocol import NoiseProtocol +from noise.state import CipherState, SymmetricState + + +class TestRevision33Compatibility(object): + def test_noise_protocol_accepts_slash(self): + class FakeSHA3_256(): + fn = None + + noise_name = b"Noise_NN_25519_AESGCM_SHA3/256" + modified_class = NoiseProtocol + modified_class.methods['hash']['SHA3/256'] = FakeSHA3_256 # Add callable to hash functions mapping + modified_class(noise_name) + + def test_cipher_state_set_nonce(self): + noise_protocol = NoiseProtocol(b"Noise_NN_25519_AESGCM_SHA256") + cipher_state = CipherState(noise_protocol) + cipher_state.initialize_key(b'\x00'*32) + assert cipher_state.n == 0 + cipher_state.set_nonce(42) + assert cipher_state.n == 42 + + def test_symmetric_state_get_handshake_hash(self): + symmetric_state = SymmetricState() + symmetric_state.h = 42 + assert symmetric_state.get_handshake_hash() == 42 diff --git a/tests/test_vectors.py b/tests/test_vectors.py index 990cf68..2eece8c 100644 --- a/tests/test_vectors.py +++ b/tests/test_vectors.py @@ -15,10 +15,10 @@ vector_files = [ # As in test vectors specification (https://github.com/noiseprotocol/noise_wiki/wiki/Test-vectors) # We use this to cast read strings into bytes -byte_fields = ['protocol_name'] -hexbyte_fields = ['init_prologue', 'init_static', 'init_ephemeral', 'init_remote_static', 'resp_static', - 'resp_prologue', 'resp_ephemeral', 'resp_remote_static', 'handshake_hash'] -list_fields = ['init_psks', 'resp_psks'] +byte_field = 'protocol_name' +hexbyte_fields = ('init_prologue', 'init_static', 'init_ephemeral', 'init_remote_static', 'resp_static', + 'resp_prologue', 'resp_ephemeral', 'resp_remote_static', 'handshake_hash') +list_fields = ('init_psks', 'resp_psks') dict_field = 'messages' @@ -31,7 +31,7 @@ def _prepare_test_vectors(): for vector in vectors_list: for key, value in vector.copy().items(): - if key in byte_fields: + if key == byte_field: vector[key] = value.encode() if key in hexbyte_fields: vector[key] = bytes.fromhex(value)