Merge pull request #12 from plizonczyk/trunk

Trunk to master - towards 0.2.0 release
This commit is contained in:
Piotr Lizończyk
2017-10-30 23:37:46 +01:00
committed by GitHub
17 changed files with 373 additions and 119 deletions

View File

@@ -2,9 +2,9 @@ language: python
notifications: notifications:
email: false email: false
python: python:
# - "3.5" - "3.5"
- "3.6" - "3.6"
# - "3.5-dev" # 3.5 development branch - "3.5-dev" # 3.5 development branch
- "3.6-dev" # 3.6 development branch - "3.6-dev" # 3.6 development branch
# - "3.7-dev" # 3.7 development branch # - "3.7-dev" # 3.7 development branch
# command to install dependencies # command to install dependencies

22
CHANGELOG.rst Normal file
View File

@@ -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.

View File

@@ -2,15 +2,21 @@ noiseprotocol
============= =============
[![Build Status](https://travis-ci.org/plizonczyk/noiseprotocol.svg?branch=master)](https://travis-ci.org/plizonczyk/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) [![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/). 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 ### Warning
This package shall not be used (yet) for production purposes. There was little to none peer review done so far. 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. 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 ## Installation and prerequisites
For now, only Python 3.6 is supported. For now, only Python 3.5+ is supported.
Install via pip: 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: through this class' interfaces. An example for setting up NoiseBuilder could look like this:
```python ```python
import socket
from noise.builder import NoiseBuilder 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 # Create instance of NoiseBuilder, set up to use NN handshake pattern, Curve25519 for
# elliptic curve keypair, ChaCha20Poly1305 as cipher function and SHA256 for hashing. # 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 # Set role in this connection as initiator
proto.set_as_initiator() proto.set_as_initiator()
@@ -41,20 +52,62 @@ proto.start_handshake()
message = proto.write_message() message = proto.write_message()
# Send the message to the responder - you may simply use sockets or any other way # Send the message to the responder - you may simply use sockets or any other way
# to exchange bytes between communicating parties. # to exchange bytes between communicating parties.
# For clarity - we omit socket creation in this example. sock.sendall(message)
sock.send(message)
# Receive the message from the responder # Receive the message from the responder
received = sock.recv() received = sock.recv(2048)
# Feed the received message into noise # Feed the received message into noise
payload = proto.read_message(received) payload = proto.read_message(received)
# As of now, the handshake should be finished (as we are using NN pattern). # 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. # 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. # 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) 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 #### Wireguard integration example
@@ -82,11 +135,9 @@ pytest
### Todo-list for the project: ### Todo-list for the project:
- [ ] fallback patterns support - [ ] fallback patterns support
- [ ] documentation on Read the Docs and more extensive readme
- [ ] scripts for keypair generation (+ console entry points) - [ ] scripts for keypair generation (+ console entry points)
- [ ] "echo" (noise-c like) example - [ ] "echo" (noise-c like) example
- [ ] extensive logging - [ ] extensive logging
- [ ] bringing back Python 3.5 support and supporting Python 3.7 (dependent on Cryptography package updates)
- [ ] move away from custom ed448 implementation - [ ] move away from custom ed448 implementation
- [ ] implement countermeasures for side-channel attacks - [ ] implement countermeasures for side-channel attacks
- [ ] **get peer review of the code** - [ ] **get peer review of the code**

4
dev_requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
pytest>=3.2.0
sphinx>=1.6.4
sphinx-autobuild>=0.7.1
sphinx_rtd_theme>=0.2.4

20
docs/Makefile Normal file
View File

@@ -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)

108
docs/conf.py Normal file
View File

@@ -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'

26
docs/index.rst Normal file
View File

@@ -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`

View File

@@ -1,4 +1,4 @@
from enum import Enum, auto from enum import Enum
from typing import Union, List from typing import Union, List
from cryptography.exceptions import InvalidTag from cryptography.exceptions import InvalidTag
@@ -9,10 +9,10 @@ from .noise_protocol import NoiseProtocol
class Keypair(Enum): class Keypair(Enum):
STATIC = auto() STATIC = 1
REMOTE_STATIC = auto() REMOTE_STATIC = 2
EPHEMERAL = auto() EPHEMERAL = 3
REMOTE_EPHEMERAL = auto() REMOTE_EPHEMERAL = 4
_keypairs = {Keypair.STATIC: 's', Keypair.REMOTE_STATIC: 'rs', _keypairs = {Keypair.STATIC: 's', Keypair.REMOTE_STATIC: 'rs',

View File

@@ -61,8 +61,8 @@ class X448(object):
d = (x3 - z3) % P d = (x3 - z3) % P
da = (d * a) % P da = (d * a) % P
cb = (c * b) % P cb = (c * b) % P
x3 = (((da + cb) % P) ** 2) % P x3 = pow((da + cb) % P, 2, P)
z3 = (x1 * (((da - cb) % P) ** 2) % P) % P z3 = (x1 * pow((da - cb) % P, 2, P)) % P
x2 = (aa * bb) % P x2 = (aa * bb) % P
z2 = (e * ((aa + (A24 * e) % P) % P)) % P z2 = (e * ((aa + (A24 * e) % P) % P)) % P
@@ -82,31 +82,29 @@ class X448(object):
# Self-test # Self-test
# Test vectors taken from RFC 7748 section 5.2 and 6.2 # Test vectors taken from RFC 7748 section 5.2 and 6.2
scalar1 = bytes.fromhex('203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b437' scalar1 = bytes.fromhex(
'1282dd2c8d5be3095f') '203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f')
u1 = bytes.fromhex('0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfb' u1 = bytes.fromhex(
'de71ce8d157db') '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db')
assert X448.mul(scalar1, u1) == bytes.fromhex('884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df3' assert X448.mul(scalar1, u1) == bytes.fromhex(
'4321d62077e63633c575c1c954514e99da7c179d') '884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d')
scalar2 = bytes.fromhex('3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f5' scalar2 = bytes.fromhex(
'4b29259a4f1c600ad3') '3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3')
u2 = bytes.fromhex('06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa' u2 = bytes.fromhex(
'7a7fb4ef8a086') '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086')
assert X448.mul(scalar2, u2) == bytes.fromhex('ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc6' assert X448.mul(scalar2, u2) == bytes.fromhex(
'6a01b0779d98223961111e21766282f73dd96b6f') 'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f')
alice_priv = bytes.fromhex('9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f12' alice_priv = bytes.fromhex(
'7ab1d9ac2d8c0a598726b') '9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b')
alice_pub = bytes.fromhex('9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da8812' alice_pub = bytes.fromhex(
'0d53177f80e532c41fa0') '9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0')
bob_priv = bytes.fromhex('1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d' bob_priv = bytes.fromhex(
'1b0366f10b65173992d') '1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d')
bob_pub = bytes.fromhex('3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3d' bob_pub = bytes.fromhex(
'ae07bdc1c67bf33609') '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609')
assert alice_pub == X448.mul_5(alice_priv) assert alice_pub == X448.mul_5(alice_priv)
assert bob_pub == X448.mul_5(bob_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' assert X448.mul(alice_priv, bob_pub) == X448.mul(bob_priv, alice_pub) == bytes.fromhex(
'12da232ce40a77552281d282bb60c0b' '07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d')
'56fd2464c335543936521c24403085d'
'59a449a5037514a879d')

View File

@@ -1,15 +1,13 @@
import abc import abc
import warnings import warnings
from functools import partial # Turn back on when Cryptography gets fixed from functools import partial
import hashlib
import hmac
import os import os
from cryptography.hazmat.backends import default_backend 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.asymmetric import x25519
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 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.constants import MAX_NONCE
from noise.exceptions import NoiseValueError from noise.exceptions import NoiseValueError
from .crypto import X448 from .crypto import X448
@@ -67,28 +65,22 @@ class Cipher(object):
self.rekey = self._default_rekey self.rekey = self._default_rekey
else: else:
raise NotImplementedError('Cipher method: {}'.format(method)) raise NotImplementedError('Cipher method: {}'.format(method))
self.cipher = None
def _aesgcm_encrypt(self, k, n, ad, plaintext): 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 return self.cipher.encrypt(nonce=self._aesgcm_nonce(n), data=plaintext, associated_data=ad)
# 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)
def _aesgcm_decrypt(self, k, n, ad, ciphertext): def _aesgcm_decrypt(self, k, n, ad, ciphertext):
cipher = self._cipher(k) return self.cipher.decrypt(nonce=self._aesgcm_nonce(n), data=ciphertext, associated_data=ad)
return cipher.decrypt(nonce=self._aesgcm_nonce(n), data=ciphertext, associated_data=ad)
def _aesgcm_nonce(self, n): def _aesgcm_nonce(self, n):
return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='big') return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='big')
def _chacha20_encrypt(self, k, n, ad, plaintext): def _chacha20_encrypt(self, k, n, ad, plaintext):
# Same comment as with AESGCM return self.cipher.encrypt(nonce=self._chacha20_nonce(n), data=plaintext, associated_data=ad)
cipher = self._cipher(k)
return cipher.encrypt(nonce=self._chacha20_nonce(n), data=plaintext, associated_data=ad)
def _chacha20_decrypt(self, k, n, ad, ciphertext): def _chacha20_decrypt(self, k, n, ad, ciphertext):
cipher = self._cipher(k) return self.cipher.decrypt(nonce=self._chacha20_nonce(n), data=ciphertext, associated_data=ad)
return cipher.decrypt(nonce=self._chacha20_nonce(n), data=ciphertext, associated_data=ad)
def _chacha20_nonce(self, n): def _chacha20_nonce(self, n):
return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='little') 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): def _default_rekey(self, k):
return self.encrypt(k, MAX_NONCE, b'', b'\x00' * 32)[:32] return self.encrypt(k, MAX_NONCE, b'', b'\x00' * 32)[:32]
def initialize(self, key):
self.cipher = self._cipher(key)
class Hash(object): class Hash(object):
def __init__(self, method): def __init__(self, method):
@@ -103,60 +98,44 @@ class Hash(object):
self.hashlen = 32 self.hashlen = 32
self.blocklen = 64 self.blocklen = 64
self.hash = self._hash_sha256 self.hash = self._hash_sha256
# self.fn = hashes.SHA256 # Turn back on when Cryptography gets fixed self.fn = hashes.SHA256
self.fn = 'SHA256'
elif method == 'SHA512': elif method == 'SHA512':
self.hashlen = 64 self.hashlen = 64
self.blocklen = 128 self.blocklen = 128
self.hash = self._hash_sha512 self.hash = self._hash_sha512
# self.fn = hashes.SHA512 # Turn back on when Cryptography gets fixed self.fn = hashes.SHA512
self.fn = 'SHA512'
elif method == 'BLAKE2s': elif method == 'BLAKE2s':
self.hashlen = 32 self.hashlen = 32
self.blocklen = 64 self.blocklen = 64
self.hash = self._hash_blake2s self.hash = self._hash_blake2s
# self.fn = partial(hashes.BLAKE2s, digest_size=self.hashlen) # Turn back on when Cryptography gets fixed self.fn = partial(hashes.BLAKE2s, digest_size=self.hashlen)
self.fn = 'blake2s'
elif method == 'BLAKE2b': elif method == 'BLAKE2b':
self.hashlen = 64 self.hashlen = 64
self.blocklen = 128 self.blocklen = 128
self.hash = self._hash_blake2b self.hash = self._hash_blake2b
# self.fn = partial(hashes.BLAKE2b, digest_size=self.hashlen) # Turn back on when Cryptography gets fixed self.fn = partial(hashes.BLAKE2b, digest_size=self.hashlen)
self.fn = 'blake2b'
else: else:
raise NotImplementedError('Hash method: {}'.format(method)) raise NotImplementedError('Hash method: {}'.format(method))
def _hash_sha256(self, data): 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): 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): 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): def _hash_blake2b(self, data):
return hashlib.blake2b(data).digest() digest = hashes.Hash(hashes.BLAKE2b(digest_size=self.hashlen), backend)
digest.update(data)
# def _hash_sha256(self, data): # Turn back on when Cryptography gets fixed return digest.finalize()
# 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()
class _KeyPair(object): class _KeyPair(object):
@@ -227,8 +206,8 @@ dh_map = {
} }
cipher_map = { cipher_map = {
'AESGCM': Cipher('AESGCM'), 'AESGCM': partial(Cipher, 'AESGCM'),
'ChaChaPoly': Cipher('ChaCha20') 'ChaChaPoly': partial(Cipher, 'ChaCha20')
} }
hash_map = { 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): def hmac_hash(key, data, algorithm):
# Applies HMAC using the HASH() function. # 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): def hkdf(chaining_key, input_key_material, num_outputs, hmac_hash_fn):

View File

@@ -102,7 +102,7 @@ class NoiseProtocol(object):
self.cipher_state_decrypt = None self.cipher_state_decrypt = None
else: else:
self.cipher_state_encrypt = None 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.handshake_state
del self.symmetric_state del self.symmetric_state
del self.cipher_state_handshake del self.cipher_state_handshake

View File

@@ -60,7 +60,7 @@ class Pattern(object):
def get_required_keypairs(self, initiator: bool) -> list: def get_required_keypairs(self, initiator: bool) -> list:
required = [] required = []
if initiator: if initiator:
if self.name[0] in ['K', 'X', 'I']: if self.name[0] in ('K', 'X', 'I'):
required.append('s') required.append('s')
if self.one_way or self.name[1] == 'K': if self.one_way or self.name[1] == 'K':
required.append('rs') required.append('rs')

View File

@@ -6,31 +6,42 @@ from .constants import Empty, TOKEN_E, TOKEN_S, TOKEN_EE, TOKEN_ES, TOKEN_SE, TO
class CipherState(object): 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. 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): def __init__(self, noise_protocol):
self.k = Empty() self.k = Empty()
self.n = None self.n = None
self.noise_protocol = noise_protocol self.cipher = noise_protocol.cipher_fn()
def initialize_key(self, key): def initialize_key(self, key):
""" """
:param key: Key to set within CipherState :param key: Key to set within CipherState
""" """
self.k = key self.k = key
self.n = 0 self.n = 0
if self.has_key():
self.cipher.initialize(key)
def has_key(self): def has_key(self):
""" """
:return: True if self.k is not an instance of Empty :return: True if self.k is not an instance of Empty
""" """
return not isinstance(self.k, 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: def encrypt_with_ad(self, ad: bytes, plaintext: bytes) -> bytes:
""" """
If k is non-empty returns ENCRYPT(k, n++, ad, plaintext). Otherwise returns plaintext. If k is non-empty returns ENCRYPT(k, n++, ad, plaintext). Otherwise returns plaintext.
:param ad: bytes sequence :param ad: bytes sequence
:param plaintext: bytes sequence :param plaintext: bytes sequence
:return: ciphertext bytes sequence :return: ciphertext bytes sequence
@@ -41,7 +52,7 @@ class CipherState(object):
if not self.has_key(): if not self.has_key():
return plaintext 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 self.n = self.n + 1
return ciphertext 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 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. failure occurs in DECRYPT() then n is not incremented and an error is signaled to the caller.
:param ad: bytes sequence :param ad: bytes sequence
:param ciphertext: bytes sequence :param ciphertext: bytes sequence
:return: plaintext bytes sequence :return: plaintext bytes sequence
@@ -59,17 +71,18 @@ class CipherState(object):
if not self.has_key(): if not self.has_key():
return ciphertext 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 self.n = self.n + 1
return plaintext return plaintext
def rekey(self): 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): 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. 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 protocol name and crypto functions
Comments below are mostly copied from specification. Comments below are mostly copied from specification.
:param noise_protocol: a valid NoiseProtocol instance :param noise_protocol: a valid NoiseProtocol instance
:return: initialised SymmetricState instance :return: initialised SymmetricState instance
""" """
@@ -112,6 +126,7 @@ class SymmetricState(object):
def mix_key(self, input_key_material: bytes): def mix_key(self, input_key_material: bytes):
""" """
:param input_key_material: :param input_key_material:
:return: :return:
""" """
@@ -127,6 +142,7 @@ class SymmetricState(object):
def mix_hash(self, data: bytes): def mix_hash(self, data: bytes):
""" """
Sets h = HASH(h + data). Sets h = HASH(h + data).
:param data: bytes sequence :param data: bytes sequence
""" """
self.h = self.noise_protocol.hash_fn.hash(self.h + data) self.h = self.noise_protocol.hash_fn.hash(self.h + data)
@@ -142,10 +158,14 @@ class SymmetricState(object):
# Calls InitializeKey(temp_k). # Calls InitializeKey(temp_k).
self.cipher_state.initialize_key(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: def encrypt_and_hash(self, plaintext: bytes) -> bytes:
""" """
Sets ciphertext = EncryptWithAd(h, plaintext), calls MixHash(ciphertext), and returns ciphertext. Note that if 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. k is empty, the EncryptWithAd() call will set ciphertext equal to plaintext.
:param plaintext: bytes sequence :param plaintext: bytes sequence
:return: ciphertext 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 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. k is empty, the DecryptWithAd() call will set plaintext equal to ciphertext.
:param ciphertext: bytes sequence :param ciphertext: bytes sequence
:return: plaintext bytes sequence :return: plaintext bytes sequence
""" """
@@ -167,6 +188,7 @@ class SymmetricState(object):
def split(self): def split(self):
""" """
Returns a pair of CipherState objects for encrypting/decrypting transport messages. Returns a pair of CipherState objects for encrypting/decrypting transport messages.
:return: tuple (CipherState, CipherState) :return: tuple (CipherState, CipherState)
""" """
# Sets temp_k1, temp_k2 = HKDF(ck, b'', 2). # Sets temp_k1, temp_k2 = HKDF(ck, b'', 2).
@@ -197,7 +219,7 @@ class SymmetricState(object):
class HandshakeState(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. 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 noise_protocol: a valid NoiseProtocol instance
:param initiator: boolean indicating the initiator or responder role :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 :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 s: local static key pair
:param e: local ephemeral key pair :param e: local ephemeral key pair
:param rs: remote partys static public key :param rs: remote partys static public key
@@ -270,6 +292,7 @@ class HandshakeState(object):
def write_message(self, payload: Union[bytes, bytearray], message_buffer: bytearray): def write_message(self, payload: Union[bytes, bytearray], message_buffer: bytearray):
""" """
Comments below are mostly copied from specification. Comments below are mostly copied from specification.
:param payload: byte sequence which may be zero-length :param payload: byte sequence which may be zero-length
:param message_buffer: buffer-like object :param message_buffer: buffer-like object
:return: None or result of SymmetricState.split() - tuple (CipherState, CipherState) :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): def read_message(self, message: Union[bytes, bytearray], payload_buffer: bytearray):
""" """
Comments below are mostly copied from specification. Comments below are mostly copied from specification.
:param message: byte sequence containing a Noise handshake message :param message: byte sequence containing a Noise handshake message
:param payload_buffer: buffer-like object :param payload_buffer: buffer-like object
:return: None or result of SymmetricState.split() - tuple (CipherState, CipherState) :return: None or result of SymmetricState.split() - tuple (CipherState, CipherState)

View File

@@ -1 +1 @@
cryptography==2.0.3 cryptography==2.1.1

View File

@@ -13,7 +13,7 @@ except (IOError, ImportError):
setup( setup(
name='noiseprotocol', name='noiseprotocol',
version='0.1.1', version='0.2.0',
description='Implementation of Noise Protocol Framework', description='Implementation of Noise Protocol Framework',
long_description=long_description, long_description=long_description,
url='https://github.com/plizonczyk/noiseprotocol', url='https://github.com/plizonczyk/noiseprotocol',
@@ -26,12 +26,12 @@ setup(
'Topic :: Security :: Cryptography', 'Topic :: Security :: Cryptography',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
# 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
# 'Programming Language :: Python :: 3.7', # 'Programming Language :: Python :: 3.7',
], ],
keywords='cryptography noiseprotocol noise security', keywords='cryptography noiseprotocol noise security',
packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']), packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']),
install_requires=['cryptography==2.0.3'], install_requires=['cryptography==2.1.1'],
python_requires='~=3.6', python_requires='~=3.5,~=3.6',
) )

View File

@@ -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

View File

@@ -15,10 +15,10 @@ vector_files = [
# As in test vectors specification (https://github.com/noiseprotocol/noise_wiki/wiki/Test-vectors) # As in test vectors specification (https://github.com/noiseprotocol/noise_wiki/wiki/Test-vectors)
# We use this to cast read strings into bytes # We use this to cast read strings into bytes
byte_fields = ['protocol_name'] byte_field = 'protocol_name'
hexbyte_fields = ['init_prologue', 'init_static', 'init_ephemeral', 'init_remote_static', 'resp_static', hexbyte_fields = ('init_prologue', 'init_static', 'init_ephemeral', 'init_remote_static', 'resp_static',
'resp_prologue', 'resp_ephemeral', 'resp_remote_static', 'handshake_hash'] 'resp_prologue', 'resp_ephemeral', 'resp_remote_static', 'handshake_hash')
list_fields = ['init_psks', 'resp_psks'] list_fields = ('init_psks', 'resp_psks')
dict_field = 'messages' dict_field = 'messages'
@@ -31,7 +31,7 @@ def _prepare_test_vectors():
for vector in vectors_list: for vector in vectors_list:
for key, value in vector.copy().items(): for key, value in vector.copy().items():
if key in byte_fields: if key == byte_field:
vector[key] = value.encode() vector[key] = value.encode()
if key in hexbyte_fields: if key in hexbyte_fields:
vector[key] = bytes.fromhex(value) vector[key] = bytes.fromhex(value)