A professional, secure, and easy-to-use implementation of the Noise Protocol Framework in Python
Project description
NoiseFramework
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
- Installation
- Quick Start
- Python API Documentation
- CLI Documentation
- Supported Patterns
- Cryptographic Primitives
- Architecture
- Testing
- Performance
- Contributing
- Security
- FAQ
- License
- Acknowledgments
โจ 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:
NoiseConnectionandAsyncNoiseConnectionfor 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 initiatorawait set_as_responder()- Set role as responderawait generate_static_keypair()- Generate static keysawait set_static_keypair(private, public)- Set existing keysawait set_remote_static_public_key(public)- Set remote's public keyawait initialize()- Initialize handshake stateawait write_message(payload)- Write handshake messageawait read_message(message)- Read handshake messageawait to_transport()- Get AsyncNoiseTransport after completionawait 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 messageawait 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 messageawait close()- Close reader.messages_received- Message counter (property)
AsyncFramedWriter: Async framed message writer
await write_message(message)- Write length-prefixed messageawait close()- Close writer and wait for completion.messages_sent- Message counter (property)
Key Points:
- All async operations use
run_in_executorinternally - No blocking calls in the async event loop
- Compatible with
asyncio.StreamReaderandasyncio.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 messagepsk2: 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:
- Responder receives initiator's first message but cannot decrypt
- Responder extracts initiator's ephemeral key from failed message
- Responder calls
start_fallback(remote_ephemeral_public_key) - Pattern switches (e.g., XX โ XXfallback), state re-initialized
- Responder sends first message (role reversal)
- Initiator also switches to fallback pattern
- 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
withstatements - โ 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:
- Fork the repository and create a feature branch
- Follow the coding style: PEP 8, type hints, and existing conventions
- Write tests: All new features must include tests
- Update documentation: Add examples and update
CHANGELOG.md - Run the test suite: Ensure all tests pass
- 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:
- Email security concerns to: [jjgpleunes@gmail.com]
- Include a detailed description and steps to reproduce
- 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
- Noise Protocol Framework Specification
- Noise Explorer - Formal verification of Noise patterns
- Noise Wiki
- PyCA Cryptography Documentation
๐ 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:
-
Rekey Support - Prevent nonce exhaustion for long-lived connections
- Manual and automatic rekeying
- Essential for IoT, VPNs, and persistent servers
- Spec: Section 11.3
-
Deferred Patterns - Support NX, KX, IX patterns
- Responder identity revealed later in handshake
- Server-side optimization opportunities
- Spec: Section 10.4
-
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
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Full Documentation
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f2cde573d1ea2f9c12bb972652288d0ab145a1b725c3b44d4f30cb368e0bf1d
|
|
| MD5 |
0cc1328118a750c12ef9e63773f7a62f
|
|
| BLAKE2b-256 |
fb609ca97242c11a1d84700fdef221311f1191ab8372e9cea1fb06c86195dd7c
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20ffdc7abb3d516b50899956e7c4b921a5796b8aab043e9ad964144cb816a06b
|
|
| MD5 |
accd275cff9a2bd8fdb83b7e03c8f451
|
|
| BLAKE2b-256 |
39f0cb40a1d77fd5092985c79d03af6dfb1dfd7ed689f27a3f2af816cae6b9f9
|