Skip to main content

A professional, secure, and easy-to-use implementation of the Noise Protocol Framework in Python

Project description

NoiseFramework

Official Website


PyPI version Python Version License GitHub Issues GitHub Stars Code Style

A professional, secure, and easy-to-use implementation of the Noise Protocol Framework in Python.

NoiseFramework provides cryptographically sound, specification-compliant implementations of Noise handshake patterns for building secure communication channels. It is designed to be both simple to integrate into applications and robust enough for production use.


๐Ÿ“‹ Table of Contents


โœจ Features

  • ๐Ÿ“œ Spec-Compliant: Implements the Noise Protocol Framework specification faithfully
  • ๐Ÿ”’ Secure by Default: Uses well-vetted cryptographic primitives from trusted libraries
  • ๐Ÿ Pythonic API: Simple, type-hinted interfaces that are easy to use and hard to misuse
  • ๐Ÿ› ๏ธ CLI Tool: Command-line interface for encryption, decryption, and handshake operations
  • โœ… Well-Tested: Comprehensive test suite with 311 tests achieving 100% pass rate
  • ๐Ÿ“ฆ Zero Config: Works out-of-the-box with sensible defaults
  • ๐Ÿ”ง Flexible: Supports multiple DH functions, cipher suites, and hash functions
  • ๐Ÿ›ก๏ธ PSK Support: Pre-Shared Key patterns for quantum-resistant authentication (psk0-psk4)
  • ๐Ÿ“– Documented: Extensive documentation with examples and best practices
  • ๐Ÿ” Helpful Errors: Custom exceptions with actionable error messages guide you to fix issues quickly
  • ๐Ÿ“ Built-in Logging: Comprehensive logging support for debugging and monitoring
  • ๐Ÿ“ฆ Message Framing: Automatic length-prefixed framing for stream-based transports
  • โšก Async/Await: Full async support for modern Python asyncio applications
  • ๐Ÿ”— High-Level Connection API: NoiseConnection and AsyncNoiseConnection for easy-to-use connection management

๐Ÿ“ฆ Installation

From PyPI (Recommended)

pip install noiseframework

From Source

git clone https://github.com/juliuspleunes4/noiseframework.git
cd noiseframework
pip install -e .

Requirements

  • Python 3.8 or higher
  • Dependencies are automatically installed via pip

๐Ÿš€ Quick Start

Python API

from noiseframework import NoiseHandshake, NoiseTransport

# === INITIATOR SIDE ===
initiator = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
initiator.set_as_initiator()
initiator.generate_static_keypair()
initiator.initialize()

# === RESPONDER SIDE ===
responder = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
responder.set_as_responder()
responder.generate_static_keypair()
responder.initialize()

# === HANDSHAKE ===
msg1 = initiator.write_message(b"")
responder.read_message(msg1)

msg2 = responder.write_message(b"")
initiator.read_message(msg2)

msg3 = initiator.write_message(b"")
responder.read_message(msg3)

# === TRANSPORT ENCRYPTION ===
init_send, init_recv = initiator.to_transport()
resp_send, resp_recv = responder.to_transport()

init_transport = NoiseTransport(init_send, init_recv)
resp_transport = NoiseTransport(resp_send, resp_recv)

# Send encrypted messages
ciphertext = init_transport.send(b"Hello, secure world!")
plaintext = resp_transport.receive(ciphertext)
print(plaintext)  # b"Hello, secure world!"

Command-Line Interface

# Generate a keypair
noiseframework generate-keypair --dh 25519 -o mykey
# Creates: mykey_private.key, mykey_public.key

# Validate a pattern string
noiseframework validate-pattern "Noise_XX_25519_ChaChaPoly_SHA256"

# Show supported primitives
noiseframework info

# Use shorter aliases
noiseframework genkey --dh 25519 -o mykey
noiseframework validate "Noise_XX_25519_ChaChaPoly_SHA256"

๐Ÿ“– Python API Documentation

Basic Handshake (Noise_XX)

The XX pattern provides mutual authentication with no prior knowledge required. Both parties exchange static keys during the handshake.

from noiseframework import NoiseHandshake, NoiseTransport

# === INITIATOR SIDE ===
initiator = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
initiator.set_as_initiator()
initiator.generate_static_keypair()  # Generate static key
initiator.initialize()

# Send first message (-> e)
msg1 = initiator.write_message(b"")

# === RESPONDER SIDE ===
responder = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
responder.set_as_responder()
responder.generate_static_keypair()  # Generate static key
responder.initialize()

# Process first message and send response (-> e, ee, s, es)
responder.read_message(msg1)
msg2 = responder.write_message(b"")

# === INITIATOR SIDE (continued) ===
# Process second message and send final (-> s, se)
initiator.read_message(msg2)
msg3 = initiator.write_message(b"")

# === RESPONDER SIDE (continued) ===
# Process final message
responder.read_message(msg3)

# === BOTH SIDES NOW HAVE SECURE CHANNEL ===
# Get transport cipher pairs
init_send, init_recv = initiator.to_transport()
resp_send, resp_recv = responder.to_transport()

# Create transport wrappers
init_transport = NoiseTransport(init_send, init_recv)
resp_transport = NoiseTransport(resp_send, resp_recv)

# Send encrypted data (initiator -> responder)
ciphertext = init_transport.send(b"Secret payload")
plaintext = resp_transport.receive(ciphertext)
assert plaintext == b"Secret payload"

# Send encrypted data (responder -> initiator)
ciphertext = resp_transport.send(b"Response data")
plaintext = init_transport.receive(ciphertext)
assert plaintext == b"Response data"

Anonymous Pattern (Noise_NN)

The NN pattern provides encryption without authentication. No static keys are required.

from noiseframework import NoiseHandshake, NoiseTransport

# === INITIATOR SIDE ===
initiator = NoiseHandshake("Noise_NN_25519_ChaChaPoly_SHA256")
initiator.set_as_initiator()
initiator.initialize()

# Send first message (-> e)
msg1 = initiator.write_message(b"")

# === RESPONDER SIDE ===
responder = NoiseHandshake("Noise_NN_25519_ChaChaPoly_SHA256")
responder.set_as_responder()
responder.initialize()

# Process first message and send response (-> e, ee)
responder.read_message(msg1)
msg2 = responder.write_message(b"")

# === INITIATOR SIDE (continued) ===
# Process second message - handshake complete
initiator.read_message(msg2)

# === CREATE TRANSPORT ===
init_send, init_recv = initiator.to_transport()
resp_send, resp_recv = responder.to_transport()

init_transport = NoiseTransport(init_send, init_recv)
resp_transport = NoiseTransport(resp_send, resp_recv)

