mirror of
https://github.com/morgan9e/noiseprotocol
synced 2026-04-14 00:14:05 +09:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
.. _v0-3-0:
|
||||
|
||||
0.3.0 - 2019-02-24
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added support for non-default crypto backends
|
||||
* Loosened restriction on Cryptography version in requirements.txt and bumped version to 2.5
|
||||
* Moved away from custom x448 implementation in favor of OpenSSL implementation (for default backend)
|
||||
|
||||
.. _v0-2-2:
|
||||
|
||||
0.2.2 - 2018-03-21
|
||||
|
||||
@@ -10,10 +10,6 @@ Compatible with revisions 32 and 33.
|
||||
|
||||
Master branch contains latest version released. Trunk branch is an active development branch.
|
||||
|
||||
### 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.
|
||||
@@ -138,12 +134,12 @@ pytest
|
||||
|
||||
### Todo-list for the project:
|
||||
|
||||
- [ ] custom crypto backends
|
||||
- [x] add non-default crypto algorithms support, as requested
|
||||
- [ ] fallback patterns support
|
||||
- [ ] scripts for keypair generation (+ console entry points)
|
||||
- [ ] "echo" (noise-c like) example
|
||||
- [ ] extensive logging
|
||||
- [ ] move away from custom ed448 implementation
|
||||
- [x] move away from custom ed448 implementation
|
||||
- [ ] implement countermeasures for side-channel attacks
|
||||
- [ ] **get peer review of the code**
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ author = 'Piotr Lizonczyk'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.2'
|
||||
version = '0.3'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.2.0'
|
||||
release = '0.3.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
0
noise/backends/__init__.py
Normal file
0
noise/backends/__init__.py
Normal file
3
noise/backends/default/__init__.py
Normal file
3
noise/backends/default/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .backend import DefaultNoiseBackend
|
||||
|
||||
noise_backend = DefaultNoiseBackend()
|
||||
38
noise/backends/default/backend.py
Normal file
38
noise/backends/default/backend.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from noise.backends.default.ciphers import ChaCha20Cipher, AESGCMCipher
|
||||
from noise.backends.default.diffie_hellmans import ED25519, ED448
|
||||
from noise.backends.default.hashes import hmac_hash, BLAKE2sHash, BLAKE2bHash, SHA256Hash, SHA512Hash
|
||||
from noise.backends.default.keypairs import KeyPair25519, KeyPair448
|
||||
from noise.backends.noise_backend import NoiseBackend
|
||||
|
||||
|
||||
class DefaultNoiseBackend(NoiseBackend):
|
||||
"""
|
||||
Contains all the crypto methods endorsed by Noise Protocol specification, using Cryptography as backend
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(DefaultNoiseBackend, self).__init__()
|
||||
|
||||
self.diffie_hellmans = {
|
||||
'25519': ED25519,
|
||||
'448': ED448
|
||||
}
|
||||
|
||||
self.ciphers = {
|
||||
'AESGCM': AESGCMCipher,
|
||||
'ChaChaPoly': ChaCha20Cipher
|
||||
}
|
||||
|
||||
self.hashes = {
|
||||
'BLAKE2s': BLAKE2sHash,
|
||||
'BLAKE2b': BLAKE2bHash,
|
||||
'SHA256': SHA256Hash,
|
||||
'SHA512': SHA512Hash
|
||||
}
|
||||
|
||||
self.keypairs = {
|
||||
'25519': KeyPair25519,
|
||||
'448': KeyPair448
|
||||
}
|
||||
|
||||
self.hmac = hmac_hash
|
||||
35
noise/backends/default/ciphers.py
Normal file
35
noise/backends/default/ciphers.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import abc
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
|
||||
|
||||
from noise.functions.cipher import Cipher
|
||||
|
||||
|
||||
class CryptographyCipher(Cipher, metaclass=abc.ABCMeta):
|
||||
def encrypt(self, k, n, ad, plaintext):
|
||||
return self.cipher.encrypt(nonce=self.format_nonce(n), data=plaintext, associated_data=ad)
|
||||
|
||||
def decrypt(self, k, n, ad, ciphertext):
|
||||
return self.cipher.decrypt(nonce=self.format_nonce(n), data=ciphertext, associated_data=ad)
|
||||
|
||||
@abc.abstractmethod
|
||||
def format_nonce(self, n):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AESGCMCipher(CryptographyCipher):
|
||||
@property
|
||||
def klass(self):
|
||||
return AESGCM
|
||||
|
||||
def format_nonce(self, n):
|
||||
return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='big')
|
||||
|
||||
|
||||
class ChaCha20Cipher(CryptographyCipher):
|
||||
@property
|
||||
def klass(self):
|
||||
return ChaCha20Poly1305
|
||||
|
||||
def format_nonce(self, n):
|
||||
return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='little')
|
||||
45
noise/backends/default/diffie_hellmans.py
Normal file
45
noise/backends/default/diffie_hellmans.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from cryptography.hazmat.primitives.asymmetric import x25519, x448
|
||||
|
||||
from noise.backends.default.keypairs import KeyPair25519, KeyPair448
|
||||
from noise.exceptions import NoiseValueError
|
||||
from noise.functions.dh import DH
|
||||
|
||||
|
||||
class ED25519(DH):
|
||||
@property
|
||||
def klass(self):
|
||||
return KeyPair25519
|
||||
|
||||
@property
|
||||
def dhlen(self):
|
||||
return 32
|
||||
|
||||
def generate_keypair(self) -> 'KeyPair':
|
||||
private_key = x25519.X25519PrivateKey.generate()
|
||||
public_key = private_key.public_key()
|
||||
return KeyPair25519(private_key, public_key, public_key.public_bytes())
|
||||
|
||||
def dh(self, private_key, public_key) -> bytes:
|
||||
if not isinstance(private_key, x25519.X25519PrivateKey) or not isinstance(public_key, x25519.X25519PublicKey):
|
||||
raise NoiseValueError('Invalid keys! Must be x25519.X25519PrivateKey and x25519.X25519PublicKey instances')
|
||||
return private_key.exchange(public_key)
|
||||
|
||||
|
||||
class ED448(DH):
|
||||
@property
|
||||
def klass(self):
|
||||
return KeyPair448
|
||||
|
||||
@property
|
||||
def dhlen(self):
|
||||
return 56
|
||||
|
||||
def generate_keypair(self) -> 'KeyPair':
|
||||
private_key = x448.X448PrivateKey.generate()
|
||||
public_key = private_key.public_key()
|
||||
return KeyPair448(private_key, public_key, public_key.public_bytes())
|
||||
|
||||
def dh(self, private_key, public_key) -> bytes:
|
||||
if not isinstance(private_key, x448.X448PrivateKey) or not isinstance(public_key, x448.X448PublicKey):
|
||||
raise NoiseValueError('Invalid keys! Must be x448.X448PrivateKey and x448.X448PublicKey instances')
|
||||
return private_key.exchange(public_key)
|
||||
80
noise/backends/default/hashes.py
Normal file
80
noise/backends/default/hashes.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import abc
|
||||
from functools import partial
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.hmac import HMAC
|
||||
|
||||
from noise.functions.hash import Hash
|
||||
|
||||
cryptography_backend = default_backend()
|
||||
|
||||
|
||||
class CryptographyHash(Hash, metaclass=abc.ABCMeta):
|
||||
def hash(self, data):
|
||||
digest = hashes.Hash(self.fn(), cryptography_backend)
|
||||
digest.update(data)
|
||||
return digest.finalize()
|
||||
|
||||
|
||||
class SHA256Hash(CryptographyHash):
|
||||
@property
|
||||
def fn(self):
|
||||
return hashes.SHA256
|
||||
|
||||
@property
|
||||
def hashlen(self):
|
||||
return 32
|
||||
|
||||
@property
|
||||
def blocklen(self):
|
||||
return 64
|
||||
|
||||
|
||||
class SHA512Hash(CryptographyHash):
|
||||
@property
|
||||
def fn(self):
|
||||
return hashes.SHA512
|
||||
|
||||
@property
|
||||
def hashlen(self):
|
||||
return 64
|
||||
|
||||
@property
|
||||
def blocklen(self):
|
||||
return 128
|
||||
|
||||
|
||||
class BLAKE2sHash(CryptographyHash):
|
||||
@property
|
||||
def fn(self):
|
||||
return partial(hashes.BLAKE2s, digest_size=self.hashlen)
|
||||
|
||||
@property
|
||||
def hashlen(self):
|
||||
return 32
|
||||
|
||||
@property
|
||||
def blocklen(self):
|
||||
return 64
|
||||
|
||||
|
||||
class BLAKE2bHash(CryptographyHash):
|
||||
@property
|
||||
def fn(self):
|
||||
return partial(hashes.BLAKE2b, digest_size=self.hashlen)
|
||||
|
||||
@property
|
||||
def hashlen(self):
|
||||
return 64
|
||||
|
||||
@property
|
||||
def blocklen(self):
|
||||
return 128
|
||||
|
||||
|
||||
def hmac_hash(key, data, algorithm):
|
||||
# Applies HMAC using the HASH() function.
|
||||
hmac = HMAC(key=key, algorithm=algorithm(), backend=cryptography_backend)
|
||||
hmac.update(data=data)
|
||||
return hmac.finalize()
|
||||
39
noise/backends/default/keypairs.py
Normal file
39
noise/backends/default/keypairs.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import x25519, x448
|
||||
|
||||
from noise.exceptions import NoiseValueError
|
||||
from noise.functions.keypair import KeyPair
|
||||
|
||||
|
||||
class KeyPair25519(KeyPair):
|
||||
@classmethod
|
||||
def from_private_bytes(cls, private_bytes):
|
||||
if len(private_bytes) != 32:
|
||||
raise NoiseValueError('Invalid length of private_bytes! Should be 32')
|
||||
private = x25519.X25519PrivateKey.from_private_bytes(private_bytes)
|
||||
public = private.public_key()
|
||||
return cls(private=private, public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw))
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, public_bytes):
|
||||
if len(public_bytes) != 32:
|
||||
raise NoiseValueError('Invalid length of public_bytes! Should be 32')
|
||||
public = x25519.X25519PublicKey.from_public_bytes(public_bytes)
|
||||
return cls(public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw))
|
||||
|
||||
|
||||
class KeyPair448(KeyPair):
|
||||
@classmethod
|
||||
def from_private_bytes(cls, private_bytes):
|
||||
if len(private_bytes) != 56:
|
||||
raise NoiseValueError('Invalid length of private_bytes! Should be 56')
|
||||
private = x448.X448PrivateKey.from_private_bytes(private_bytes)
|
||||
public = private.public_key()
|
||||
return cls(private=private, public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw))
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, public_bytes):
|
||||
if len(public_bytes) != 56:
|
||||
raise NoiseValueError('Invalid length of private_bytes! Should be 56')
|
||||
public = x448.X448PublicKey.from_public_bytes(public_bytes)
|
||||
return cls(public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw))
|
||||
3
noise/backends/experimental/__init__.py
Normal file
3
noise/backends/experimental/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from noise.backends.experimental.backend import ExperimentalNoiseBackend
|
||||
|
||||
noise_backend = ExperimentalNoiseBackend()
|
||||
8
noise/backends/experimental/backend.py
Normal file
8
noise/backends/experimental/backend.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from noise.backends.default import DefaultNoiseBackend
|
||||
|
||||
|
||||
class ExperimentalNoiseBackend(DefaultNoiseBackend):
|
||||
"""
|
||||
Contains all the default crypto methods, but also methods not directly endorsed by Noise Protocol specification
|
||||
"""
|
||||
pass
|
||||
64
noise/backends/noise_backend.py
Normal file
64
noise/backends/noise_backend.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from noise.exceptions import NoiseProtocolNameError
|
||||
from noise.functions.hash import hkdf
|
||||
from noise.patterns import (PatternN, PatternK, PatternX, PatternNN, PatternKN, PatternNK, PatternKK, PatternNX,
|
||||
PatternKX, PatternXN, PatternIN, PatternXK, PatternIK, PatternXX, PatternIX)
|
||||
|
||||
|
||||
class NoiseBackend:
|
||||
"""
|
||||
Base for creating backends.
|
||||
Implementing classes must define supported crypto methods in appropriate dict (diffie_hellmans, ciphers, etc.)
|
||||
HMAC function must be defined as well.
|
||||
|
||||
Dicts use convention for keys - they must match the string that occurs in Noise Protocol name.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.patterns = {
|
||||
'N': PatternN,
|
||||
'K': PatternK,
|
||||
'X': PatternX,
|
||||
'NN': PatternNN,
|
||||
'KN': PatternKN,
|
||||
'NK': PatternNK,
|
||||
'KK': PatternKK,
|
||||
'NX': PatternNX,
|
||||
'KX': PatternKX,
|
||||
'XN': PatternXN,
|
||||
'IN': PatternIN,
|
||||
'XK': PatternXK,
|
||||
'IK': PatternIK,
|
||||
'XX': PatternXX,
|
||||
'IX': PatternIX,
|
||||
}
|
||||
|
||||
self.diffie_hellmans = {}
|
||||
self.ciphers = {}
|
||||
self.hashes = {}
|
||||
self.keypairs = {}
|
||||
self.hmac = None
|
||||
|
||||
self.hkdf = hkdf
|
||||
|
||||
@property
|
||||
def methods(self):
|
||||
return {
|
||||
'pattern': self.patterns,
|
||||
'dh': self.diffie_hellmans,
|
||||
'cipher': self.ciphers,
|
||||
'hash': self.hashes,
|
||||
'keypair': self.keypairs
|
||||
}
|
||||
|
||||
def map_protocol_name_to_crypto(self, unpacked_name):
|
||||
mappings = {}
|
||||
# Validate if we know everything that Noise Protocol is supposed to use and map appropriate functions
|
||||
for method, map_dict in self.methods.items():
|
||||
looked_up_func = getattr(unpacked_name, method)
|
||||
func = map_dict.get(looked_up_func)
|
||||
if not func:
|
||||
raise NoiseProtocolNameError('Unknown {} in Noise Protocol name, given {}, known {}'.format(
|
||||
method, looked_up_func, " ".join(map_dict)))
|
||||
mappings[method] = func
|
||||
|
||||
return mappings
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Union, List
|
||||
|
||||
from cryptography.exceptions import InvalidTag
|
||||
|
||||
from noise.backends.default import noise_backend
|
||||
from noise.constants import MAX_MESSAGE_LEN
|
||||
from noise.exceptions import NoisePSKError, NoiseValueError, NoiseHandshakeError, NoiseInvalidMessage
|
||||
from .noise_protocol import NoiseProtocol
|
||||
@@ -21,6 +22,7 @@ _keypairs = {Keypair.STATIC: 's', Keypair.REMOTE_STATIC: 'rs',
|
||||
|
||||
class NoiseConnection(object):
|
||||
def __init__(self):
|
||||
self.backend = None
|
||||
self.noise_protocol = None
|
||||
self.protocol_name = None
|
||||
self.handshake_finished = False
|
||||
@@ -28,14 +30,14 @@ class NoiseConnection(object):
|
||||
self._next_fn = None
|
||||
|
||||
@classmethod
|
||||
def from_name(cls, name: Union[str, bytes]):
|
||||
def from_name(cls, name: Union[str, bytes], backend=noise_backend):
|
||||
instance = cls()
|
||||
# Forgiving passing string. Bytes are good too, anything else will fail inside NoiseProtocol
|
||||
try:
|
||||
instance.protocol_name = name.encode('ascii') if isinstance(name, str) else name
|
||||
except ValueError:
|
||||
raise NoiseValueError('If passing string as protocol name, it must contain only ASCII characters')
|
||||
instance.noise_protocol = NoiseProtocol(protocol_name=name)
|
||||
instance.noise_protocol = NoiseProtocol(protocol_name=name, backend=backend)
|
||||
return instance
|
||||
|
||||
def set_psks(self, psk: Union[bytes, str] = None, psks: List[Union[str, bytes]] = None):
|
||||
@@ -74,21 +76,21 @@ class NoiseConnection(object):
|
||||
|
||||
def set_keypair_from_private_bytes(self, keypair: Keypair, private_bytes: bytes):
|
||||
self.noise_protocol.keypairs[_keypairs[keypair]] = \
|
||||
self.noise_protocol.dh_fn.keypair_cls.from_private_bytes(private_bytes)
|
||||
self.noise_protocol.dh_fn.klass.from_private_bytes(private_bytes)
|
||||
|
||||
def set_keypair_from_public_bytes(self, keypair: Keypair, private_bytes: bytes):
|
||||
self.noise_protocol.keypairs[_keypairs[keypair]] = \
|
||||
self.noise_protocol.dh_fn.keypair_cls.from_public_bytes(private_bytes)
|
||||
self.noise_protocol.dh_fn.klass.from_public_bytes(private_bytes)
|
||||
|
||||
def set_keypair_from_private_path(self, keypair: Keypair, path: str):
|
||||
with open(path, 'rb') as fd:
|
||||
self.noise_protocol.keypairs[_keypairs[keypair]] = \
|
||||
self.noise_protocol.dh_fn.keypair_cls.from_private_bytes(fd.read())
|
||||
self.noise_protocol.dh_fn.klass.from_private_bytes(fd.read())
|
||||
|
||||
def set_keypair_from_public_path(self, keypair: Keypair, path: str):
|
||||
with open(path, 'rb') as fd:
|
||||
self.noise_protocol.keypairs[_keypairs[keypair]] = \
|
||||
self.noise_protocol.dh_fn.keypair_cls.from_public_bytes(fd.read())
|
||||
self.noise_protocol.dh_fn.klass.from_public_bytes(fd.read())
|
||||
|
||||
def start_handshake(self):
|
||||
self.noise_protocol.validate()
|
||||
|
||||
110
noise/crypto.py
110
noise/crypto.py
@@ -1,110 +0,0 @@
|
||||
DHLEN = 56
|
||||
P = 2 ** 448 - 2 ** 224 - 1
|
||||
A24 = 39081
|
||||
|
||||
|
||||
class X448(object):
|
||||
# Based on RFC 7748 and heavily relying on it - https://tools.ietf.org/html/rfc7748#section-5
|
||||
# Modified mostly to fulfill python3 changes
|
||||
# Almost surely unsafe from side-channel attacks.
|
||||
# Should be replaced with safer implementation (most likely one from OpenSSL and/or pyca/Cryptography)
|
||||
@staticmethod
|
||||
def decode_little_endian(b):
|
||||
assert len(b) == DHLEN
|
||||
return sum([b[i] << 8 * i for i in range(DHLEN)])
|
||||
|
||||
@staticmethod
|
||||
def decode_u_coordinate(u):
|
||||
u[-1] &= (1 << 56) - 1
|
||||
return X448.decode_little_endian(u)
|
||||
|
||||
@staticmethod
|
||||
def encode_u_coordinate(u):
|
||||
return bytes([(u >> 8 * i) & 255 for i in range(DHLEN)])
|
||||
|
||||
@staticmethod
|
||||
def decode_scalar448(k):
|
||||
k = [b for b in k]
|
||||
k[0] &= 252
|
||||
k[55] |= 128
|
||||
return X448.decode_little_endian(k)
|
||||
|
||||
@staticmethod
|
||||
def cswap(swap, x2, x3):
|
||||
dummy = (swap * (x2 - x3)) % P
|
||||
x2 = (x2 - dummy) % P
|
||||
x3 = (x3 + dummy) % P
|
||||
return x2, x3
|
||||
|
||||
@staticmethod
|
||||
def x448(k, u):
|
||||
x1 = u
|
||||
x2 = 1
|
||||
z2 = 0
|
||||
x3 = u
|
||||
z3 = 1
|
||||
swap = 0
|
||||
|
||||
for t in range(448-1, -1, -1):
|
||||
k_t = (k >> t) & 1
|
||||
swap ^= k_t
|
||||
x2, x3 = X448.cswap(swap, x2, x3)
|
||||
z2, z3 = X448.cswap(swap, z2, z3)
|
||||
swap = k_t
|
||||
|
||||
a = (x2 + z2) % P
|
||||
aa = (a * a) % P
|
||||
b = (x2 - z2) % P
|
||||
bb = (b * b) % P
|
||||
e = (aa - bb) % P
|
||||
c = (x3 + z3) % P
|
||||
d = (x3 - z3) % P
|
||||
da = (d * a) % P
|
||||
cb = (c * b) % 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
|
||||
|
||||
x2, x3 = X448.cswap(swap, x2, x3)
|
||||
z2, z3 = X448.cswap(swap, z2, z3)
|
||||
|
||||
return (x2 * pow(z2, P - 2, P)) % P
|
||||
|
||||
@staticmethod
|
||||
def mul(n, p):
|
||||
return X448.encode_u_coordinate(X448.x448(X448.decode_scalar448(n), X448.decode_little_endian(p)))
|
||||
|
||||
@staticmethod
|
||||
def mul_5(n):
|
||||
return X448.encode_u_coordinate(X448.x448(X448.decode_scalar448(n), 5))
|
||||
|
||||
|
||||
# Self-test
|
||||
# Test vectors taken from RFC 7748 section 5.2 and 6.2
|
||||
scalar1 = bytes.fromhex(
|
||||
'203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f')
|
||||
u1 = bytes.fromhex(
|
||||
'0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db')
|
||||
assert X448.mul(scalar1, u1) == bytes.fromhex(
|
||||
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d')
|
||||
|
||||
scalar2 = bytes.fromhex(
|
||||
'3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3')
|
||||
u2 = bytes.fromhex(
|
||||
'06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086')
|
||||
assert X448.mul(scalar2, u2) == bytes.fromhex(
|
||||
'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f')
|
||||
|
||||
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(
|
||||
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d')
|
||||
@@ -1,251 +0,0 @@
|
||||
import abc
|
||||
import warnings
|
||||
from functools import partial
|
||||
import os
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
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
|
||||
from noise.constants import MAX_NONCE
|
||||
from noise.exceptions import NoiseValueError
|
||||
from .crypto import X448
|
||||
|
||||
backend = default_backend()
|
||||
|
||||
|
||||
class DH(object):
|
||||
def __init__(self, method):
|
||||
if method == 'ed25519':
|
||||
self.method = method
|
||||
self.dhlen = 32
|
||||
self.keypair_cls = KeyPair25519
|
||||
self.generate_keypair = self._25519_generate_keypair
|
||||
self.dh = self._25519_dh
|
||||
elif method == 'ed448':
|
||||
self.method = method
|
||||
self.dhlen = 56
|
||||
self.keypair_cls = KeyPair448
|
||||
self.generate_keypair = self._448_generate_keypair
|
||||
self.dh = self._448_dh
|
||||
else:
|
||||
raise NotImplementedError('DH method: {}'.format(method))
|
||||
|
||||
def _25519_generate_keypair(self) -> '_KeyPair':
|
||||
private_key = x25519.X25519PrivateKey.generate()
|
||||
public_key = private_key.public_key()
|
||||
return KeyPair25519(private_key, public_key, public_key.public_bytes())
|
||||
|
||||
def _25519_dh(self, private_key: 'x25519.X25519PrivateKey', public_key: 'x25519.X25519PublicKey') -> bytes:
|
||||
if not isinstance(private_key, x25519.X25519PrivateKey) or not isinstance(public_key, x25519.X25519PublicKey):
|
||||
raise NoiseValueError('Invalid keys! Must be x25519.X25519PrivateKey and x25519.X25519PublicKey instances')
|
||||
return private_key.exchange(public_key)
|
||||
|
||||
def _448_generate_keypair(self) -> '_KeyPair':
|
||||
return KeyPair448.new()
|
||||
|
||||
def _448_dh(self, private_key: bytes, public_key: bytes) -> bytes:
|
||||
if len(private_key) != self.dhlen or len(public_key) != self.dhlen:
|
||||
raise NoiseValueError('Invalid length of keys! Should be {}'.format(self.dhlen))
|
||||
return X448.mul(private_key, public_key)
|
||||
|
||||
|
||||
class Cipher(object):
|
||||
def __init__(self, method):
|
||||
if method == 'AESGCM':
|
||||
self._cipher = AESGCM
|
||||
self.encrypt = self._aesgcm_encrypt
|
||||
self.decrypt = self._aesgcm_decrypt
|
||||
self.rekey = self._default_rekey
|
||||
elif method == 'ChaCha20':
|
||||
self._cipher = ChaCha20Poly1305
|
||||
self.encrypt = self._chacha20_encrypt
|
||||
self.decrypt = self._chacha20_decrypt
|
||||
self.rekey = self._default_rekey
|
||||
else:
|
||||
raise NotImplementedError('Cipher method: {}'.format(method))
|
||||
self.cipher = None
|
||||
|
||||
def _aesgcm_encrypt(self, k, n, ad, plaintext):
|
||||
return self.cipher.encrypt(nonce=self._aesgcm_nonce(n), data=plaintext, associated_data=ad)
|
||||
|
||||
def _aesgcm_decrypt(self, k, n, ad, ciphertext):
|
||||
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):
|
||||
return self.cipher.encrypt(nonce=self._chacha20_nonce(n), data=plaintext, associated_data=ad)
|
||||
|
||||
def _chacha20_decrypt(self, k, n, ad, ciphertext):
|
||||
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')
|
||||
|
||||
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):
|
||||
if method == 'SHA256':
|
||||
self.hashlen = 32
|
||||
self.blocklen = 64
|
||||
self.hash = self._hash_sha256
|
||||
self.fn = hashes.SHA256
|
||||
elif method == 'SHA512':
|
||||
self.hashlen = 64
|
||||
self.blocklen = 128
|
||||
self.hash = self._hash_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)
|
||||
elif method == 'BLAKE2b':
|
||||
self.hashlen = 64
|
||||
self.blocklen = 128
|
||||
self.hash = self._hash_blake2b
|
||||
self.fn = partial(hashes.BLAKE2b, digest_size=self.hashlen)
|
||||
else:
|
||||
raise NotImplementedError('Hash method: {}'.format(method))
|
||||
|
||||
def _hash_sha256(self, data):
|
||||
digest = hashes.Hash(hashes.SHA256(), backend)
|
||||
digest.update(data)
|
||||
return digest.finalize()
|
||||
|
||||
def _hash_sha512(self, data):
|
||||
digest = hashes.Hash(hashes.SHA512(), backend)
|
||||
digest.update(data)
|
||||
return digest.finalize()
|
||||
|
||||
def _hash_blake2s(self, data):
|
||||
digest = hashes.Hash(hashes.BLAKE2s(digest_size=self.hashlen), backend)
|
||||
digest.update(data)
|
||||
return digest.finalize()
|
||||
|
||||
def _hash_blake2b(self, data):
|
||||
digest = hashes.Hash(hashes.BLAKE2b(digest_size=self.hashlen), backend)
|
||||
digest.update(data)
|
||||
return digest.finalize()
|
||||
|
||||
|
||||
class _KeyPair(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, private=None, public=None, public_bytes=None):
|
||||
self.private = private
|
||||
self.public = public
|
||||
self.public_bytes = public_bytes
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_private_bytes(cls, private_bytes):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_public_bytes(cls, public_bytes):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class KeyPair25519(_KeyPair):
|
||||
@classmethod
|
||||
def from_private_bytes(cls, private_bytes):
|
||||
if len(private_bytes) != 32:
|
||||
raise NoiseValueError('Invalid length of private_bytes! Should be 32')
|
||||
private = x25519.X25519PrivateKey.from_private_bytes(private_bytes)
|
||||
public = private.public_key()
|
||||
return cls(private=private, public=public, public_bytes=public.public_bytes())
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, public_bytes):
|
||||
if len(public_bytes) != 32:
|
||||
raise NoiseValueError('Invalid length of public_bytes! Should be 32')
|
||||
public = x25519.X25519PublicKey.from_public_bytes(public_bytes)
|
||||
return cls(public=public, public_bytes=public.public_bytes())
|
||||
|
||||
|
||||
class KeyPair448(_KeyPair):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KeyPair448, self).__init__(*args, **kwargs)
|
||||
warnings.warn('This implementation of ed448 is likely to be very insecure! USE ONLY FOR TESTING!')
|
||||
|
||||
@classmethod
|
||||
def from_private_bytes(cls, private_bytes):
|
||||
if len(private_bytes) != 56:
|
||||
raise NoiseValueError('Invalid length of private_bytes! Should be 56')
|
||||
private = private_bytes
|
||||
public = X448.mul_5(private)
|
||||
return cls(private=private, public=public, public_bytes=public)
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, public_bytes):
|
||||
if len(public_bytes) != 56:
|
||||
raise NoiseValueError('Invalid length of private_bytes! Should be 56')
|
||||
return cls(public=public_bytes, public_bytes=public_bytes)
|
||||
|
||||
@classmethod
|
||||
def new(cls):
|
||||
private = os.urandom(56)
|
||||
public = X448.mul_5(private)
|
||||
return cls(private=private, public=public, public_bytes=public)
|
||||
|
||||
|
||||
dh_map = {
|
||||
'25519': DH('ed25519'),
|
||||
'448': DH('ed448')
|
||||
}
|
||||
|
||||
cipher_map = {
|
||||
'AESGCM': partial(Cipher, 'AESGCM'),
|
||||
'ChaChaPoly': partial(Cipher, 'ChaCha20')
|
||||
}
|
||||
|
||||
hash_map = {
|
||||
'BLAKE2s': Hash('BLAKE2s'),
|
||||
'BLAKE2b': Hash('BLAKE2b'),
|
||||
'SHA256': Hash('SHA256'),
|
||||
'SHA512': Hash('SHA512')
|
||||
}
|
||||
|
||||
keypair_map = {
|
||||
'25519': KeyPair25519,
|
||||
'448': KeyPair448
|
||||
}
|
||||
|
||||
|
||||
def hmac_hash(key, data, algorithm):
|
||||
# Applies HMAC using the HASH() function.
|
||||
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):
|
||||
# Sets temp_key = HMAC-HASH(chaining_key, input_key_material).
|
||||
temp_key = hmac_hash_fn(chaining_key, input_key_material)
|
||||
|
||||
# Sets output1 = HMAC-HASH(temp_key, byte(0x01)).
|
||||
output1 = hmac_hash_fn(temp_key, b'\x01')
|
||||
|
||||
# Sets output2 = HMAC-HASH(temp_key, output1 || byte(0x02)).
|
||||
output2 = hmac_hash_fn(temp_key, output1 + b'\x02')
|
||||
|
||||
# If num_outputs == 2 then returns the pair (output1, output2).
|
||||
if num_outputs == 2:
|
||||
return output1, output2
|
||||
|
||||
# Sets output3 = HMAC-HASH(temp_key, output2 || byte(0x03)).
|
||||
output3 = hmac_hash_fn(temp_key, output2 + b'\x03')
|
||||
|
||||
# Returns the triple (output1, output2, output3).
|
||||
return output1, output2, output3
|
||||
0
noise/functions/__init__.py
Normal file
0
noise/functions/__init__.py
Normal file
27
noise/functions/cipher.py
Normal file
27
noise/functions/cipher.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import abc
|
||||
|
||||
from noise.constants import MAX_NONCE
|
||||
|
||||
|
||||
class Cipher(metaclass=abc.ABCMeta):
|
||||
def __init__(self):
|
||||
self.cipher = None
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def klass(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def encrypt(self, k, n, ad, plaintext):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def decrypt(self, k, n, ad, ciphertext):
|
||||
raise NotImplementedError
|
||||
|
||||
def rekey(self, k):
|
||||
return self.encrypt(k, MAX_NONCE, b'', b'\x00' * 32)[:32]
|
||||
|
||||
def initialize(self, key):
|
||||
self.cipher = self.klass(key)
|
||||
21
noise/functions/dh.py
Normal file
21
noise/functions/dh.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import abc
|
||||
|
||||
|
||||
class DH(metaclass=abc.ABCMeta):
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def klass(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def dhlen(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_keypair(self) -> 'KeyPair':
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def dh(self, private_key, public_key) -> bytes:
|
||||
raise NotImplementedError
|
||||
43
noise/functions/hash.py
Normal file
43
noise/functions/hash.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import abc
|
||||
|
||||
|
||||
class Hash(metaclass=abc.ABCMeta):
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def fn(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def hashlen(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def blocklen(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def hash(self, data):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def hkdf(chaining_key, input_key_material, num_outputs, hmac_hash_fn):
|
||||
# Sets temp_key = HMAC-HASH(chaining_key, input_key_material).
|
||||
temp_key = hmac_hash_fn(chaining_key, input_key_material)
|
||||
|
||||
# Sets output1 = HMAC-HASH(temp_key, byte(0x01)).
|
||||
output1 = hmac_hash_fn(temp_key, b'\x01')
|
||||
|
||||
# Sets output2 = HMAC-HASH(temp_key, output1 || byte(0x02)).
|
||||
output2 = hmac_hash_fn(temp_key, output1 + b'\x02')
|
||||
|
||||
# If num_outputs == 2 then returns the pair (output1, output2).
|
||||
if num_outputs == 2:
|
||||
return output1, output2
|
||||
|
||||
# Sets output3 = HMAC-HASH(temp_key, output2 || byte(0x03)).
|
||||
output3 = hmac_hash_fn(temp_key, output2 + b'\x03')
|
||||
|
||||
# Returns the triple (output1, output2, output3).
|
||||
return output1, output2, output3
|
||||
18
noise/functions/keypair.py
Normal file
18
noise/functions/keypair.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import abc
|
||||
|
||||
|
||||
class KeyPair(metaclass=abc.ABCMeta):
|
||||
def __init__(self, private=None, public=None, public_bytes=None):
|
||||
self.private = private
|
||||
self.public = public
|
||||
self.public_bytes = public_bytes
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_private_bytes(cls, private_bytes):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_public_bytes(cls, public_bytes):
|
||||
raise NotImplementedError
|
||||
78
noise/functions/patterns.py
Normal file
78
noise/functions/patterns.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from typing import List
|
||||
|
||||
from noise.constants import TOKEN_PSK
|
||||
|
||||
|
||||
class Pattern(object):
|
||||
"""
|
||||
TODO document
|
||||
"""
|
||||
def __init__(self):
|
||||
# As per specification, if both parties have pre-messages, the initiator is listed first. To reduce complexity,
|
||||
# pre_messages shall be a list of two lists:
|
||||
# the first for the initiator's pre-messages, the second for the responder
|
||||
self.pre_messages = [
|
||||
[],
|
||||
[]
|
||||
]
|
||||
|
||||
# List of lists of valid tokens, alternating between tokens for initiator and responder
|
||||
self.tokens = []
|
||||
|
||||
self.name = ''
|
||||
self.one_way = False
|
||||
self.psk_count = 0
|
||||
|
||||
def has_pre_messages(self):
|
||||
return any(map(lambda x: len(x) > 0, self.pre_messages))
|
||||
|
||||
def get_initiator_pre_messages(self) -> list:
|
||||
return self.pre_messages[0].copy()
|
||||
|
||||
def get_responder_pre_messages(self) -> list:
|
||||
return self.pre_messages[1].copy()
|
||||
|
||||
def apply_pattern_modifiers(self, modifiers: List[str]) -> None:
|
||||
# Applies given pattern modifiers to self.tokens of the Pattern instance.
|
||||
for modifier in modifiers:
|
||||
if modifier.startswith('psk'):
|
||||
try:
|
||||
index = int(modifier.replace('psk', '', 1))
|
||||
except ValueError:
|
||||
raise ValueError('Improper psk modifier {}'.format(modifier))
|
||||
|
||||
if index // 2 > len(self.tokens):
|
||||
raise ValueError('Modifier {} cannot be applied - pattern has not enough messages'.format(modifier))
|
||||
|
||||
# Add TOKEN_PSK in the correct place in the correct message
|
||||
if index == 0: # if 0, insert at the beginning of first message
|
||||
self.tokens[0].insert(0, TOKEN_PSK)
|
||||
else: # if bigger than zero, append at the end of first, second etc.
|
||||
self.tokens[index - 1].append(TOKEN_PSK)
|
||||
self.psk_count += 1
|
||||
|
||||
elif modifier == 'fallback':
|
||||
raise NotImplementedError # TODO implement
|
||||
|
||||
else:
|
||||
raise ValueError('Unknown pattern modifier {}'.format(modifier))
|
||||
|
||||
def get_required_keypairs(self, initiator: bool) -> list:
|
||||
required = []
|
||||
if initiator:
|
||||
if self.name[0] in ('K', 'X', 'I'):
|
||||
required.append('s')
|
||||
if self.one_way or self.name[1] == 'K':
|
||||
required.append('rs')
|
||||
else:
|
||||
if self.name[0] == 'K':
|
||||
required.append('rs')
|
||||
if self.one_way or self.name[1] in ['K', 'X']:
|
||||
required.append('s')
|
||||
return required
|
||||
|
||||
|
||||
class OneWayPattern(Pattern):
|
||||
def __init__(self):
|
||||
super(OneWayPattern, self).__init__()
|
||||
self.one_way = True
|
||||
@@ -5,48 +5,37 @@ from typing import Tuple
|
||||
from noise.exceptions import NoiseProtocolNameError, NoisePSKError, NoiseValidationError
|
||||
from noise.state import HandshakeState
|
||||
from .constants import MAX_PROTOCOL_NAME_LEN, Empty
|
||||
from .functions import dh_map, cipher_map, hash_map, keypair_map, hmac_hash, hkdf
|
||||
from .patterns import patterns_map
|
||||
|
||||
|
||||
class NoiseProtocol(object):
|
||||
"""
|
||||
TODO: Document
|
||||
"""
|
||||
methods = {
|
||||
'pattern': patterns_map,
|
||||
'dh': dh_map,
|
||||
'cipher': cipher_map,
|
||||
'hash': hash_map,
|
||||
'keypair': keypair_map
|
||||
}
|
||||
|
||||
def __init__(self, protocol_name: bytes):
|
||||
if not isinstance(protocol_name, bytes):
|
||||
raise NoiseProtocolNameError('Protocol name has to be of type "bytes" not {}'.format(type(protocol_name)))
|
||||
if len(protocol_name) > MAX_PROTOCOL_NAME_LEN:
|
||||
raise NoiseProtocolNameError('Protocol name too long, has to be at most '
|
||||
'{} chars long'.format(MAX_PROTOCOL_NAME_LEN))
|
||||
|
||||
def __init__(self, protocol_name: bytes, backend: 'NoiseBackend'):
|
||||
self.name = protocol_name
|
||||
mappings, pattern_modifiers = self._parse_protocol_name()
|
||||
self.backend = backend
|
||||
unpacked_name = UnpackedName.from_protocol_name(self.name)
|
||||
mappings = self.backend.map_protocol_name_to_crypto(unpacked_name)
|
||||
|
||||
# A valid Pattern instance (see Section 7 of specification (rev 32))
|
||||
self.pattern = mappings['pattern']()
|
||||
self.pattern_modifiers = pattern_modifiers
|
||||
self.pattern_modifiers = unpacked_name.pattern_modifiers
|
||||
if self.pattern_modifiers:
|
||||
self.pattern.apply_pattern_modifiers(pattern_modifiers)
|
||||
self.pattern.apply_pattern_modifiers(self.pattern_modifiers)
|
||||
|
||||
# Handle PSK handshake options
|
||||
self.psks = None
|
||||
self.is_psk_handshake = any([modifier.startswith('psk') for modifier in self.pattern_modifiers])
|
||||
|
||||
self.dh_fn = mappings['dh']
|
||||
self.cipher_fn = mappings['cipher']
|
||||
self.hash_fn = mappings['hash']
|
||||
self.keypair_fn = mappings['keypair']
|
||||
self.hmac = partial(hmac_hash, algorithm=self.hash_fn.fn)
|
||||
self.hkdf = partial(hkdf, hmac_hash_fn=self.hmac)
|
||||
# Preinitialized
|
||||
self.dh_fn = mappings['dh']()
|
||||
self.hash_fn = mappings['hash']()
|
||||
self.hmac = partial(backend.hmac, algorithm=self.hash_fn.fn)
|
||||
self.hkdf = partial(backend.hkdf, hmac_hash_fn=self.hmac)
|
||||
|
||||
# Initialized where needed
|
||||
self.cipher_class = mappings['cipher']
|
||||
self.keypair_class = mappings['keypair']
|
||||
|
||||
self.prologue = None
|
||||
self.initiator = None
|
||||
@@ -60,42 +49,6 @@ class NoiseProtocol(object):
|
||||
|
||||
self.keypairs = {'s': None, 'e': None, 'rs': None, 're': None}
|
||||
|
||||
def _parse_protocol_name(self) -> Tuple[dict, list]:
|
||||
unpacked = self.name.decode().split('_')
|
||||
if unpacked[0] != 'Noise':
|
||||
raise NoiseProtocolNameError('Noise Protocol name shall begin with Noise! Provided: {}'.format(self.name))
|
||||
|
||||
# Extract pattern name and pattern modifiers
|
||||
pattern = ''
|
||||
modifiers_str = None
|
||||
for i, char in enumerate(unpacked[1]):
|
||||
if char.isupper():
|
||||
pattern += char
|
||||
else:
|
||||
# End of pattern, now look for modifiers
|
||||
modifiers_str = unpacked[1][i:] # Will be empty string if it exceeds string size
|
||||
break
|
||||
modifiers = modifiers_str.split('+') if modifiers_str else []
|
||||
|
||||
data = {'pattern': 'Pattern' + pattern,
|
||||
'dh': unpacked[2],
|
||||
'cipher': unpacked[3],
|
||||
'hash': unpacked[4],
|
||||
'keypair': unpacked[2],
|
||||
'pattern_modifiers': modifiers}
|
||||
|
||||
mapped_data = {}
|
||||
|
||||
# Validate if we know everything that Noise Protocol is supposed to use and map appropriate functions
|
||||
for key, map_dict in self.methods.items():
|
||||
func = map_dict.get(data[key])
|
||||
if not func:
|
||||
raise NoiseProtocolNameError('Unknown {} in Noise Protocol name, given {}, known {}'.format(
|
||||
key, data[key], " ".join(map_dict)))
|
||||
mapped_data[key] = func
|
||||
|
||||
return mapped_data, modifiers
|
||||
|
||||
def handshake_done(self):
|
||||
if self.pattern.one_way:
|
||||
if self.initiator:
|
||||
@@ -110,7 +63,7 @@ class NoiseProtocol(object):
|
||||
del self.initiator
|
||||
del self.dh_fn
|
||||
del self.hash_fn
|
||||
del self.keypair_fn
|
||||
del self.keypair_class
|
||||
|
||||
def validate(self):
|
||||
if self.is_psk_handshake:
|
||||
@@ -141,3 +94,46 @@ class NoiseProtocol(object):
|
||||
kwargs[keypair] = value
|
||||
self.handshake_state = HandshakeState.initialize(self, **kwargs)
|
||||
self.symmetric_state = self.handshake_state.symmetric_state
|
||||
|
||||
|
||||
class UnpackedName:
|
||||
def __init__(self, pattern, dh, cipher, hash, keypair, pattern_modifiers):
|
||||
self.pattern = pattern
|
||||
self.dh = dh
|
||||
self.cipher = cipher
|
||||
self.hash = hash
|
||||
self.keypair = keypair
|
||||
self.pattern_modifiers = pattern_modifiers
|
||||
|
||||
@classmethod
|
||||
def from_protocol_name(cls, name):
|
||||
if not isinstance(name, bytes):
|
||||
raise NoiseProtocolNameError('Protocol name has to be of type "bytes" not {}'.format(type(name)))
|
||||
if len(name) > MAX_PROTOCOL_NAME_LEN:
|
||||
raise NoiseProtocolNameError('Protocol name too long, has to be at most '
|
||||
'{} chars long'.format(MAX_PROTOCOL_NAME_LEN))
|
||||
|
||||
unpacked = name.decode().split('_')
|
||||
if unpacked[0] != 'Noise':
|
||||
raise NoiseProtocolNameError('Noise Protocol name shall begin with Noise! Provided: {}'.format(name))
|
||||
|
||||
# Extract pattern name and pattern modifiers
|
||||
pattern = ''
|
||||
modifiers_str = None
|
||||
for i, char in enumerate(unpacked[1]):
|
||||
if char.isupper():
|
||||
pattern += char
|
||||
else:
|
||||
# End of pattern, now look for modifiers
|
||||
modifiers_str = unpacked[1][i:] # Will be empty string if it exceeds string size
|
||||
break
|
||||
modifiers = modifiers_str.split('+') if modifiers_str else []
|
||||
|
||||
return cls(
|
||||
pattern=pattern,
|
||||
dh=unpacked[2],
|
||||
cipher=unpacked[3],
|
||||
hash=unpacked[4],
|
||||
keypair=unpacked[2],
|
||||
pattern_modifiers=modifiers
|
||||
)
|
||||
|
||||
@@ -1,85 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from .constants import TOKEN_E, TOKEN_S, TOKEN_EE, TOKEN_ES, TOKEN_SE, TOKEN_SS, TOKEN_PSK
|
||||
|
||||
|
||||
class Pattern(object):
|
||||
"""
|
||||
TODO document
|
||||
"""
|
||||
def __init__(self):
|
||||
# As per specification, if both parties have pre-messages, the initiator is listed first. To reduce complexity,
|
||||
# pre_messages shall be a list of two lists:
|
||||
# the first for the initiator's pre-messages, the second for the responder
|
||||
self.pre_messages = [
|
||||
[],
|
||||
[]
|
||||
]
|
||||
|
||||
# List of lists of valid tokens, alternating between tokens for initiator and responder
|
||||
self.tokens = []
|
||||
|
||||
self.name = ''
|
||||
self.one_way = False
|
||||
self.psk_count = 0
|
||||
|
||||
def has_pre_messages(self):
|
||||
return any(map(lambda x: len(x) > 0, self.pre_messages))
|
||||
|
||||
def get_initiator_pre_messages(self) -> list:
|
||||
return self.pre_messages[0].copy()
|
||||
|
||||
def get_responder_pre_messages(self) -> list:
|
||||
return self.pre_messages[1].copy()
|
||||
|
||||
def apply_pattern_modifiers(self, modifiers: List[str]) -> None:
|
||||
# Applies given pattern modifiers to self.tokens of the Pattern instance.
|
||||
for modifier in modifiers:
|
||||
if modifier.startswith('psk'):
|
||||
try:
|
||||
index = int(modifier.replace('psk', '', 1))
|
||||
except ValueError:
|
||||
raise ValueError('Improper psk modifier {}'.format(modifier))
|
||||
|
||||
if index // 2 > len(self.tokens):
|
||||
raise ValueError('Modifier {} cannot be applied - pattern has not enough messages'.format(modifier))
|
||||
|
||||
# Add TOKEN_PSK in the correct place in the correct message
|
||||
if index == 0: # if 0, insert at the beginning of first message
|
||||
self.tokens[0].insert(0, TOKEN_PSK)
|
||||
else: # if bigger than zero, append at the end of first, second etc.
|
||||
self.tokens[index - 1].append(TOKEN_PSK)
|
||||
self.psk_count += 1
|
||||
|
||||
elif modifier == 'fallback':
|
||||
raise NotImplementedError # TODO implement
|
||||
|
||||
else:
|
||||
raise ValueError('Unknown pattern modifier {}'.format(modifier))
|
||||
|
||||
def get_required_keypairs(self, initiator: bool) -> list:
|
||||
required = []
|
||||
if initiator:
|
||||
if self.name[0] in ('K', 'X', 'I'):
|
||||
required.append('s')
|
||||
if self.one_way or self.name[1] == 'K':
|
||||
required.append('rs')
|
||||
else:
|
||||
if self.name[0] == 'K':
|
||||
required.append('rs')
|
||||
if self.one_way or self.name[1] in ['K', 'X']:
|
||||
required.append('s')
|
||||
return required
|
||||
from noise.constants import TOKEN_S, TOKEN_E, TOKEN_ES, TOKEN_SS, TOKEN_EE, TOKEN_SE
|
||||
from noise.functions.patterns import OneWayPattern, Pattern
|
||||
|
||||
|
||||
# One-way patterns
|
||||
|
||||
class OneWayPattern(Pattern):
|
||||
def __init__(self):
|
||||
super(OneWayPattern, self).__init__()
|
||||
self.one_way = True
|
||||
|
||||
|
||||
class PatternN(OneWayPattern):
|
||||
def __init__(self):
|
||||
super(PatternN, self).__init__()
|
||||
@@ -281,22 +205,3 @@ class PatternIX(Pattern):
|
||||
[TOKEN_E, TOKEN_S],
|
||||
[TOKEN_E, TOKEN_EE, TOKEN_SE, TOKEN_S, TOKEN_ES]
|
||||
]
|
||||
|
||||
|
||||
patterns_map = {
|
||||
'PatternN': PatternN,
|
||||
'PatternK': PatternK,
|
||||
'PatternX': PatternX,
|
||||
'PatternNN': PatternNN,
|
||||
'PatternKN': PatternKN,
|
||||
'PatternNK': PatternNK,
|
||||
'PatternKK': PatternKK,
|
||||
'PatternNX': PatternNX,
|
||||
'PatternKX': PatternKX,
|
||||
'PatternXN': PatternXN,
|
||||
'PatternIN': PatternIN,
|
||||
'PatternXK': PatternXK,
|
||||
'PatternIK': PatternIK,
|
||||
'PatternXX': PatternXX,
|
||||
'PatternIX': PatternIX,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class CipherState(object):
|
||||
def __init__(self, noise_protocol):
|
||||
self.k = Empty()
|
||||
self.n = None
|
||||
self.cipher = noise_protocol.cipher_fn()
|
||||
self.cipher = noise_protocol.cipher_class()
|
||||
|
||||
def initialize_key(self, key):
|
||||
"""
|
||||
@@ -363,7 +363,7 @@ class HandshakeState(object):
|
||||
for token in message_pattern:
|
||||
if token == TOKEN_E:
|
||||
# Sets re to the next DHLEN bytes from the message. Calls MixHash(re.public_key).
|
||||
self.re = self.noise_protocol.keypair_fn.from_public_bytes(bytes(message[:dhlen]))
|
||||
self.re = self.noise_protocol.keypair_class.from_public_bytes(bytes(message[:dhlen]))
|
||||
message = message[dhlen:]
|
||||
self.symmetric_state.mix_hash(self.re.public_bytes)
|
||||
if self.noise_protocol.is_psk_handshake:
|
||||
@@ -378,7 +378,9 @@ class HandshakeState(object):
|
||||
else:
|
||||
temp = bytes(message[:dhlen])
|
||||
message = message[dhlen:]
|
||||
self.rs = self.noise_protocol.keypair_fn.from_public_bytes(self.symmetric_state.decrypt_and_hash(temp))
|
||||
self.rs = self.noise_protocol.keypair_class.from_public_bytes(
|
||||
self.symmetric_state.decrypt_and_hash(temp)
|
||||
)
|
||||
|
||||
elif token == TOKEN_EE:
|
||||
# Calls MixKey(DH(e, re)).
|
||||
|
||||
@@ -1 +1 @@
|
||||
cryptography==2.1.4
|
||||
cryptography>=2.5
|
||||
|
||||
4
setup.py
4
setup.py
@@ -13,7 +13,7 @@ except (IOError, ImportError):
|
||||
|
||||
setup(
|
||||
name='noiseprotocol',
|
||||
version='0.2.2',
|
||||
version='0.3.0',
|
||||
description='Implementation of Noise Protocol Framework',
|
||||
long_description=long_description,
|
||||
url='https://github.com/plizonczyk/noiseprotocol',
|
||||
@@ -35,6 +35,6 @@ setup(
|
||||
],
|
||||
keywords='cryptography noiseprotocol noise security',
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']),
|
||||
install_requires=['cryptography>=2.1.4'],
|
||||
install_requires=['cryptography>=2.5'],
|
||||
python_requires='~=3.5', # we like 3.5, 3.6, and beyond, but not 4.0
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from noise.backends.default import noise_backend
|
||||
from noise.noise_protocol import NoiseProtocol
|
||||
from noise.state import CipherState, SymmetricState
|
||||
|
||||
@@ -8,12 +9,14 @@ class TestRevision33Compatibility(object):
|
||||
fn = None
|
||||
|
||||
noise_name = b"Noise_NN_25519_AESGCM_SHA3/256"
|
||||
|
||||
modified_backend = noise_backend
|
||||
modified_backend.hashes['SHA3/256'] = FakeSHA3_256 # Add callable to hash functions mapping
|
||||
modified_class = NoiseProtocol
|
||||
modified_class.methods['hash']['SHA3/256'] = FakeSHA3_256 # Add callable to hash functions mapping
|
||||
modified_class(noise_name)
|
||||
modified_class(noise_name, modified_backend)
|
||||
|
||||
def test_cipher_state_set_nonce(self):
|
||||
noise_protocol = NoiseProtocol(b"Noise_NN_25519_AESGCM_SHA256")
|
||||
noise_protocol = NoiseProtocol(b"Noise_NN_25519_AESGCM_SHA256", backend=noise_backend)
|
||||
cipher_state = CipherState(noise_protocol)
|
||||
cipher_state.initialize_key(b'\x00'*32)
|
||||
assert cipher_state.n == 0
|
||||
|
||||
Reference in New Issue
Block a user