Version: 0.9.0 Last Updated: 2026-04-25
This document provides a comprehensive reference for the NoisePQC++ v0.9.0 public API, including classical, post-quantum (PQNoise), and hybrid forward secrecy (HFS) features.
- Importing the Module
- Common Types
- Logging Control
- Cipher
- CipherState
- Hash
- Dh
- SymmetricState
- Protocol
- Pattern
- HandshakeState
import noise;
using namespace Noise;The actual enums in code use simplified names:
// Cipher algorithms
enum class CipherAlgo {
ChaChaPoly, // ChaCha20-Poly1305 (default)
AESGCM // AES-256-GCM
};
// Hash algorithms
enum class HashAlgo {
SHA256, // SHA-256 (default, 32 bytes)
SHA512, // SHA-512 (64 bytes)
BLAKE2b, // BLAKE2b (64 bytes)
BLAKE2s // BLAKE2s (32 bytes)
};
// DH and KEM algorithms
enum class KeyAlgo {
X25519, // Curve25519 (default, 32 bytes)
X448, // Curve448 (56 bytes)
M512, // ML-KEM-512 (800-byte public key, 768-byte ciphertext)
M768, // ML-KEM-768 (1184-byte public key, 1088-byte ciphertext) - Recommended
M1024 // ML-KEM-1024 (1568-byte public key, 1568-byte ciphertext)
};Algorithm names as std::string_view constants:
// Hash algorithms
constexpr std::string_view SHA256 = "SHA256";
constexpr std::string_view SHA512 = "SHA512";
constexpr std::string_view BLAKE2b = "BLAKE2b";
constexpr std::string_view BLAKE2s = "BLAKE2s";
// Cipher algorithms
constexpr std::string_view AESGCM = "AESGCM";
constexpr std::string_view ChaChaPoly = "ChaChaPoly";
// DH algorithms
constexpr std::string_view X25519 = "25519";
constexpr std::string_view X448 = "448";
// KEM algorithms (Post-Quantum)
constexpr std::string_view M512 = "M512";
constexpr std::string_view M768 = "M768";
constexpr std::string_view M1024 = "M1024";// Core Noise constants
constexpr std::uint8_t SYMMETRIC_KEY_SIZE = 32; // 32 bytes
constexpr auto MAX_NONCE = std::numeric_limits<std::uint64_t>::max();
constexpr std::uint16_t NOISE_MAX_MESSAGE_SIZE = 65535;
constexpr std::string_view protocolPrefix = "Noise";
// ML-KEM public key sizes
constexpr std::size_t M512_PUBLIC_KEY_SIZE = 800;
constexpr std::size_t M768_PUBLIC_KEY_SIZE = 1184;
constexpr std::size_t M1024_PUBLIC_KEY_SIZE = 1568;
// ML-KEM ciphertext sizes
constexpr std::size_t M512_CIPHERTEXT_SIZE = 768;
constexpr std::size_t M768_CIPHERTEXT_SIZE = 1088;
constexpr std::size_t M1024_CIPHERTEXT_SIZE = 1568;Note: Logging functions are only available in debug builds (-DNOISE_DEBUG_BUILD=ON). In production builds, they are no-ops.
// Enable all logging (handshake + symmetric state)
void EnableAllLogging();
// Disable all logging
void DisableAllLogging();// Control handshake logging separately
void SetHandshakeLogging(bool enable);
// Control symmetric state logging separately
void SetSymmetricLogging(bool enable);import noise;
using namespace Noise;
int main() {
// Enable debug logging
EnableAllLogging();
// ... perform handshake with full logging ...
// Disable for performance-sensitive operations
DisableAllLogging();
return 0;
}For detailed information on debug builds and logging, see LoggingAndDebugging.md.
AEAD (Authenticated Encryption with Associated Data) cipher operations.
Supported Algorithms:
- ChaCha20-Poly1305 (default) - 12-byte nonce, 16-byte tag
- AES-256-GCM - 12-byte nonce, 16-byte tag
// Default constructor (ChaCha20-Poly1305)
Cipher();
// From CipherAlgo enum
explicit Cipher(CipherAlgo algo);
// From algorithm name string
explicit Cipher(std::string_view cipher_name);
// Examples
Cipher cipher1; // ChaCha20-Poly1305 (default)
Cipher cipher2(CipherAlgo::AESGCM); // AES-256-GCM
Cipher cipher3(ChaChaPoly); // Using string constant
Cipher cipher4("AESGCM"); // Direct stringNote: String names are mapped internally:
"ChaChaPoly"→"ChaCha20Poly1305"(Botan name)"AESGCM"→"AES-256/GCM"(Botan name)
Cipher(const Cipher& other); // Copy constructor
Cipher(Cipher&& other) noexcept; // Move constructor
Cipher& operator=(const Cipher&) = delete; // No copy assignment
Cipher& operator=(Cipher&&) = delete; // No move assignmentvoid Encrypt(
std::vector<std::uint8_t>& dst, // Output buffer (ciphertext appended)
std::span<const std::uint8_t> k, // 32-byte key
std::uint64_t n, // 64-bit nonce
std::span<const std::uint8_t> ad, // Associated data
std::span<const std::uint8_t> plaintext // Data to encrypt
) const;Parameters:
dst: Buffer to which ciphertext (with appended tag) is writtenk: 32-byte symmetric keyn: 64-bit nonce (encoded as 12 bytes with 4-byte zero prefix)ad: Associated data (authenticated but not encrypted)plaintext: Data to encrypt
Behavior: Appends plaintext.size() + 16 bytes to dst (ciphertext + 16-byte tag).
Nonce Encoding:
- ChaCha20-Poly1305: 4 zero bytes + 8-byte little-endian counter
- AES-GCM: 4 zero bytes + 8-byte big-endian counter
void Decrypt(
std::vector<std::uint8_t>& dst,
std::span<const std::uint8_t> k,
std::uint64_t n,
std::span<const std::uint8_t> ad,
std::span<const std::uint8_t> ciphertext
) const;Parameters: Same as Encrypt, but takes ciphertext with tag and appends decrypted plaintext to dst.
Throws: Botan::Invalid_Authentication_Tag on AEAD tag verification failure.
std::string_view String() const; // Get algorithm name ("ChaChaPoly" or "AESGCM")
auto GetAeadTagSize() const; // Get tag size (16 bytes)
void Reset() const; // Reset internal cipher stateManages cipher state with automatic nonce handling and key initialization tracking.
CipherState(Cipher cipher, std::uint32_t maxMessageSize);
// Example
Cipher cipher(ChaChaPoly);
CipherState state(std::move(cipher), NOISE_MAX_MESSAGE_SIZE); // 65535CipherState(const CipherState&) = default; // Copy constructor
CipherState(CipherState&&) noexcept = default; // Move constructor
CipherState& operator=(const CipherState&) = delete; // No copy assignment
CipherState& operator=(CipherState&&) = delete; // No move assignmentCipher cipher; // AEAD cipher instance
std::array<std::uint8_t, SYMMETRIC_KEY_SIZE> key; // 32-byte key (zero-initialized)
std::uint64_t nonce; // 8-byte nonce counter
std::size_t aeadOverhead; // AEAD tag size (16 bytes)
std::uint32_t maxMessageSize; // Max message size
bool hadKeyBefore; // Track plaintext → encrypted transitionNote: hadKeyBefore tracks state transition from plaintext mode to encrypted mode, important for debug logging.
void InitializeKey(std::span<const std::uint8_t> key);Description: Sets the cipher key (must be 32 bytes) and resets nonce to 0.
Parameters: key - 32-byte key data
Throws: Error if key.size() != 32
Behavior: Tracks first key initialization via hadKeyBefore flag.
bool HasKey() const;Returns: true if any byte in the key is non-zero
void SetNonce(std::uint64_t nonce);Description: Manually set the nonce value (for testing or specialized use cases)
[[nodiscard]] std::vector<std::uint8_t> EncryptWithAd(
std::span<const std::uint8_t> ad,
std::span<const std::uint8_t> plaintext
);Description: Encrypts plaintext with associated data, returning ciphertext. Auto-increments nonce.
Behavior:
- If no key set: Returns plaintext unchanged (plaintext passthrough per Noise spec)
- If key set: Returns ciphertext with 16-byte tag appended
Throws: NoiseError if nonce > MAX_NONCE
Note: A buffer-appending overload exists privately and is used by SymmetricState (which is a friend of CipherState).
[[nodiscard]] std::vector<std::uint8_t> DecryptWithAd(
std::span<const std::uint8_t> ad,
std::span<const std::uint8_t> ciphertext
);Description: Decrypts ciphertext with associated data, returning plaintext. Auto-increments nonce.
Behavior:
- If no key set: Returns ciphertext unchanged (plaintext passthrough per Noise spec)
- If key set: Returns decrypted plaintext
Throws: NoiseError if nonce > MAX_NONCE; Botan::Invalid_Authentication_Tag on tag verification failure.
void Rekey(std::string_view hash_algo = "HKDF(SHA-256)");Description: Derives a new key from the current key using HKDF.
Purpose: Provides forward secrecy for long-lived connections.
Behavior: Does nothing if no key is set.
Parameters: hash_algo - KDF algorithm (default: HKDF with SHA-256)
void Reset();Description: Securely wipes the key using Botan::secure_scrub_memory and resets cipher state.
Note: Does NOT reset nonce (by Noise spec design).
Cryptographic hash functions for key derivation and message authentication.
Supported Algorithms:
- SHA-256 (default) - 32-byte output
- SHA-512 - 64-byte output
- BLAKE2b - 64-byte output
- BLAKE2s - 32-byte output
// Default constructor (SHA-256)
Hash();
// From HashAlgo enum
explicit Hash(HashAlgo algo);
// From algorithm name string
explicit Hash(std::string_view algo_name);
// Examples
Hash hash1; // SHA-256 (default)
Hash hash2(HashAlgo::BLAKE2b); // BLAKE2b
Hash hash3(SHA256); // Using string constant
Hash hash4("SHA512"); // Direct stringNote: String names are mapped internally:
"SHA256"→"SHA-256"(Botan name)"SHA512"→"SHA-512"(Botan name)"BLAKE2b"and"BLAKE2s"pass through unchanged
Hash(const Hash& other); // Copy constructor
Hash(Hash&& other) noexcept; // Move constructor
Hash& operator=(const Hash&) = delete; // No copy assignment
Hash& operator=(Hash&&) = delete; // No move assignmentstd::unique_ptr<Botan::HashFunction> hash_function; // Botan hash instanceNote: Direct access to hash_function allows advanced usage with Botan's API.
std::string_view String() const;Returns: Algorithm name ("SHA256", "SHA512", "BLAKE2b", or "BLAKE2s")
std::size_t Size() const;Returns: Hash output length in bytes (32 or 64)
The Hash class primarily wraps Botan's HashFunction for use by SymmetricState. Users typically access hash functions through the Botan API via the public hash_function member:
Hash hasher(SHA256);
// One-shot hashing
auto digest = hasher.hash_function->process(data);
// Incremental hashing
hasher.hash_function->update(data1);
hasher.hash_function->update(data2);
auto digest = hasher.hash_function->final_stdvec();
// Reset for reuse
hasher.hash_function->clear();
// Get output size
auto size = hasher.hash_function->output_length(); // 32 for SHA-256Diffie-Hellman key exchange and Key Encapsulation Mechanism (KEM) operations.
Supported DH Algorithms:
- X25519 (default) - 32-byte keys, based on Curve25519
- X448 - 56-byte keys, based on Curve448
Supported KEM Algorithms (Post-Quantum):
- ML-KEM-512 - 800-byte public key, 768-byte ciphertext
- ML-KEM-768 - 1184-byte public key, 1088-byte ciphertext (Recommended)
- ML-KEM-1024 - 1568-byte public key, 1568-byte ciphertext
using PrivateKey = std::vector<std::uint8_t>;
using PublicKey = std::vector<std::uint8_t>;
struct KeyPair {
PrivateKey private_key;
PublicKey public_key;
bool IsEmpty() const; // Returns true if both keys are empty
};Note: Keys are stored as raw bytes (not ASN.1 encoded).
// Default constructor (X25519)
Dh();
// From KeyAlgo enum
explicit Dh(KeyAlgo algo);
// From algorithm name string
explicit Dh(std::string_view algo_name);
// DH Examples
Dh dh1; // X25519 (default)
Dh dh2(KeyAlgo::X448); // X448
Dh dh3(X25519); // Using string constant "25519"
Dh dh4("448"); // Direct string for X448
// KEM Examples (Post-Quantum)
Dh kem1(KeyAlgo::M768); // ML-KEM-768 (recommended)
Dh kem2("M512"); // ML-KEM-512
Dh kem3("M1024"); // ML-KEM-1024Note: String names "25519" and "448" map to Botan names "X25519" and "X448". KEM names "M512", "M768", "M1024" map to "ML-KEM-512", etc.
Dh(const Dh& other) = default; // Copy constructor
Dh(Dh&& other) noexcept = default; // Move constructor
Dh& operator=(const Dh&) = delete; // No copy assignment
Dh& operator=(Dh&&) = delete; // No move assignmentKeyPair GenerateKeypair() const;Description: Generates a new cryptographically-random public/private key pair.
Returns: KeyPair with raw key bytes:
- X25519: 32 bytes for both private and public keys
- X448: 56 bytes for both private and public keys
Thread Safety: Uses Botan::System_RNG which is thread-safe.
std::vector<std::uint8_t> DH(
std::span<const std::uint8_t> my_raw_priv,
std::span<const std::uint8_t> peer_raw_pub
) const;Description: Performs Diffie-Hellman key agreement.
Parameters:
my_raw_priv: Your private key (32 or 56 bytes)peer_raw_pub: Peer's public key (32 or 56 bytes)
Returns: Shared secret (same size as keys)
Throws: NoiseError if called on a KEM algorithm or key sizes don't match.
std::vector<std::uint8_t> DH(const KeyPair& me, const KeyPair& peer) const;Description: Convenience overload that extracts keys from KeyPair structs.
Parameters:
me: Your KeyPair (usesprivate_key)peer: Peer's KeyPair (usespublic_key)
Returns: Shared secret
std::uint8_t Size() const;Returns: Shared secret size in bytes:
- X25519: 32
- X448: 56
- ML-KEM-512/768/1024: 32 (uniform shared secret size for all ML-KEM variants)
std::size_t PublicKeySize() const;Returns: Public key size in bytes:
- X25519: 32
- X448: 56
- ML-KEM-512: 800
- ML-KEM-768: 1184
- ML-KEM-1024: 1568
std::size_t CiphertextSize() const;Returns: KEM ciphertext size in bytes:
- ML-KEM-512: 768
- ML-KEM-768: 1088
- ML-KEM-1024: 1568
Throws: NoiseError if called on a DH algorithm (X25519 or X448).
bool IsKEM() const;Returns: true if this is a KEM algorithm (M512, M768, M1024), false for DH algorithms
std::pair<std::vector<std::uint8_t>, std::vector<std::uint8_t>>
Encapsulate(std::span<const std::uint8_t> peer_public_key) const;Description: Performs KEM encapsulation using the peer's public key.
Parameters: peer_public_key - Peer's KEM public key (800/1184/1568 bytes)
Returns: Pair of {ciphertext, shared_secret}
ciphertext- Encapsulated key (768/1088/1568 bytes)shared_secret- Derived 32-byte shared secret
Throws: NoiseError if called on a DH algorithm.
Example:
Dh kem(KeyAlgo::M768);
auto aliceKeys = kem.GenerateKeypair();
auto [ciphertext, sharedSecret] = kem.Encapsulate(aliceKeys.public_key);std::vector<std::uint8_t> Decapsulate(
std::span<const std::uint8_t> private_key,
std::span<const std::uint8_t> ciphertext
) const;Description: Performs KEM decapsulation using your private key.
Parameters:
private_key- Your KEM private keyciphertext- Encapsulated key from peer
Returns: 32-byte shared secret (matches the sender's shared secret)
Throws: NoiseError if called on a DH algorithm.
Example:
Dh kem(KeyAlgo::M768);
auto bobKeys = kem.GenerateKeypair();
// ... receive ciphertext from Alice ...
auto sharedSecret = kem.Decapsulate(bobKeys.private_key, ciphertext);std::string_view String() const;Returns: Algorithm name ("25519", "448", "M512", "M768", or "M1024")
void PrintKeys(const KeyPair& keypair) const;Description: Prints keys in hexadecimal format to stdout (for debugging).
Output Format:
Algorithm: 25519
Private Key: a29e64e41f70ac061900d1a01ae6b9bc...
Public Key: 3850ea9104be4bf7235ff08985954b1a...
Warning: Only use for debugging. Never log private keys in production.
Manages symmetric cryptographic state during the Noise handshake, including the chaining key (ck), handshake hash (h), and embedded CipherState.
SymmetricState(const Cipher& cipher, const Hash& hash);
// Example
Cipher cipher(ChaChaPoly);
Hash hash(SHA256);
SymmetricState ss(cipher, hash);SymmetricState(const SymmetricState&) = default; // Copy constructor
SymmetricState(SymmetricState&&) noexcept; // Move constructor
SymmetricState& operator=(const SymmetricState&) = delete; // No copy assignment
SymmetricState& operator=(SymmetricState&&) = delete; // No move assignmentCipherState cs; // Embedded cipher state
std::vector<std::uint8_t> ck; // Chaining key (HASHLEN bytes)
std::vector<std::uint8_t> h; // Handshake hash (HASHLEN bytes)
std::size_t hashLen; // Hash output length (32 or 64)Note: ck and h are resized during InitializeSymmetric() to match the hash algorithm's output length.
void InitializeSymmetric(std::span<const std::uint8_t> protocolName);Description: Initializes symmetric state with the protocol name.
Behavior:
- If
protocolName.size() <= hashLen: Copy directly toh, zero-pad if needed - If
protocolName.size() > hashLen: Hash the protocol name and use result ash - Sets
ck = h - Initializes CipherState with zero-filled 32-byte key
Example:
std::string protoName = "Noise_XX_25519_ChaChaPoly_SHA256";
ss.InitializeSymmetric(std::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(protoName.data()),
protoName.size()
));void MixKey(std::span<const std::uint8_t> input_km);Description: Performs HKDF to derive new chaining key and cipher key from input key material.
Operation: (ck, temp_k) = HKDF(ck, input_km, 2)
Behavior:
- Derives 2 outputs using HKDF
- Updates
ckwith first output - Initializes
cskey with second output (truncated to 32 bytes if HASHLEN=64)
Use Case: Called after DH operations to mix shared secrets into the state.
void MixHash(std::span<const std::uint8_t> data);Description: Updates handshake hash: h = HASH(h || data)
Parameters: data - Data to mix into handshake hash
Use Case: Called when transmitting/receiving public keys and ciphertext.
void MixKeyAndHash(std::span<const std::uint8_t> input_km);Description: Combines key and hash mixing for pre-shared keys.
Operation: (ck, temp_h, temp_k) = HKDF(ck, input_km, 3)
Behavior:
- Derives 3 outputs using HKDF
- Updates
ckwith first output - Mixes second output into
h - Initializes
cskey with third output
Use Case: Used for PSK patterns to mix pre-shared keys.
void EncryptAndHash(
std::vector<std::uint8_t>& dst,
std::span<const std::uint8_t> plaintext
);Description: Encrypts plaintext using CipherState (with h as AD), appends to dst, then mixes ciphertext into hash.
Critical Detail: Mixes the NEW ciphertext (not entire dst) into hash.
void DecryptAndHash(
std::vector<std::uint8_t>& dst,
std::span<const std::uint8_t> ciphertext
);Description: Mixes ciphertext into hash BEFORE decryption, then decrypts and appends to dst.
Critical Detail: Uses saved copy of h as AD for decryption (since h is modified before decryption).
auto GetHandshakeHash() const;Returns: Current handshake hash h (as std::vector<std::uint8_t>)
Use Case: Retrieved after handshake completion for channel binding or verification.
std::pair<CipherState, CipherState> Split() const;Description: Derives two transport CipherStates for post-handshake communication.
Operation: (k1, k2) = HKDF(ck, "", 2)
Returns: std::pair<CipherState, CipherState> - (initiator TX, initiator RX) = (responder RX, responder TX)
Use Case: Called after handshake completion to get transport encryption keys.
Defines a complete Noise protocol by combining a handshake pattern with cryptographic primitives. Supports classical, post-quantum (PQ), and hybrid forward secrecy (HFS) protocols.
// Classical: From individual components
Protocol(
HandshakePattern pattern,
std::string_view dh_name,
std::string_view cipher_name,
std::string_view hash_name
);
// HFS: With both DH and KEM
Protocol(
HandshakePattern pattern,
std::string_view dh_name,
std::string_view kem_name,
std::string_view cipher_name,
std::string_view hash_name
);
// From protocol string (auto-detects classical, PQ, or HFS)
explicit Protocol(const std::string_view& pr_string);
// Classical Examples
Protocol proto1(XX, X25519, ChaChaPoly, SHA256);
Protocol proto2("Noise_XX_25519_ChaChaPoly_SHA256");
// Post-Quantum Examples
Protocol proto3("Noise_pqXX_M768_ChaChaPoly_SHA256");
// HFS Examples (DH + KEM hybrid)
Protocol proto4("Noise_XXhfs_25519+M768_ChaChaPoly_SHA256");Protocol String Formats:
- Classical:
Noise_PATTERN_DH_CIPHER_HASH - Post-Quantum:
Noise_pqPATTERN_KEM_CIPHER_HASH - HFS:
Noise_PATTERNhfs_DH+KEM_CIPHER_HASH
Examples:
"Noise_XX_25519_ChaChaPoly_SHA256"- Classical XX"Noise_IK_448_AESGCM_BLAKE2b"- Classical IK with X448"Noise_NNpsk0_25519_ChaChaPoly_SHA512"- PSK pattern"Noise_pqXX_M768_ChaChaPoly_BLAKE2s"- Post-Quantum pqXX"Noise_XXhfs_25519+M768_ChaChaPoly_SHA256"- HFS with X25519 + ML-KEM-768"Noise_XXhfs_448+M1024_AESGCM_SHA512"- HFS with X448 + ML-KEM-1024
const HandshakePattern pattern; // Handshake pattern (XX, pqXX, XXhfs, etc.)
const Dh dh; // DH algorithm (X25519 or X448)
const std::optional<Dh> kem; // KEM algorithm (M512, M768, M1024) - for HFS only
const Cipher cipher; // AEAD cipher (ChaCha20-Poly1305 or AES-GCM)
const Hash hash; // Hash function (SHA-256, SHA-512, BLAKE2b, BLAKE2s)Note: All members are const and immutable after construction, ensuring protocol parameters cannot be changed.
std::string String() const;Returns: Full protocol name in Noise specification format.
Examples:
"Noise_XX_25519_ChaChaPoly_SHA256""Noise_pqXX_M768_ChaChaPoly_SHA256""Noise_XXhfs_25519+M768_ChaChaPoly_SHA256"
Use Case: For logging, debugging, or protocol negotiation.
bool IsHFS() const;Returns: true if this protocol uses Hybrid Forward Secrecy (has both DH and KEM).
Use Case: To check if the protocol requires both DH and KEM keys.
To access pattern details:
Protocol proto("Noise_XX_25519_ChaChaPoly_SHA256");
// Get pattern name
std::string patternName = proto.pattern.String(); // "XX"
// Check if pattern has PSK
bool hasPsk = proto.pattern.numPSKs > 0;
// Get message patterns
auto& messages = proto.pattern.messages;See Pattern section for HandshakePattern API.
Defines handshake patterns and message flows according to the Noise Protocol specification.
NoisePQC++ implements all Noise patterns including PQ and HFS:
- 3 one-way patterns (N, K, X)
- 12 fundamental interactive patterns (NN, NK, NX, etc.)
- 21 deferred patterns (with "1" suffix)
- 21 PSK patterns (with "psk" modifier)
- 13 Post-Quantum patterns (pqN, pqNN, pqNK, pqXX, etc.)
- 12+ HFS patterns (NNhfs, NKhfs, XXhfs, etc.)
For complete pattern details, see PatternGuide.md.
struct HandshakePattern {
std::string m_patternName; // Pattern name, e.g., "XX", "pqXX", "XXhfs"
std::vector<Token> pre_initiator; // Initiator's pre-message tokens
std::vector<Token> pre_responder; // Responder's pre-message tokens
std::vector<MessagePattern> messages; // Ordered handshake message sequences
bool isOneWay{false}; // True for one-way patterns (N, K, X)
std::uint8_t numPSKs{}; // Number of pre-shared keys required
[[nodiscard]] std::string_view String() const;
void describe(std::ostream& os) const;
};enum class Token : std::uint8_t {
Token_invalid, // Invalid/sentinel token
// Classical DH tokens
Token_e, // Generate and send ephemeral public key
Token_s, // Send static public key (encrypted if key established)
Token_ee, // DH(e, re) — ephemeral-ephemeral
Token_es, // DH(e, rs) or DH(s, re)
Token_se, // DH(s, re) or DH(e, rs)
Token_ss, // DH(s, rs) — static-static
// PQNoise KEM tokens
Token_ekem, // KEM encapsulation to peer's ephemeral key (ML-KEM)
Token_skem, // KEM encapsulation to peer's static key (ML-KEM)
// HFS tokens
Token_e1, // Generate and send KEM ephemeral public key
Token_ekem1, // KEM encapsulation to peer's e1 key
Token_psk // Mix pre-shared symmetric key
};struct MessagePattern {
std::vector<Token> tokens;
};One-Way Patterns:
N Pattern (pre-message: <- s):
-> e, es
K Pattern (pre-messages: -> s, <- s):
-> e, es, ss
X Pattern (pre-message: <- s):
-> e, es, s, ss
Two-Message Interactive Patterns:
NN Pattern:
-> e
<- e, ee
NK Pattern (pre-message: <- s):
-> e, es
<- e, ee
NX Pattern:
-> e
<- e, ee, s, es
KN Pattern (pre-message: -> s):
-> e
<- e, ee, se
KK Pattern (pre-messages: -> s, <- s):
-> e, es, ss
<- e, ee, se
IN Pattern:
-> e, s
<- e, ee, se
IK Pattern (pre-message: <- s):
-> e, es, s, ss
<- e, ee, se
IX Pattern:
-> e, s
<- e, ee, se, s, es
Three-Message Interactive Patterns:
XN Pattern:
-> e
<- e, ee
-> s, se
XK Pattern (pre-message: <- s):
-> e, es
<- e, ee
-> s, se
XX Pattern:
-> e
<- e, ee, s, es
-> s, se
Manages the complete Noise handshake state machine.
Configuration for initializing a handshake.
class HandshakeConfig {
public:
const Protocol& protocol; // Protocol specification
bool isInitiator = false; // True if initiator, false if responder
std::vector<std::uint8_t> prologue = {}; // Optional prologue data
KeyPair localStatic = {}; // Local static keypair (if needed)
KeyPair localEphemeral = {}; // Optional pre-generated ephemeral keypair
PublicKey remoteStatic = {}; // Remote static key (if known)
PublicKey remoteEphemeral = {}; // Remote ephemeral key (if known)
std::vector<std::array<std::uint8_t, SYMMETRIC_KEY_SIZE>> preSharedKeys = {}; // PSKs
HandshakeObserver* observer = nullptr; // Optional key validation observer
int maxMessageSize = 0; // 0 = NOISE_MAX_MESSAGE_SIZE (65535),
// positive = custom limit,
// negative = no limit
[[nodiscard]] int GetMaxMessageSize() const;
};Fields:
protocol: Reference to the protocol specification (pattern, DH, cipher, hash)isInitiator: Role in handshake (true = initiator, false = responder)prologue: Optional application-specific data mixed into handshake hashlocalStatic: Your long-term static keypair (required for patterns usings)localEphemeral: Optional pre-generated ephemeral keypair (auto-generated if empty)remoteStatic: Peer's static public key (required for patterns with pre-message<- sor-> s)remoteEphemeral: Peer's ephemeral public key (rarely pre-known)preSharedKeys: Array of 32-byte PSKs for PSK patterns (must match pattern'snumPSKs)observer: Optional callback for validating peer public keys as they're receivedmaxMessageSize: Message size limit (0 uses default 65535 bytes)
Notes:
- PSK validation: Number of PSKs must exactly match
protocol.pattern.numPSKs - Each PSK must be exactly
SYMMETRIC_KEY_SIZE(32) bytes - Missing required keys will throw
MissingKeyErrorduring handshake
Current status of the handshake.
class HandshakeStatus {
public:
// Error status (nullptr = in progress, HandshakeDoneError = complete, other = failure)
std::exception_ptr err{nullptr};
// Public keys discovered during handshake
PublicKey localEphemeral{};
PublicKey remoteStatic{};
PublicKey remoteEphemeral{};
// Resulting cipher states after completion (cs1, cs2)
std::optional<std::pair<CipherState, CipherState>> cipherStates{};
// Handshake hash (set on completion)
std::vector<std::uint8_t> handshakeHash{};
[[nodiscard]] bool IsComplete() const;
[[nodiscard]] bool HasError() const;
};Fields:
err: Exception pointer indicating handshake state:nullptr= handshake in progressHandshakeDoneError= handshake completed successfully- Any other exception = handshake failed
localEphemeral: Your ephemeral public key (set after first message withetoken)remoteStatic: Peer's static public key (set when received or from config)remoteEphemeral: Peer's ephemeral public key (set when received)cipherStates: Transport cipher states after completion (cs1,cs2)- Initiator:
cs1for sending,cs2for receiving - Responder:
cs1for receiving,cs2for sending - One-way patterns: Only
cs1is valid (cs2 is cleared)
- Initiator:
handshakeHash: Final handshake hash (available after completion)
Methods:
IsComplete(): Returnstrueif handshake completed successfully (err is HandshakeDoneError)HasError(): Returnstrueif handshake failed with an error (err is not nullptr and not HandshakeDoneError)
Optional observer interface for validating peer public keys as they are received during the handshake.
class HandshakeObserver {
public:
virtual ~HandshakeObserver() = default;
// Called when a public key is received from the peer
// Returns std::nullopt to accept, or error string to reject and abort
virtual std::optional<std::string> OnPeerPublicKey(
Token token,
const PublicKey& pubKey
) = 0;
};Purpose: Allows real-time validation of peer public keys during handshake.
Callback Parameters:
token: The token type (Token::Token_efor ephemeral,Token::Token_sfor static)pubKey: The peer's public key being received
Return Value:
std::nullopt: Accept the key, continue handshakestd::optional<std::string>with error message: Reject the key, abort handshake
Use Cases:
- Key pinning (verify against known trusted keys)
- Certificate/credential validation
- Rate limiting based on peer identity
- Real-time blocklisting
explicit HandshakeState(const HandshakeConfig& cfg);Parameters:
cfg: Handshake configuration (protocol, role, keys, observer, etc.)
Throws:
MissingPSKError: If number of PSKs doesn't matchprotocol.pattern.numPSKsBadPSKError: If any PSK is not exactlySYMMETRIC_KEY_SIZE(32) bytesPreMessageError: If required pre-message keys are missing
Initialization Steps:
- Validates PSK count and size
- Initializes symmetric state with protocol name
- Mixes prologue into handshake hash
- Processes pre-message patterns (if any)
- Sets initial handshake status with known remote keys
Notes:
- HandshakeState is non-copyable and non-movable
- Stores reference to config - config must outlive HandshakeState
- Automatically handles pre-messages based on pattern
std::vector<std::uint8_t> WriteMessage(std::span<const std::uint8_t> payload);Purpose: Processes a write step of the handshake protocol.
Parameters:
payload: Application payload to include in this handshake message (can be empty)
Returns: Complete handshake message (tokens + encrypted payload)
Throws:
OutOfOrderError: If called when it's the peer's turn to writeMessageSizeError: If message exceedsmaxMessageSizeMissingKeyError: If required key (e.g., static) is not availableInvalidTokenError: If pattern contains invalid tokenHandshakeDoneError: When handshake completes (after returning final message)- Any error from observer's
OnPeerPublicKey(if observer returns error)
Behavior:
- Checks for existing errors or out-of-order calls
- Processes pattern tokens (e, s, ee, es, se, ss, psk) for current message
- Auto-generates ephemeral key if needed (first
etoken) - Encrypts payload using
SymmetricState.EncryptAndHash - If last message in pattern, completes handshake:
- Splits symmetric state into
cs1andcs2 - Sets
cipherStatesin status - Sets
handshakeHashin status - Clears sensitive data via
Reset() - Throws
HandshakeDoneError(after returning message)
- Splits symmetric state into
Notes:
- Call alternates with
ReadMessagebased on pattern - Initiator always writes first message (for non-one-way patterns)
- After completion, use
GetStatus().cipherStatesfor transport encryption
std::vector<std::uint8_t> ReadMessage(std::span<const std::uint8_t> message);Purpose: Processes a read step of the handshake protocol.
Parameters:
message: Complete handshake message received from peer
Returns: Decrypted application payload from the message
Throws:
OutOfOrderError: If called when it's your turn to writeMessageSizeError: If message exceedsmaxMessageSizeTruncatedMessageError: If message is too short for expected tokensDecryptError: If authentication fails duringDecryptAndHashNoiseError: If observer rejects a peer public keyHandshakeDoneError: When handshake completes (after returning payload)
Behavior:
- Checks message size and turn order
- Processes pattern tokens for current message:
- Reads
etoken: Extracts peer ephemeral key, calls observer if present - Reads
stoken: Decrypts and extracts peer static key, calls observer if present - Processes DH tokens (ee, es, se, ss)
- Processes PSK tokens if applicable
- Reads
- Decrypts payload using
SymmetricState.DecryptAndHash - If last message in pattern, completes handshake (same as WriteMessage)
Notes:
- Peer public keys are available in status immediately after extraction
- Observer is called synchronously before continuing handshake
- Authentication failure throws
DecryptError(MAC verification failed)
[[nodiscard]] const HandshakeStatus& GetStatus() const;Purpose: Returns current handshake status.
Returns: Reference to HandshakeStatus containing:
- Error state (
err) - Public keys discovered during handshake
- Cipher states (if complete)
- Handshake hash (if complete)
Use Cases:
- Check completion:
GetStatus().IsComplete() - Check for errors:
GetStatus().HasError() - Access peer keys:
GetStatus().remoteStatic - Get transport ciphers:
GetStatus().cipherStates
[[nodiscard]] const SymmetricState& GetSymmetricState() const;Purpose: Returns reference to internal symmetric state.
Returns: Const reference to the SymmetricState managing handshake encryption
Use Cases:
- Advanced debugging and logging
- Accessing intermediate handshake hash (
ss.h) - Inspecting chaining key (
ss.ck) - Understanding state machine progression
Note: For normal use, access final handshake hash via GetStatus().handshakeHash after completion.
[[nodiscard]] const HandshakeConfig& GetConfig() const;Purpose: Returns reference to the configuration used to create this handshake.
Returns: Const reference to HandshakeConfig
[[nodiscard]] bool IsInitiator() const;Purpose: Returns the role in this handshake.
Returns: true if initiator, false if responder
void Reset();Purpose: Clears all sensitive handshake state (automatically called on completion).
Behavior:
- Clears symmetric state (chaining key, handshake hash)
- Securely erases ephemeral private key (if generated)
- Clears cipher state
Note: Typically not called manually - automatically invoked on handshake completion or destruction.
// Setup
Protocol protocol(XX, X25519, ChaChaPoly, SHA256);
auto aliceStatic = protocol.dh.GenerateKeypair();
auto bobStatic = protocol.dh.GenerateKeypair();
// Alice (initiator)
HandshakeConfig aliceCfg{
.protocol = protocol,
.isInitiator = true,
.localStatic = aliceStatic
};
HandshakeState aliceHs(aliceCfg);
// Bob (responder)
HandshakeConfig bobCfg{
.protocol = protocol,
.isInitiator = false,
.localStatic = bobStatic
};
HandshakeState bobHs(bobCfg);
// Handshake flow (XX pattern)
// Message 1: Alice -> Bob
auto msg1 = aliceHs.WriteMessage({'h','e','l','l','o'});
auto recv1 = bobHs.ReadMessage(msg1);
// Message 2: Bob -> Alice
auto msg2 = bobHs.WriteMessage({});
auto recv2 = aliceHs.ReadMessage(msg2);
// Message 3: Alice -> Bob (completes handshake)
auto msg3 = aliceHs.WriteMessage({});
auto recv3 = bobHs.ReadMessage(msg3);
// Check completion
if (aliceHs.GetStatus().IsComplete() && bobHs.GetStatus().IsComplete()) {
// Get transport cipher states
auto [aliceTx, aliceRx] = *aliceHs.GetStatus().cipherStates;
auto [bobRx, bobTx] = *bobHs.GetStatus().cipherStates;
// Use for post-handshake encryption
auto ciphertext = aliceTx.EncryptWithAd({}, plaintext);
auto decrypted = bobRx.DecryptWithAd({}, ciphertext);
}import noise;
using namespace Noise;
// Create cipher and state
Cipher cipher(ChaChaPoly);
CipherState state(std::move(cipher), 65535);
// Initialize key
std::array<std::uint8_t, 32> key = {/* ... */};
state.InitializeKey(key);
// Encrypt
std::vector<std::uint8_t> plaintext = {0x48, 0x65, 0x6c, 0x6c, 0x6f};
std::vector<std::uint8_t> ad;
auto ciphertext = state.EncryptWithAd(ad, plaintext);
// Decrypt
auto decrypted = state.DecryptWithAd(ad, ciphertext);Dh dh(X25519);
// Alice generates keypair
auto alice_kp = dh.GenerateKeypair();
// Bob generates keypair
auto bob_kp = dh.GenerateKeypair();
// Both compute shared secret
auto alice_shared = dh.DH(alice_kp.private_key, bob_kp.public_key);
auto bob_shared = dh.DH(bob_kp.private_key, alice_kp.public_key);
// alice_shared == bob_shared// Initialize symmetric states (cipher/hash passed by const reference)
Cipher cipher(ChaChaPoly);
Hash hash(SHA256);
SymmetricState init_ss(cipher, hash);
SymmetricState resp_ss(cipher, hash);
// Initialize with protocol name as a byte span
std::string protocol = "Noise_XX_25519_ChaChaPoly_SHA256";
std::span<const std::uint8_t> protoBytes(
reinterpret_cast<const std::uint8_t*>(protocol.data()), protocol.size());
init_ss.InitializeSymmetric(protoBytes);
resp_ss.InitializeSymmetric(protoBytes);
// Perform handshake...
// (See tests/test_symmetricstate.cpp for complete example)
// Split into transport keys
auto [init_c1, init_c2] = init_ss.Split();
auto [resp_c1, resp_c2] = resp_ss.Split();
// Now use init_c1 ↔ resp_c2 and init_c2 ↔ resp_c1import noise;
using namespace Noise;
// Create PSK pattern (NNpsk0) — PSK pattern names are looked up via the registry,
// so use the string-based constructor.
Protocol protocol("Noise_NNpsk0_25519_ChaChaPoly_SHA256");
// Generate pre-shared key (32 bytes)
std::array<std::uint8_t, SYMMETRIC_KEY_SIZE> psk{};
// ... fill PSK from secure source (e.g., previous handshake, secure channel)
// Alice (initiator)
HandshakeConfig aliceCfg{
.protocol = protocol,
.isInitiator = true,
.preSharedKeys = {psk} // Vector of PSKs
};
HandshakeState aliceHs(aliceCfg);
// Bob (responder)
HandshakeConfig bobCfg{
.protocol = protocol,
.isInitiator = false,
.preSharedKeys = {psk} // Must match Alice's PSK
};
HandshakeState bobHs(bobCfg);
// Execute handshake (NNpsk0 is 2 messages)
auto msg1 = aliceHs.WriteMessage({});
bobHs.ReadMessage(msg1);
auto msg2 = bobHs.WriteMessage({});
aliceHs.ReadMessage(msg2);
// Both sides now have matching transport keysimport noise;
using namespace Noise;
// Observer implementation for key pinning
class KeyPinningObserver : public HandshakeObserver {
public:
KeyPinningObserver(const PublicKey& trustedKey)
: m_trustedKey(trustedKey) {}
std::optional<std::string> OnPeerPublicKey(
Token token,
const PublicKey& pubKey
) override {
// Only validate static keys, not ephemeral
if (token != Token::Token_s) {
return std::nullopt; // Accept ephemeral keys
}
// Verify against pinned key
if (pubKey != m_trustedKey) {
return "Key pinning failed: untrusted static key";
}
return std::nullopt; // Accept trusted key
}
private:
PublicKey m_trustedKey;
};
// Usage
Protocol protocol(IK, X25519, ChaChaPoly, SHA256);
auto aliceStatic = protocol.dh.GenerateKeypair();
auto bobStatic = protocol.dh.GenerateKeypair();
// Alice knows Bob's static key and pins it
KeyPinningObserver aliceObserver(bobStatic.public_key);
HandshakeConfig aliceCfg{
.protocol = protocol,
.isInitiator = true,
.localStatic = aliceStatic,
.remoteStatic = bobStatic.public_key, // Pre-message
.observer = &aliceObserver // Will validate Bob's key
};
HandshakeState aliceHs(aliceCfg);
// Bob's side (no observer needed)
HandshakeConfig bobCfg{
.protocol = protocol,
.isInitiator = false,
.localStatic = bobStatic
};
HandshakeState bobHs(bobCfg);
// Execute IK handshake
try {
auto msg1 = aliceHs.WriteMessage({});
bobHs.ReadMessage(msg1);
auto msg2 = bobHs.WriteMessage({});
aliceHs.ReadMessage(msg2); // Observer validates here
} catch (const NoiseError& e) {
// Key pinning failed
std::println("Handshake failed: {}", e.what());
}import noise;
using namespace Noise;
// One-way pattern (N pattern)
Protocol protocol(N, X25519, ChaChaPoly, SHA256);
// Bob (responder) has static key known to Alice
auto bobStatic = protocol.dh.GenerateKeypair();
// Alice (initiator) sends single message to Bob
HandshakeConfig aliceCfg{
.protocol = protocol,
.isInitiator = true,
.remoteStatic = bobStatic.public_key // Pre-message: <- s
};
HandshakeState aliceHs(aliceCfg);
// Bob (responder) receives message
HandshakeConfig bobCfg{
.protocol = protocol,
.isInitiator = false,
.localStatic = bobStatic
};
HandshakeState bobHs(bobCfg);
// Single message completes handshake
auto msg = aliceHs.WriteMessage({'h','e','l','l','o'});
auto payload = bobHs.ReadMessage(msg);
// Only one transport key is valid (cs1)
auto [aliceTx, _] = *aliceHs.GetStatus().cipherStates;
auto [bobRx, __] = *bobHs.GetStatus().cipherStates;
// Alice can send encrypted messages to Bob
auto encrypted = aliceTx.EncryptWithAd({}, {'d','a','t','a'});
auto decrypted = bobRx.DecryptWithAd({}, encrypted);
// Note: Bob cannot send encrypted messages back (one-way)import noise;
using namespace Noise;
// Parse from string
Protocol protocol1("Noise_XX_25519_ChaChaPoly_SHA256");
std::println("{}", protocol1.String()); // Noise_XX_25519_ChaChaPoly_SHA256
// Equivalent manual construction
Protocol protocol2(XX, X25519, ChaChaPoly, SHA256);
// Access components
std::println("Pattern: {}", protocol1.pattern.String()); // XX
std::println("DH: X25519, Size: {}", protocol1.dh.Size()); // 32
std::println("Cipher: ChaChaPoly");
std::println("Hash: SHA256, Output size: {}", protocol1.hash.Size()); // 32NoisePQC++ uses exceptions for error reporting. All NoisePQC++-specific exceptions derive from NoiseError, which extends std::runtime_error.
Base Exception:
struct NoiseError : std::runtime_error;Handshake Errors:
HandshakeDoneError: Thrown when handshake completes successfully (signals completion)OutOfOrderError: CalledWriteMessagewhen it's peer's turn, or vice versaMessageSizeError: Message exceeds configuredmaxMessageSizeTruncatedMessageError: Message too short for expected tokensMissingKeyError: Required key (static or ephemeral) not availableInvalidTokenError: Pattern contains unsupported or invalid token
Configuration Errors:
MissingPSKError: Number of PSKs doesn't matchpattern.numPSKsBadPSKError: PSK is not exactly 32 bytesPreMessageError: Required pre-message key missing (e.g.,localStaticfor pattern with-> s)ProtocolNotSupportedError: Invalid protocol string or unsupported algorithm combination
Botan Exceptions:
In addition to NoisePQC++ exceptions, Botan may throw:
Botan::Invalid_Authentication_Tag: AEAD decryption failed (MAC verification failed)Botan::Invalid_Key_Length: Key size doesn't match algorithm requirementsBotan::Lookup_Error: Algorithm not available in Botan buildBotan::Exception: Base class for all Botan exceptions
Basic Exception Handling:
try {
auto msg = aliceHs.WriteMessage(payload);
auto response = bobHs.ReadMessage(msg);
} catch (const HandshakeDoneError&) {
// Expected - handshake completed successfully
auto [tx, rx] = *aliceHs.GetStatus().cipherStates;
} catch (const OutOfOrderError& e) {
std::println("Protocol error: {}", e.what());
} catch (const Botan::Invalid_Authentication_Tag& e) {
std::println("Authentication failed: {}", e.what());
} catch (const NoiseError& e) {
std::println("Noise error: {}", e.what());
} catch (const std::exception& e) {
std::println("Unexpected error: {}", e.what());
}Checking Status Instead of Catching:
auto msg = aliceHs.WriteMessage(payload);
if (aliceHs.GetStatus().IsComplete()) {
// Handshake complete - get cipher states
auto [tx, rx] = *aliceHs.GetStatus().cipherStates;
} else if (aliceHs.GetStatus().HasError()) {
// Error occurred
try {
std::rethrow_exception(aliceHs.GetStatus().err);
} catch (const std::exception& e) {
std::println("Error: {}", e.what());
}
}Observer Rejection:
class ValidatingObserver : public HandshakeObserver {
std::optional<std::string> OnPeerPublicKey(
Token token,
const PublicKey& pubKey
) override {
if (!IsKeyTrusted(pubKey)) {
return "Untrusted key"; // Causes NoiseError to be thrown
}
return std::nullopt; // Accept
}
};
try {
auto msg = bobHs.ReadMessage(incomingMsg);
} catch (const NoiseError& e) {
// Observer rejected the key
std::println("Key validation failed: {}", e.what());
}NoisePQC++ is not thread-safe. Each state object should be used by a single thread.
- Pattern Guide - Complete guide to all 57 Noise patterns with selection guide and use cases
- Architecture Documentation - System architecture and design principles
- Build Guide - Platform-specific build instructions and testing
- Logging and Debugging Guide - Debug builds and handshake state logging
- Noise Protocol Specification - Official specification