# Now both sides can communicate securely (but without authentication)
ciphertext = init_transport.send(b"Anonymous message")
plaintext = resp_transport.receive(ciphertext)

Pre-Shared Key Pattern (Noise_IK)

The IK pattern allows the initiator to know the responder's static public key in advance. The initiator's identity is hidden.

from noiseframework import NoiseHandshake, NoiseTransport

# === SETUP: Generate responder's static keypair ===
responder_setup = NoiseHandshake("Noise_IK_25519_ChaChaPoly_SHA256")
responder_setup.set_as_responder()
responder_setup.generate_static_keypair()
responder_private = responder_setup.static_private
responder_public = responder_setup.static_public

# === INITIATOR SIDE ===
initiator = NoiseHandshake("Noise_IK_25519_ChaChaPoly_SHA256")
initiator.set_as_initiator()
initiator.generate_static_keypair()  # Generate own static key
initiator.set_remote_static_public_key(responder_public)  # Know responder's key
initiator.initialize()

# Send first message (-> e, es, s, ss)
msg1 = initiator.write_message(b"")

# === RESPONDER SIDE ===
responder = NoiseHandshake("Noise_IK_25519_ChaChaPoly_SHA256")
responder.set_as_responder()
responder.set_static_keypair(responder_private, responder_public)  # Use existing keypair
responder.initialize()

# Process first message and send response (-> e, ee, se)
responder.read_message(msg1)
msg2 = responder.write_message(b"")

# === INITIATOR SIDE (continued) ===
# Process second message - handshake complete
initiator.read_message(msg2)

# === CREATE TRANSPORT ===
init_send, init_recv = initiator.to_transport()
resp_send, resp_recv = responder.to_transport()

init_transport = NoiseTransport(init_send, init_recv)
resp_transport = NoiseTransport(resp_send, resp_recv)

# Secure authenticated communication
ciphertext = init_transport.send(b"Authenticated message")
plaintext = resp_transport.receive(ciphertext)

Transport Layer Encryption

After handshake completion, use the transport layer for ongoing encrypted communication:

from noiseframework import NoiseTransport

# After successful handshake, get cipher states
send_cipher, recv_cipher = handshake.to_transport()

# Create transport wrapper
transport = NoiseTransport(send_cipher, recv_cipher)

# Encrypt and send data
ciphertext = transport.send(b"Sensitive data")

# Decrypt received data
plaintext = transport.receive(ciphertext)

# Send with associated data (authenticated but not encrypted)
ciphertext = transport.send(b"payload", ad=b"metadata")
plaintext = transport.receive(ciphertext, ad=b"metadata")

# Track nonces
print(f"Messages sent: {transport.get_send_nonce()}")
print(f"Messages received: {transport.get_receive_nonce()}")

# Transport automatically handles:
# - Nonce increment
# - Authentication tags
# - AEAD encryption/decryption

Error Handling

NoiseFramework provides helpful custom exceptions with actionable error messages:

from noiseframework import NoiseHandshake, NoiseTransport
from noiseframework.exceptions import (
    NoiseError,                  # Base class - catches all framework errors
    UnsupportedPatternError,     # Invalid pattern
    RoleNotSetError,             # Role not set
    AuthenticationError,         # Decryption/authentication failure
)

# Catch specific exceptions
try:
    hs = NoiseHandshake("Invalid_Pattern")
except UnsupportedPatternError as e:
    print(f"Pattern error: {e}")
    # Output: Invalid pattern string format: 'Invalid_Pattern'.
    #         Expected format: Noise_PATTERN_DH_CIPHER_HASH

# Handle state errors
try:
    hs = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
    hs.write_message()  # Error: role not set
except RoleNotSetError as e:
    print(f"State error: {e}")
    # Output: Cannot write handshake message: role not set.
    #         Call set_as_initiator() or set_as_responder() first.

# Handle authentication failures
try:
    ciphertext_tampered = ciphertext[:-1] + b"\x00"
    transport.receive(ciphertext_tampered)
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
    # DO NOT process the message - discard it

# Catch all framework errors
try:
    # ... noise operations ...
except NoiseError as e:
    print(f"Framework error: {type(e).__name__}: {e}")

Available exceptions: NoiseError (base), HandshakeError, RoleNotSetError, RoleAlreadySetError, WrongTurnError, HandshakeCompleteError, MissingKeyError, PatternError, UnsupportedPatternError, UnsupportedPrimitiveError, StateError, NoKeySetError, NonceOverflowError, InvalidKeySizeError, TransportError, AuthenticationError, CryptoError, ValidationError, FramingError.

See examples/error_handling_example.py for comprehensive error handling examples.


Logging

NoiseFramework includes comprehensive logging support for debugging and monitoring. All major operations are logged at appropriate levels.

Basic Logging Setup

import logging
from noiseframework import NoiseHandshake, NoiseTransport

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)-8s] %(name)s: %(message)s"
)

# Use NoiseFramework normally - logging happens automatically
handshake = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
handshake.set_as_initiator()  # Logs: "Role set as INITIATOR"
handshake.generate_static_keypair()  # Logs: "Generated static keypair"
handshake.initialize()  # Logs: "Handshake initialized"

msg = handshake.write_message(b"")  # Logs: "Sent handshake message 1"

Log Levels

  • DEBUG: Detailed operations (message sizes, nonces, token processing, key operations)
  • INFO: Major events (role changes, handshake completion, message send/receive)
  • WARNING: Potential issues (nonce approaching limit)
  • ERROR: Failures (validation errors, authentication failures)

Custom Logger

# Create custom logger with specific configuration
custom_logger = logging.getLogger("myapp.noise")
custom_logger.setLevel(logging.DEBUG)

# Add custom handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
custom_logger.addHandler(handler)

# Pass custom logger to NoiseHandshake
handshake = NoiseHandshake(
    "Noise_XX_25519_ChaChaPoly_SHA256",
    logger=custom_logger
)

# Pass custom logger to NoiseTransport
transport = NoiseTransport(send_cipher, recv_cipher, logger=custom_logger)

Filtering Logs by Module

# Only show INFO+ logs from handshake module
logging.getLogger("noiseframework.noise.handshake").setLevel(logging.INFO)

# Show DEBUG logs from transport module
logging.getLogger("noiseframework.transport.transport").setLevel(logging.DEBUG)

# Disable logs from state module
logging.getLogger("noiseframework.noise.state").setLevel(logging.WARNING)

Example Log Output

2025-11-24 15:30:01 [INFO    ] noiseframework.noise.handshake.NoiseHandshake: Role set as INITIATOR
2025-11-24 15:30:01 [DEBUG   ] noiseframework.noise.handshake.NoiseHandshake: Generating static keypair (Curve25519)
2025-11-24 15:30:01 [INFO    ] noiseframework.noise.handshake.NoiseHandshake: Generated static keypair
2025-11-24 15:30:01 [INFO    ] noiseframework.noise.handshake.NoiseHandshake: Handshake initialized
2025-11-24 15:30:01 [DEBUG   ] noiseframework.noise.handshake.NoiseHandshake: Writing handshake message 1 (payload=0 bytes)
2025-11-24 15:30:01 [INFO    ] noiseframework.noise.handshake.NoiseHandshake: Sent handshake message 1 (ciphertext=32 bytes)
2025-11-24 15:30:02 [INFO    ] noiseframework.noise.handshake.NoiseHandshake: Handshake complete - ready for transport mode
2025-11-24 15:30:02 [INFO    ] noiseframework.noise.handshake.NoiseHandshake: Created transport ciphers (initiator: send=c1, receive=c2)
2025-11-24 15:30:03 [INFO    ] noiseframework.transport.transport.NoiseTransport: Sent encrypted message (ciphertext=29 bytes)
2025-11-24 15:30:03 [WARNING ] noiseframework.transport.transport.NoiseTransport: Send cipher nonce high: 9223372036854775808 (approaching 2^64 limit - consider rekeying)

See examples/logging_example.py for more detailed examples.

Message Framing

When using Noise over stream-based transports (TCP, pipes, etc.), you need a way to preserve message boundaries. NoiseFramework provides length-prefixed framing utilities for this purpose.

Basic Usage

import socket
from noiseframework import NoiseHandshake, FramedWriter, FramedReader

# After handshake completes...
transport = handshake.to_transport()

# Wrap socket for framed communication
writer = FramedWriter(sock.makefile('wb'))
reader = FramedReader(sock.makefile('rb'))

# Send framed encrypted messages
ciphertext = transport.send(b"Hello, World!")
writer.write_message(ciphertext)

# Receive framed encrypted messages
framed_message = reader.read_message()
plaintext = transport.receive(framed_message)

Frame Format

NoiseFramework uses a simple 4-byte big-endian length prefix:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Length (4B)  โ”‚  Message Data      โ”‚
โ”‚ big-endian   โ”‚  (0 to 2^32-1 B)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Configuration

# Default maximum message size is 16 MB
reader = FramedReader(stream)  # max_message_size=16*1024*1024

# Set custom maximum
reader = FramedReader(stream, max_message_size=1024*1024)  # 1 MB limit

# Add logging
import logging
logger = logging.getLogger("myapp.framing")
reader = FramedReader(stream, logger=logger)
writer = FramedWriter(stream, logger=logger)

TCP Example

import socket
from noiseframework import NoiseHandshake, FramedWriter, FramedReader

# Server
def server(port):
    with socket.socket() as sock:
        sock.bind(('localhost', port))
        sock.listen(1)
        conn, _ = sock.accept()
        
        # Noise handshake (responder)
        hs = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
        hs.set_as_responder()
        hs.generate_static_keypair()
        hs.initialize()
        
        reader = FramedReader(conn.makefile('rb'))
        writer = FramedWriter(conn.makefile('wb'))
        
        # Handshake messages with framing
        msg1 = reader.read_message()
        msg2 = hs.read_message(msg1)
        msg2_out = hs.write_message(msg2)
        writer.write_message(msg2_out)
        
        msg3 = reader.read_message()
        hs.read_message(msg3)
        
        # Transport mode
        transport = hs.to_transport()
        encrypted = reader.read_message()
        plaintext = transport.receive(encrypted)
        print(f"Server received: {plaintext}")

# Client
def client(port):
    with socket.socket() as sock:
        sock.connect(('localhost', port))
        
        # Noise handshake (initiator)
        hs = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
        hs.set_as_initiator()
        hs.generate_static_keypair()
        hs.initialize()
        
        reader = FramedReader(sock.makefile('rb'))
        writer = FramedWriter(sock.makefile('wb'))
        
        # Handshake messages with framing
        msg1 = hs.write_message(b"")
        writer.write_message(msg1)
        
        msg2 = reader.read_message()
        msg3_payload = hs.read_message(msg2)
        msg3 = hs.write_message(msg3_payload)
        writer.write_message(msg3)
        
        # Transport mode
        transport = hs.to_transport()
        ciphertext = transport.send(b"Hello, Server!")
        writer.write_message(ciphertext)

Convenience Functions

For single-message operations:

from noiseframework import write_framed_message, read_framed_message

# Write single framed message
write_framed_message(stream, b"Hello")

# Read single framed message
message = read_framed_message(stream)

# With custom max size
message = read_framed_message(stream, max_message_size=1024*1024)

Error Handling

from noiseframework import FramingError

try:
    message = reader.read_message()
except FramingError as e:
    # Connection closed, oversized message, or invalid frame
    print(f"Framing error: {e}")
except IOError as e:
    # Underlying stream error
    print(f"IO error: {e}")

Key Points:

  • Automatic handling of partial reads
  • Protection against oversized messages (configurable limit)
  • Message counters for debugging (messages_sent, messages_received)
  • Thread-safe for concurrent read/write on different threads
  • Works with any byte stream (sockets, files, pipes, etc.)

See examples/framed_tcp_example.py for a complete working example.

Async/Await Support

NoiseFramework provides full asyncio support for modern async Python applications. All async classes wrap the synchronous implementation using run_in_executor, making them safe for use in async contexts without blocking the event loop.

Basic Async Usage

import asyncio
from noiseframework import AsyncNoiseHandshake, AsyncNoiseTransport

async def async_handshake():
    # Create async handshake
    handshake = AsyncNoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
    await handshake.set_as_initiator()
    await handshake.generate_static_keypair()
    await handshake.initialize()
    
    # Perform handshake (async)
    msg1 = await handshake.write_message(b"")
    # ... exchange messages over network ...
    
    # Convert to async transport
    transport = await handshake.to_transport()
    
    # Send/receive encrypted messages (async)
    ciphertext = await transport.send(b"Hello, async world!")
    plaintext = await transport.receive(ciphertext)

# Run the async function
asyncio.run(async_handshake())

Async TCP Server Example

import asyncio
from noiseframework import (
    AsyncNoiseHandshake,
    AsyncFramedReader,
    AsyncFramedWriter,
)

async def handle_client(reader, writer):
    """Handle incoming client connection."""
    # Create responder handshake
    handshake = AsyncNoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
    await handshake.set_as_responder()
    await handshake.generate_static_keypair()
    await handshake.initialize()
    
    # Wrap streams with framing
    framed_reader = AsyncFramedReader(reader)
    framed_writer = AsyncFramedWriter(writer)
    
    # Perform XX handshake
    msg1 = await framed_reader.read_message()
    await handshake.read_message(msg1)
    
    msg2 = await handshake.write_message(b"")
    await framed_writer.write_message(msg2)
    
    msg3 = await framed_reader.read_message()
    await handshake.read_message(msg3)
    
    # Switch to transport mode
    transport = await handshake.to_transport()
    
    # Receive and process encrypted messages
    while True:
        try:
            ciphertext = await framed_reader.read_message()
            plaintext = await transport.receive(ciphertext)
            print(f"Received: {plaintext.decode()}")
            
            # Send encrypted response
            response = await transport.send(b"Message received!")
            await framed_writer.write_message(response)
        except:
            break
    
    await framed_writer.close()

async def main():
    server = await asyncio.start_server(
        handle_client, '127.0.0.1', 9999
    )
    async with server:
        await server.serve_forever()

asyncio.run(main())

Async TCP Client Example

async def async_client():
    # Connect to server
    reader, writer = await asyncio.open_connection('127.0.0.1', 9999)
    
    # Create initiator handshake
    handshake = AsyncNoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
    await handshake.set_as_initiator()
    await handshake.generate_static_keypair()
    await handshake.initialize()
    
    # Wrap streams with framing
    framed_reader = AsyncFramedReader(reader)
    framed_writer = AsyncFramedWriter(writer)
    
    # Perform XX handshake (3 messages)
    msg1 = await handshake.write_message(b"")
    await framed_writer.write_message(msg1)
    
    msg2 = await framed_reader.read_message()
    await handshake.read_message(msg2)
    
    msg3 = await handshake.write_message(b"")
    await framed_writer.write_message(msg3)
    
    # Switch to transport mode
    transport = await handshake.to_transport()
    
    # Send encrypted messages
    for i in range(3):
        message = f"Message {i+1}".encode()
        ciphertext = await transport.send(message)
        await framed_writer.write_message(ciphertext)
        
        response_ct = await framed_reader.read_message()
        response = await transport.receive(response_ct)
        print(f"Server response: {response.decode()}")
    
    await framed_writer.close()

asyncio.run(async_client())

Async Framing

For asyncio streams, use AsyncFramedReader and AsyncFramedWriter:

from noiseframework import AsyncFramedReader, AsyncFramedWriter

async def async_framed_communication(reader, writer):
    framed_writer = AsyncFramedWriter(writer)
    framed_reader = AsyncFramedReader(reader)
    
    # Write framed message
    await framed_writer.write_message(b"Hello, async!")
    
    # Read framed message
    message = await framed_reader.read_message()
    
    # Close when done
    await framed_writer.close()

Async Convenience Functions

from noiseframework import (
    async_write_framed_message,
    async_read_framed_message,
)

# Write single message
await async_write_framed_message(writer, b"Hello")

# Read single message
message = await async_read_framed_message(reader)

Async API Classes

AsyncNoiseHandshake: Async wrapper for NoiseHandshake

  • await set_as_initiator() - Set role as initiator
  • await set_as_responder() - Set role as responder
  • await generate_static_keypair() - Generate static keys
  • await set_static_keypair(private, public) - Set existing keys
  • await set_remote_static_public_key(public) - Set remote's public key
  • await initialize() - Initialize handshake state
  • await write_message(payload) - Write handshake message
  • await read_message(message) - Read handshake message
  • await to_transport() - Get AsyncNoiseTransport after completion
  • await get_handshake_hash() - Get handshake hash
  • .is_complete - Check if handshake is complete (property)

AsyncNoiseTransport: Async wrapper for NoiseTransport

  • await send(plaintext, ad=b"") - Encrypt and send message
  • await receive(ciphertext, ad=b"") - Decrypt and receive message
  • .send_nonce - Current send nonce (property)
  • .receive_nonce - Current receive nonce (property)

AsyncFramedReader: Async framed message reader

  • await read_message() - Read length-prefixed message
  • await close() - Close reader
  • .messages_received - Message counter (property)

AsyncFramedWriter: Async framed message writer

  • await write_message(message) - Write length-prefixed message
  • await close() - Close writer and wait for completion
  • .messages_sent - Message counter (property)

Key Points:

  • All async operations use run_in_executor internally
  • No blocking calls in the async event loop
  • Compatible with asyncio.StreamReader and asyncio.StreamWriter
  • Same security guarantees as synchronous version
  • Logging support in all async classes

See examples/async_tcp_example.py for a complete working example with server and client.


Pre-Shared Key (PSK) Support

NoiseFramework supports Pre-Shared Key (PSK) patterns for quantum-resistant authentication. PSK patterns add an additional layer of security by mixing a shared secret into the handshake, providing protection against future quantum computer attacks on Diffie-Hellman key exchange.

What are PSK Patterns?

PSK patterns combine traditional Noise handshakes with pre-shared keys:

  • Quantum Resistance: PSKs are immune to quantum computer attacks (unlike DH)
  • Additional Authentication: Extra authentication layer beyond public keys
  • Pre-computation Resistance: Attackers can't pre-compute attacks
  • Common Use Cases: IoT devices, enterprise VPNs, defense systems, embedded systems

PSK Pattern Format

PSK patterns use modifiers (psk0 through psk4) that indicate when the PSK is mixed:

Noise_XXpsk3_25519_ChaChaPoly_SHA256  # XX with PSK after third message
Noise_NNpsk0_25519_ChaChaPoly_SHA256  # NN with PSK before first message
Noise_IKpsk2_448_AESGCM_BLAKE2b       # IK with PSK after second message

PSK Modifiers:

  • psk0: PSK mixed before first message (maximum quantum resistance)
  • psk1: PSK mixed after first message
  • psk2: PSK mixed after second message (common for IK patterns)
  • psk3: PSK mixed after third message (most common - XXpsk3)
  • psk4: PSK mixed after fourth message (rare)

Basic PSK Usage

import os
from noiseframework import NoiseHandshake, NoiseTransport

# Generate or load a 32-byte pre-shared key
psk = os.urandom(32)  # Must be exchanged securely out-of-band

# Initiator
initiator = NoiseHandshake("Noise_NNpsk0_25519_ChaChaPoly_SHA256")
initiator.set_as_initiator()
initiator.set_psk(psk)  # Set PSK before initialize()
initiator.initialize()

# Responder
responder = NoiseHandshake("Noise_NNpsk0_25519_ChaChaPoly_SHA256")
responder.set_as_responder()
responder.set_psk(psk)  # Must use same PSK
responder.initialize()

# Perform handshake (PSK mixed automatically)
msg1 = initiator.write_message(b"")
responder.read_message(msg1)

msg2 = responder.write_message(b"")
initiator.read_message(msg2)

# Create transport - now quantum-resistant!
init_send, init_recv = initiator.to_transport()
resp_send, resp_recv = responder.to_transport()

init_transport = NoiseTransport(init_send, init_recv)
resp_transport = NoiseTransport(resp_send, resp_recv)

# Secure communication
ciphertext = init_transport.send(b"Quantum-resistant message!")
plaintext = resp_transport.receive(ciphertext)

XXpsk3 - Most Common PSK Pattern

The XXpsk3 pattern (mutual authentication + late PSK) is the most commonly used PSK pattern:

psk = os.urandom(32)

# Initiator
initiator = NoiseHandshake("Noise_XXpsk3_25519_ChaChaPoly_SHA256")
initiator.set_as_initiator()
initiator.generate_static_keypair()  # XX requires static keys
initiator.set_psk(psk)
initiator.initialize()

# Responder
responder = NoiseHandshake("Noise_XXpsk3_25519_ChaChaPoly_SHA256")
responder.set_as_responder()
responder.generate_static_keypair()
responder.set_psk(psk)
responder.initialize()

# Three-message handshake
msg1 = initiator.write_message(b"")
responder.read_message(msg1)

msg2 = responder.write_message(b"")
initiator.read_message(msg2)

msg3 = initiator.write_message(b"")  # PSK mixed here
responder.read_message(msg3)

# Both parties mutually authenticated + quantum-resistant
assert initiator.remote_static_public == responder.static_public
assert responder.remote_static_public == initiator.static_public

PSK with Async/Await

PSK works seamlessly with async code:

async def async_psk_example():
    psk = os.urandom(32)
    
    handshake = AsyncNoiseHandshake("Noise_XXpsk3_25519_ChaChaPoly_SHA256")
    await handshake.set_as_initiator()
    await handshake.generate_static_keypair()
    await handshake.set_psk(psk)  # Async PSK setting
    await handshake.initialize()
    
    # Continue with async handshake...

PSK Security Considerations

When to Use PSK:

  • You need quantum resistance
  • You can securely exchange a shared secret out-of-band
  • You're building IoT or embedded systems
  • You want additional authentication beyond public keys

PSK Management:

  • PSKs must be exchanged securely (never over an insecure channel)
  • Use strong randomness (32 bytes from os.urandom())
  • PSKs can be safely reused across sessions
  • Consider key rotation policies for long-lived PSKs

Placement Trade-offs:

  • Early PSK (psk0): Maximum quantum resistance, protects entire handshake
  • Late PSK (psk3): Allows public key exchange first, then adds PSK protection

See examples/psk_example.py for complete working examples of NNpsk0, XXpsk3, and IKpsk2 patterns.


Fallback Pattern Support

NoiseFramework supports fallback patterns for graceful handshake degradation when the initiator's first message cannot be decrypted. This implements the Noise Pipes protocol (Section 10.2 of the Noise spec).

What are Fallback Patterns?

Fallback patterns enable recovery from handshake failures without dropping the connection:

  • Graceful Degradation: When IK/NK fails, fallback to XX pattern
  • Key Rotation: Handle responder static key changes
  • PSK Outdated: Recover from outdated pre-shared keys
  • Role Reversal: Responder becomes effective initiator in fallback

Noise Pipes Protocol (IK โ†’ XXfallback)

The most common fallback scenario is Noise Pipes: Alice attempts IK, Bob detects wrong static key and falls back to XXfallback.

import os
from noiseframework import NoiseHandshake

# Bob generates his real keys
bob = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
bob.set_as_responder()
bob.generate_static_keypair()
bob.initialize()

# Alice attempts IK with WRONG Bob static key (outdated)
alice = NoiseHandshake("Noise_IK_25519_ChaChaPoly_SHA256")
alice.set_as_initiator()
alice.generate_static_keypair()
alice.set_remote_static_public_key(wrong_bob_key)  # Outdated key
alice.initialize()

# Alice sends IK first message - will fail decryption
ik_msg1 = alice.write_message(b"Hello Bob")

# Bob cannot decrypt - extract Alice's ephemeral key (first 32 bytes)
alice_ephemeral = ik_msg1[:32]

# Bob initiates fallback to XXfallback
bob.start_fallback(alice_ephemeral)

# Bob now sends first XXfallback message (role reversal)
fallback_msg1 = bob.write_message(b"Fallback initiated")

# Alice detects IK failure and switches to XXfallback
alice_fallback = NoiseHandshake("Noise_XXfallback_25519_ChaChaPoly_SHA256")
alice_fallback.set_as_initiator()
alice_fallback.set_static_keypair(alice.static_private, alice.static_public)

# Reuse Alice's ephemeral keys (XXfallback uses them as pre-message)
alice_fallback.ephemeral_private = alice.ephemeral_private
alice_fallback.ephemeral_public = alice.ephemeral_public
alice_fallback.initialize()

# Alice reads Bob's fallback message
payload1 = alice_fallback.read_message(fallback_msg1)

# Alice sends her static key (second XXfallback message)
fallback_msg2 = alice_fallback.write_message(b"Acknowledged")

# Bob reads Alice's response - handshake complete!
payload2 = bob.read_message(fallback_msg2)

# Create transport channels
bob_send, bob_recv = bob.to_transport()
alice_send, alice_recv = alice_fallback.to_transport()

# Secure communication established despite initial IK failure!

Fallback Mechanics

Pattern Transformation:

XX โ†’ XXfallback    # First message "e" becomes pre-message
NN โ†’ NNfallback    # First message "e" becomes pre-message
IK โ†’ XXfallback    # Cannot use IKfallback (first message contains DH)
NK โ†’ XXfallback    # Cannot use NKfallback (first message contains DH)

Valid Fallback Patterns:

  • Fallback requires initiator's first message to be "e", "s", or "e, s"
  • Cannot fallback patterns where first message contains DH operations (es, se, ss, ee)

Fallback Process:

  1. Responder receives initiator's first message but cannot decrypt
  2. Responder extracts initiator's ephemeral key from failed message
  3. Responder calls start_fallback(remote_ephemeral_public_key)
  4. Pattern switches (e.g., XX โ†’ XXfallback), state re-initialized
  5. Responder sends first message (role reversal)
  6. Initiator also switches to fallback pattern
  7. Handshake completes normally with fallback pattern

API Usage

# Responder only - call when decryption fails
handshake.start_fallback(remote_ephemeral_public_key: bytes) -> None

# Async version
await handshake.start_fallback(remote_ephemeral_public_key: bytes)

# Example error handling with fallback
try:
    payload = responder.read_message(initiator_msg1)
except Exception:
    # Extract ephemeral key (first DHLEN bytes)
    alice_ephemeral = initiator_msg1[:32]  # 32 for Curve25519
    responder.start_fallback(alice_ephemeral)
    # Continue with fallback pattern...

Security Considerations

When to Use Fallback:

  • Server static key rotation scenarios
  • PSK may be outdated but fallback acceptable
  • Graceful degradation preferred over connection failure

Fallback Limitations:

  • Both parties must coordinate fallback (initiator must also switch)
  • Ephemeral key must be preserved from failed message
  • Only works with patterns where first message can be pre-message

Noise Pipes Use Case: The IK โ†’ XXfallback pattern is standardized as "Noise Pipes" and widely used for:

  • IoT device provisioning (device has outdated server key)
  • Server key rotation (clients gradually update)
  • Fallback from authenticated to mutual authentication

See examples/fallback_example.py for a complete working demonstration of the Noise Pipes protocol with IK โ†’ XXfallback.


High-Level Connection API

For most applications, the high-level NoiseConnection and AsyncNoiseConnection classes provide the simplest way to establish secure connections. These classes automatically handle handshakes, transport mode transitions, and message framing.

Synchronous Connection Example

from noiseframework import NoiseConnection
import socket
import threading

def server():
    """Responder side."""
    server_sock = socket.socket()
    server_sock.bind(("localhost", 9999))
    server_sock.listen(1)
    client_sock, _ = server_sock.accept()
    
    # Create connection and accept - handshake happens automatically
    with NoiseConnection("Noise_XX_25519_ChaChaPoly_SHA256", "responder") as conn:
        conn.accept(client_sock)
        
        # Now in transport mode - send/receive encrypted messages
        data = conn.receive()
        conn.send(b"Echo: " + data)
    
    server_sock.close()

# Start server in background
threading.Thread(target=server, daemon=True).start()

# Client (initiator side)
with NoiseConnection("Noise_XX_25519_ChaChaPoly_SHA256", "initiator") as conn:
    conn.connect(("localhost", 9999))  # Handshake happens automatically
    
    conn.send(b"Hello, NoiseFramework!")
    response = conn.receive()
    print(response)  # b"Echo: Hello, NoiseFramework!"

Asynchronous Connection Example

import asyncio
from noiseframework import AsyncNoiseConnection

async def handle_client(reader, writer):
    """Async responder."""
    async with AsyncNoiseConnection("Noise_XX_25519_ChaChaPoly_SHA256", "responder") as conn:
        await conn.accept_streams(reader, writer)  # Auto handshake
        
        data = await conn.receive()
        await conn.send(b"Echo: " + data)

async def main():
    # Start async server
    server = await asyncio.start_server(handle_client, "localhost", 9999)
    
    # Client
    async with AsyncNoiseConnection("Noise_XX_25519_ChaChaPoly_SHA256", "initiator") as conn:
        await conn.connect(("localhost", 9999))  # Auto handshake
        
        await conn.send(b"Hello, async!")
        response = await conn.receive()
        print(response)  # b"Echo: Hello, async!"
    
    server.close()
    await server.wait_closed()

asyncio.run(main())

Connection API Features

NoiseConnection (sync) and AsyncNoiseConnection (async):

# Constructor
conn = NoiseConnection(
    pattern="Noise_XX_25519_ChaChaPoly_SHA256",
    role="initiator",  # or "responder"
    static_private_key=None,  # Optional: use custom keys
    static_public_key=None,
    remote_static_public_key=None,  # For IK/NK patterns
    max_message_size=16*1024*1024,  # 16 MB default
    logger=None  # Optional logging
)

# Initiator methods
conn.connect(("host", port))  # Sync
await conn.connect(("host", port))  # Async

# Responder methods
conn.accept(client_socket)  # Sync
await conn.accept_streams(reader, writer)  # Async

# Communication (same for both roles)
conn.send(plaintext_bytes)  # Sync
await conn.send(plaintext_bytes)  # Async

plaintext = conn.receive()  # Sync
plaintext = await conn.receive()  # Async

# Connection state
if conn.is_connected:
    print("Connected and handshake complete")

# Identity verification
remote_key = conn.remote_static_public_key  # Get remote's identity
local_key = conn.local_static_public_key    # Get our identity

# Cleanup
conn.close()  # Sync
await conn.close()  # Async

# Or use context managers (automatic cleanup)
with NoiseConnection(...) as conn:
    # ... use conn ...
# Closes automatically

async with AsyncNoiseConnection(...) as conn:
    # ... use conn ...
# Closes automatically

Benefits of Connection API

  • โœ… Automatic handshake - No manual write_message()/read_message() calls
  • โœ… Automatic framing - Built-in length-prefixed message framing
  • โœ… Automatic transport - Seamless transition from handshake to encryption
  • โœ… Simple lifecycle - Just connect()/accept(), send(), receive(), close()
  • โœ… Identity access - Easy access to local and remote public keys
  • โœ… Context managers - Automatic cleanup with with statements
  • โœ… Clear errors - ValidationError, HandshakeError, TransportError

When to Use Each API

Use NoiseConnection/AsyncNoiseConnection when:

  • Building complete secure connections (most common use case)
  • You want simplicity and automatic handling
  • Standard patterns (XX, IK, NK, etc.) are sufficient

Use NoiseHandshake + NoiseTransport when:

  • You need fine control over handshake steps
  • Implementing custom protocols on top of Noise
  • Building non-standard integrations

See examples/connection_example.py for complete examples including custom keys and identity verification.


๐Ÿ–ฅ๏ธ CLI Documentation

The NoiseFramework command-line tool provides easy access to key operations without writing code.

Generate Keypair

Generate static keypairs for use in Noise handshakes:

# Generate Curve25519 keypair (default)
noiseframework generate-keypair -o mykey
# Creates: mykey_private.key (32 bytes), mykey_public.key (32 bytes)

# Generate Curve448 keypair
noiseframework generate-keypair --dh 448 -o mykey448
# Creates: mykey448_private.key (56 bytes), mykey448_public.key (56 bytes)

# Use short alias
noiseframework genkey -o server_key

Output:

Generated keypair:
  Private key: mykey_private.key
  Public key:  mykey_public.key
  Key size:    32 bytes

Usage in Python:

from pathlib import Path
from noiseframework import NoiseHandshake

# Load generated keys
private_key = Path("mykey_private.key").read_bytes()
public_key = Path("mykey_public.key").read_bytes()

# Use in handshake
hs = NoiseHandshake("Noise_XX_25519_ChaChaPoly_SHA256")
hs.set_static_keypair(private_key, public_key)

Validate Pattern

Validate Noise pattern strings and view their components:

# Validate a pattern
noiseframework validate-pattern "Noise_XX_25519_ChaChaPoly_SHA256"

# Use short alias
noiseframework validate "Noise_IK_448_AESGCM_BLAKE2b"

Output:

Pattern: Noise_XX_25519_ChaChaPoly_SHA256
  Valid: โœ“
  Name:       Noise_XX_25519_ChaChaPoly_SHA256
  Handshake:  XX
  DH:         25519
  Cipher:     ChaChaPoly
  Hash:       SHA256

Invalid pattern:

noiseframework validate "Noise_INVALID_Pattern"
# Error: Invalid pattern: Unsupported handshake pattern: INVALID

Show Information

Display supported cryptographic primitives and patterns:

noiseframework info

Output:

NoiseFramework - Noise Protocol Framework Implementation

Supported DH functions:
  - 25519 (Curve25519/X25519)
  - 448 (Curve448/X448)

Supported ciphers:
  - ChaChaPoly (ChaCha20-Poly1305) [recommended]
  - AESGCM (AES-256-GCM)

Supported hash functions:
  - SHA256 [recommended]
  - SHA512
  - BLAKE2s
  - BLAKE2b

Supported patterns:
  NN, NK, NX, KN, KK, KX, XN, XK, XX, IN, IK, IX

Example pattern string:
  Noise_XX_25519_ChaChaPoly_SHA256

Help and Version

# Show help
noiseframework --help
noiseframework generate-keypair --help

# Show version
noiseframework --version

๐Ÿ” Supported Patterns

NoiseFramework supports all fundamental and interactive Noise patterns:

Pattern Description Use Case
NN No static keys Anonymous communication
KN Initiator known Server authentication
NK Responder known Client knows server's key
KK Both known Pre-shared public keys
NX Responder transmits Certificate-like exchange
KX Initiator known, responder transmits Hybrid authentication
XN Initiator transmits Basic server setup
IN Initiator identity hidden Privacy-preserving
XK Responder known, initiator transmits Standard mutual auth
IK Responder known, initiator identity hidden Tor-like handshake
XX Both transmit Full mutual authentication
IX Initiator identity hidden, responder transmits Privacy + auth

Pattern Modifiers

  • psk0, psk1, psk2: Pre-shared symmetric key modes
  • Fallback patterns: For retry and downgrade scenarios

๐Ÿ”‘ Cryptographic Primitives

NoiseFramework uses battle-tested cryptographic libraries:

Diffie-Hellman Functions

  • Curve25519 (X25519) - Recommended
  • Curve448 (X448)

Cipher Functions (AEAD)

  • ChaChaPoly (ChaCha20-Poly1305) - Recommended
  • AESGCM (AES-256-GCM)

Hash Functions

  • SHA-256 - Recommended
  • SHA-512
  • BLAKE2s
  • BLAKE2b

Example pattern string: Noise_XX_25519_ChaChaPoly_SHA256

Format: Noise_[PATTERN]_[DH]_[CIPHER]_[HASH]


๐Ÿ—๏ธ Architecture

noiseframework/
โ”œโ”€โ”€ noiseframework/
โ”‚   โ”œโ”€โ”€ __init__.py          # Public API
โ”‚   โ”œโ”€โ”€ exceptions.py        # Custom exception hierarchy
โ”‚   โ”œโ”€โ”€ noise/
โ”‚   โ”‚   โ”œโ”€โ”€ handshake.py     # Handshake state machine
โ”‚   โ”‚   โ”œโ”€โ”€ pattern.py       # Pattern parser and validator
โ”‚   โ”‚   โ””โ”€โ”€ state.py         # Cipher and symmetric state
โ”‚   โ”œโ”€โ”€ crypto/
โ”‚   โ”‚   โ”œโ”€โ”€ dh.py            # Diffie-Hellman functions
โ”‚   โ”‚   โ”œโ”€โ”€ cipher.py        # AEAD cipher implementations
โ”‚   โ”‚   โ””โ”€โ”€ hash.py          # Hash function wrappers
โ”‚   โ”œโ”€โ”€ transport/
โ”‚   โ”‚   โ””โ”€โ”€ transport.py     # Post-handshake encryption
โ”‚   โ”œโ”€โ”€ framing.py           # Message framing utilities
โ”‚   โ”œโ”€โ”€ async_support.py     # Async/await wrappers
โ”‚   โ””โ”€โ”€ cli/
โ”‚       โ””โ”€โ”€ main.py          # Command-line interface
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ test_handshake.py
โ”‚   โ”œโ”€โ”€ test_transport.py
โ”‚   โ”œโ”€โ”€ test_pattern.py
โ”‚   โ”œโ”€โ”€ test_cipher.py
โ”‚   โ”œโ”€โ”€ test_exceptions.py   # Exception tests
โ”‚   โ”œโ”€โ”€ test_logging.py      # Logging tests
โ”‚   โ”œโ”€โ”€ test_framing.py      # Framing tests
โ”‚   โ””โ”€โ”€ test_async.py        # Async tests
โ”œโ”€โ”€ examples/
โ”‚   โ”œโ”€โ”€ basic_client_server.py
โ”‚   โ”œโ”€โ”€ simple_chat.py
โ”‚   โ”œโ”€โ”€ file_encryption.py
โ”‚   โ”œโ”€โ”€ error_handling_example.py
โ”‚   โ”œโ”€โ”€ logging_example.py
โ”‚   โ”œโ”€โ”€ framed_tcp_example.py
โ”‚   โ””โ”€โ”€ async_tcp_example.py
โ”œโ”€โ”€ docs/
โ”‚   โ”œโ”€โ”€ API.md
โ”‚   โ”œโ”€โ”€ CHANGELOG.md
โ”‚   โ”œโ”€โ”€ ARCHITECTURE.md
โ”‚   โ”œโ”€โ”€ SECURITY.md
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ pyproject.toml
โ””โ”€โ”€ README.md

๐Ÿงช Testing

NoiseFramework has comprehensive test coverage with 311 tests achieving 100% pass rate.

Test Categories

  • Core functionality (156 tests): Handshake, transport, patterns, crypto primitives
  • Exception handling (15 tests): Custom exception hierarchy and error messages
  • Logging (21 tests): Logging functionality across all components
  • Framing (30 tests): Message framing for stream-based transports
  • Async support (21 tests): Async/await functionality

โšก Performance

NoiseFramework delivers production-ready performance with real-world benchmarks:

  • Handshakes: ~1,500-1,800 complete handshakes/sec (XX pattern)
  • Transport encryption: 3+ GB/s throughput for large messages
  • Key generation: ~32,000 keypairs/sec (Curve25519)
  • Latency: <3 ยตs per small message encryption

Quick Benchmark Results

Operation Performance
Complete XX handshake 558-642 ยตs
Encrypt 64 KB message 18.5 ยตs (3.29 GB/s)
Encrypt 1 KB message 2.4 ยตs (403 MB/s)
Generate Curve25519 keypair 31 ยตs

See BENCHMARKS.md for comprehensive performance analysis, methodology, and optimization tips.

Run Benchmarks Yourself

# Activate virtual environment
source .venv/bin/activate  # Linux/macOS
# or
.\.venv\Scripts\Activate.ps1  # Windows PowerShell

# Run benchmark script
python benchmark.py

๐Ÿงช Testing (Detailed)

Run the test suite:

# Install test dependencies
pip install -e ".[dev]"

# Run all tests
pytest

# Run with coverage
pytest --cov=noiseframework --cov-report=html

# Run specific test file
pytest tests/test_handshake.py

# Run with verbose output
pytest -v

Test Categories

  • Unit tests: Test individual components in isolation
  • Integration tests: Test complete handshake flows
  • Property-based tests: Use Hypothesis for invariant testing
  • Vector tests: Validate against official Noise test vectors

๐Ÿค Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository and create a feature branch
  2. Follow the coding style: PEP 8, type hints, and existing conventions
  3. Write tests: All new features must include tests
  4. Update documentation: Add examples and update CHANGELOG.md
  5. Run the test suite: Ensure all tests pass
  6. Submit a pull request: Describe your changes clearly

See CONTRIBUTING.md for detailed guidelines.

Development Setup

git clone https://github.com/juliuspleunes4/noiseframework.git
cd noiseframework
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -e ".[dev]"

โ“ FAQ

Which pattern should I use?

  • XX: Default choice for mutual authentication
  • NN: Quick anonymous encryption (no authentication)
  • IK: When client knows server's key in advance (like Tor)
  • NK: When server identity is public (like HTTPS with pinning)

Is NoiseFramework production-ready?

Yes, but with caveats:

  • โœ… Cryptographically sound (uses battle-tested primitives)
  • โœ… Specification-compliant implementation
  • โœ… Well-tested (311 tests, 100% pass rate)
  • โœ… Comprehensive error handling with helpful messages
  • โœ… Production-ready logging and monitoring support
  • โš ๏ธ Consider security audit for high-stakes applications
  • โš ๏ธ Keep dependencies updated

How does it compare to other Noise implementations?

  • PyNaCl/libsodium: Lower-level, NoiseFramework is higher-level Noise protocol
  • noiseprotocol (Python): Similar, but NoiseFramework has better docs and CLI
  • snow (Rust): Faster, but NoiseFramework is pure Python with better accessibility

Can I use custom cryptographic primitives?

Yes, you can extend the crypto modules. However, we strongly recommend using only well-vetted primitives from established libraries.

Does it support post-quantum cryptography?

Not yet. Post-quantum Noise patterns (pqXX, etc.) are planned for future releases.


๐Ÿ”’ Security

Reporting Vulnerabilities

If you discover a security vulnerability, please DO NOT open a public issue. Instead:

  1. Email security concerns to: [jjgpleunes@gmail.com]
  2. Include a detailed description and steps to reproduce
  3. Allow reasonable time for a fix before public disclosure

Security Best Practices

  • Key Management: Never hard-code keys in source code
  • RNG: Use system-provided cryptographically secure random number generators
  • Updates: Keep NoiseFramework and its dependencies up-to-date
  • Audit: Consider professional security audits for production use
  • Side-Channels: Be aware of timing and other side-channel attacks

Dependencies

NoiseFramework relies on:

  • cryptography - Audited, well-maintained Python cryptography library
  • No custom cryptographic primitives

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.


๐Ÿ™ Acknowledgments

  • Trevor Perrin - Creator of the Noise Protocol Framework
  • Noise Protocol Community - For the specification and test vectors
  • PyCA Cryptography - For providing robust cryptographic primitives

๐Ÿ“š Resources


๐Ÿš€ Roadmap & Future Features

NoiseFramework v1.3.0 is feature-complete with all planned production-readiness enhancements implemented. Future development is planned for v1.4.0 and beyond.

Completed in v1.3.0 โœ…

All 7 major features have been implemented:

  • โœ… Async/Await Support (21 tests)
  • โœ… Message Framing (30 tests)
  • โœ… Better Error Messages (15 tests)
  • โœ… High-Level Connection API (25 tests)
  • โœ… Logging Support (21 tests)
  • โœ… PSK Support (22 tests)
  • โœ… Fallback Pattern Support (21 tests)

Planned for v1.4.0 ๐Ÿ”„

The next version will focus on advanced Noise Protocol Framework features:

  1. Rekey Support - Prevent nonce exhaustion for long-lived connections

    • Manual and automatic rekeying
    • Essential for IoT, VPNs, and persistent servers
    • Spec: Section 11.3
  2. Deferred Patterns - Support NX, KX, IX patterns

    • Responder identity revealed later in handshake
    • Server-side optimization opportunities
    • Spec: Section 10.4
  3. Channel Binding - Link Noise session to application context

    • Prevents session confusion attacks
    • Required for some compliance scenarios
    • Spec: Section 11.2

Future Considerations (v1.5.0+) ๐Ÿ’ก

  • Out-of-Order Transport (UDP support)
  • Post-Quantum Cryptography (Kyber, hybrid schemes)
  • Hardware Security Module (HSM) support
  • Additional cipher suites
  • Performance optimizations
  • Protocol plugins/extensions

๐Ÿ“‹ See docs/TODO.md for detailed feature descriptions, planned APIs, and implementation roadmap.


๐Ÿ“ž Support


Built with โค๏ธ for secure communications

If you find this project useful, please consider giving it a โญ๏ธ

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

noiseframework-1.3.0.tar.gz (100.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

noiseframework-1.3.0-py3-none-any.whl (50.4 kB view details)

Uploaded Python 3

File details

Details for the file noiseframework-1.3.0.tar.gz.

File metadata

  • Download URL: noiseframework-1.3.0.tar.gz
  • Upload date:
  • Size: 100.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for noiseframework-1.3.0.tar.gz
Algorithm Hash digest
SHA256 5f2cde573d1ea2f9c12bb972652288d0ab145a1b725c3b44d4f30cb368e0bf1d
MD5 0cc1328118a750c12ef9e63773f7a62f
BLAKE2b-256 fb609ca97242c11a1d84700fdef221311f1191ab8372e9cea1fb06c86195dd7c

See more details on using hashes here.

File details

Details for the file noiseframework-1.3.0-py3-none-any.whl.

File metadata

  • Download URL: noiseframework-1.3.0-py3-none-any.whl
  • Upload date:
  • Size: 50.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for noiseframework-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 20ffdc7abb3d516b50899956e7c4b921a5796b8aab043e9ad964144cb816a06b
MD5 accd275cff9a2bd8fdb83b7e03c8f451
BLAKE2b-256 39f0cb40a1d77fd5092985c79d03af6dfb1dfd7ed689f27a3f2af816cae6b9f9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page