#include "noise_handshake.h"
#include <sodium.h>
#include "debug_log.h"
#include <cstring>
// Protocol name for Noise_XX_25519_ChaChaPoly_BLAKE2b
static const char *PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_BLAKE2b";
// ---- Construction / Destruction ----
NoiseHandshake::NoiseHandshake(bool is_initiator, const NoiseKeypair &static_key)
: _initiator(is_initiator), _message_index(0), _complete(false),
_e_generated(false), _has_key(false), _nonce(0) {
// Ensure sodium is initialised.
if (sodium_init() < 0) {
DHT_LOG("NoiseHandshake: sodium_init failed\n");
}
memcpy(&_s, &static_key, sizeof(NoiseKeypair));
memset(_rs, 0, PKLEN);
memset(_re, 0, PKLEN);
memset(&_e, 0, sizeof(NoiseKeypair));
memset(_cipher_key, 0, KEYBYTES);
memset(_tx, 0, KEYBYTES);
memset(_rx, 0, KEYBYTES);
memset(_hash, 0, HASHLEN);
// Initialise symmetric state.
// If protocol name fits in HASHLEN, pad with zeros; else hash it.
size_t proto_len = strlen(PROTOCOL_NAME);
memset(_digest, 0, HASHLEN);
if (proto_len <= HASHLEN) {
memcpy(_digest, PROTOCOL_NAME, proto_len);
} else {
crypto_generichash(_digest, HASHLEN,
(const uint8_t *)PROTOCOL_NAME, proto_len,
nullptr, 0);
}
// chainingKey = digest (copy)
memcpy(_chaining_key, _digest, HASHLEN);
// mixHash(prologue) — empty prologue for standard usage.
uint8_t empty = 0;
_mix_hash(&empty, 0);
}
NoiseHandshake::~NoiseHandshake() {
sodium_memzero(&_s, sizeof(NoiseKeypair));
sodium_memzero(&_e, sizeof(NoiseKeypair));
sodium_memzero(_cipher_key, KEYBYTES);
sodium_memzero(_tx, KEYBYTES);
sodium_memzero(_rx, KEYBYTES);
sodium_memzero(_digest, HASHLEN);
sodium_memzero(_chaining_key, HASHLEN);
}
// ---- Key Generation ----
NoiseKeypair NoiseHandshake::generate_keypair() {
if (sodium_init() < 0) {
DHT_LOG("NoiseHandshake: sodium_init failed\n");
}
NoiseKeypair kp;
// Generate Curve25519 keypair using crypto_kx (which produces Curve25519 keys).
crypto_kx_keypair(kp.public_key, kp.secret_key);
return kp;
}
// ---- Handshake: Send ----
std::vector<uint8_t> NoiseHandshake::send(const uint8_t *payload, size_t payload_len) {
std::vector<uint8_t> out;
// XX pattern send sequence:
// Initiator sends msg 0 and 2, responder sends msg 1.
int msg = _message_index++;
if (msg == 0 && _initiator) {
// Msg 1: initiator → responder
// Token: e (send ephemeral public key)
// Generate ephemeral keypair.
crypto_kx_keypair(_e.public_key, _e.secret_key);
_e_generated = true;
// mixHash(e.publicKey)
_mix_hash(_e.public_key, PKLEN);
// Write ephemeral public key.
out.insert(out.end(), _e.public_key, _e.public_key + PKLEN);
// Encrypt and hash payload (no key yet, so plaintext).
_encrypt_and_hash(out, payload, payload_len);
} else if (msg == 0 && !_initiator) {
// Msg 2: responder → initiator
// Tokens: e, ee, s, es
// e: generate and send ephemeral.
crypto_kx_keypair(_e.public_key, _e.secret_key);
_e_generated = true;
_mix_hash(_e.public_key, PKLEN);
out.insert(out.end(), _e.public_key, _e.public_key + PKLEN);
// ee: DH(e, re)
_mix_key_from_dh(_re, _e.secret_key);
// s: encrypt and send static public key.
_encrypt_and_hash(out, _s.public_key, PKLEN);
// es: DH(s, re) — responder's static with initiator's ephemeral.
// For responder: es means DH(local_static, remote_ephemeral).
_mix_key_from_dh(_re, _s.secret_key);
// Encrypt and hash payload.
_encrypt_and_hash(out, payload, payload_len);
} else if (msg == 1 && _initiator) {
// Msg 3: initiator → responder
// Tokens: s, se
// s: encrypt and send static public key.
_encrypt_and_hash(out, _s.public_key, PKLEN);
// se: DH(s, re) — initiator's static with responder's ephemeral.
// For initiator: se means DH(local_static, remote_ephemeral).
_mix_key_from_dh(_re, _s.secret_key);
// Encrypt and hash payload.
_encrypt_and_hash(out, payload, payload_len);
// Handshake complete.
_split();
}
return out;
}
// ---- Handshake: Receive ----
std::vector<uint8_t> NoiseHandshake::recv(const uint8_t *data, size_t len) {
size_t offset = 0;
int msg = _message_index++;
if (msg == 0 && !_initiator) {
// Receive Msg 1: initiator → responder
// Token: e (receive ephemeral public key)
if (len < PKLEN) return {};
// e: read remote ephemeral.
memcpy(_re, data + offset, PKLEN);
offset += PKLEN;
_mix_hash(_re, PKLEN);
// Decrypt and hash payload.
return _decrypt_and_hash(data + offset, len - offset);
} else if (msg == 0 && _initiator) {
// Receive Msg 2: responder → initiator
// Tokens: e, ee, s, es
if (len < PKLEN) return {};
// e: read remote ephemeral.
memcpy(_re, data + offset, PKLEN);
offset += PKLEN;
_mix_hash(_re, PKLEN);
// ee: DH(e, re)
_mix_key_from_dh(_re, _e.secret_key);
// s: read encrypted static key.
size_t s_len = _has_key ? (PKLEN + MACBYTES) : PKLEN;
if (offset + s_len > len) return {};
auto s_dec = _decrypt_and_hash(data + offset, s_len);
offset += s_len;
if (s_dec.size() != PKLEN) return {};
memcpy(_rs, s_dec.data(), PKLEN);
// es: DH(e, rs) — initiator's ephemeral with responder's static.
// For initiator: es means DH(local_ephemeral, remote_static).
_mix_key_from_dh(_rs, _e.secret_key);
// Decrypt and hash payload.
return _decrypt_and_hash(data + offset, len - offset);
} else if (msg == 1 && !_initiator) {
// Receive Msg 3: initiator → responder
// Tokens: s, se
// s: read encrypted static key.
size_t s_len = _has_key ? (PKLEN + MACBYTES) : PKLEN;
if (offset + s_len > len) return {};
auto s_dec = _decrypt_and_hash(data + offset, s_len);
offset += s_len;
if (s_dec.size() != PKLEN) return {};
memcpy(_rs, s_dec.data(), PKLEN);
// se: DH(e, rs) — responder's ephemeral with initiator's static.
// For responder: se means DH(local_ephemeral, remote_static).
_mix_key_from_dh(_rs, _e.secret_key);
// Decrypt and hash payload.
auto payload = _decrypt_and_hash(data + offset, len - offset);
// Handshake complete.
_split();
return payload;
}
return {};
}
// ---- Symmetric State Operations ----
void NoiseHandshake::_mix_hash(const uint8_t *data, size_t len) {
// digest = BLAKE2b(digest || data)
uint8_t tmp[HASHLEN];
size_t total = HASHLEN + len;
std::vector<uint8_t> buf(total);
memcpy(buf.data(), _digest, HASHLEN);
if (len > 0) {
memcpy(buf.data() + HASHLEN, data, len);
}
crypto_generichash(tmp, HASHLEN, buf.data(), total, nullptr, 0);
memcpy(_digest, tmp, HASHLEN);
}
void NoiseHandshake::_mix_key_from_dh(const uint8_t *remote_pk, const uint8_t *local_sk) {
uint8_t dh_output[DHLEN];
_dh(dh_output, local_sk, remote_pk);
// HKDF(chainingKey, dh_output) → (new_chaining_key, temp_key)
uint8_t new_ck[HASHLEN];
uint8_t temp_key[HASHLEN];
_hkdf(_chaining_key, HASHLEN, dh_output, DHLEN, new_ck, temp_key);
memcpy(_chaining_key, new_ck, HASHLEN);
_initialise_key(temp_key); // Uses first 32 bytes of temp_key.
sodium_memzero(dh_output, DHLEN);
sodium_memzero(new_ck, HASHLEN);
sodium_memzero(temp_key, HASHLEN);
}
void NoiseHandshake::_mix_key_normal(const uint8_t *key, size_t key_len) {
uint8_t new_ck[HASHLEN];
uint8_t temp_key[HASHLEN];
_hkdf(_chaining_key, HASHLEN, key, key_len, new_ck, temp_key);
memcpy(_chaining_key, new_ck, HASHLEN);
_initialise_key(temp_key);
sodium_memzero(new_ck, HASHLEN);
sodium_memzero(temp_key, HASHLEN);
}
void NoiseHandshake::_initialise_key(const uint8_t *key) {
memcpy(_cipher_key, key, KEYBYTES);
_has_key = true;
_nonce = 0;
}
void NoiseHandshake::_encrypt_and_hash(std::vector<uint8_t> &out,
const uint8_t *pt, size_t pt_len) {
if (!_has_key) {
// No key yet — plaintext pass-through.
out.insert(out.end(), pt, pt + pt_len);
_mix_hash(pt, pt_len);
} else {
// Encrypt with AEAD, using digest as AD.
size_t ct_len = pt_len + MACBYTES;
size_t old_size = out.size();
out.resize(old_size + ct_len);
_encrypt(out.data() + old_size, pt, pt_len, _digest, HASHLEN);
// Mix the ciphertext into hash.
_mix_hash(out.data() + old_size, ct_len);
}
}
std::vector<uint8_t> NoiseHandshake::_decrypt_and_hash(const uint8_t *ct, size_t ct_len) {
if (!_has_key) {
// No key — plaintext pass-through.
std::vector<uint8_t> pt(ct, ct + ct_len);
_mix_hash(ct, ct_len);
return pt;
} else {
if (ct_len < MACBYTES) return {};
size_t pt_len = ct_len - MACBYTES;
std::vector<uint8_t> pt(pt_len);
if (!_decrypt(pt.data(), ct, ct_len, _digest, HASHLEN)) {
return {};
}
// Mix the ciphertext into hash.
_mix_hash(ct, ct_len);
return pt;
}
}
// ---- AEAD (ChaCha20-Poly1305 IETF) ----
void NoiseHandshake::_encrypt(uint8_t *ct, const uint8_t *pt, size_t pt_len,
const uint8_t *ad, size_t ad_len) {
// Nonce: 12 bytes, counter in bytes 4-7 (little-endian), matching JS impl.
uint8_t nonce[12];
memset(nonce, 0, 12);
uint32_t counter = static_cast<uint32_t>(_nonce);
memcpy(nonce + 4, &counter, 4); // LE on x86
unsigned long long ct_len_out;
crypto_aead_chacha20poly1305_ietf_encrypt(
ct, &ct_len_out,
pt, pt_len,
ad, ad_len,
nullptr, nonce, _cipher_key);
_nonce++;
}
bool NoiseHandshake::_decrypt(uint8_t *pt, const uint8_t *ct, size_t ct_len,
const uint8_t *ad, size_t ad_len) {
uint8_t nonce[12];
memset(nonce, 0, 12);
uint32_t counter = static_cast<uint32_t>(_nonce);
memcpy(nonce + 4, &counter, 4);
unsigned long long pt_len_out;
int ret = crypto_aead_chacha20poly1305_ietf_decrypt(
pt, &pt_len_out,
nullptr,
ct, ct_len,
ad, ad_len,
nonce, _cipher_key);
_nonce++;
return ret == 0;
}
// ---- Diffie-Hellman (Curve25519) ----
void NoiseHandshake::_dh(uint8_t *output, const uint8_t *local_sk, const uint8_t *remote_pk) {
crypto_scalarmult(output, local_sk, remote_pk);
}
// ---- HKDF (HMAC-BLAKE2b based) ----
void NoiseHandshake::_hmac(uint8_t *out, const uint8_t *key, size_t key_len,
const uint8_t *data, size_t data_len) {
// HMAC-BLAKE2b matching the JS implementation.
uint8_t hmac_key[BLOCKLEN];
memset(hmac_key, 0, BLOCKLEN);
if (key_len > BLOCKLEN) {
crypto_generichash(hmac_key, HASHLEN, key, key_len, nullptr, 0);
} else {
memcpy(hmac_key, key, key_len);
}
uint8_t outer_pad[BLOCKLEN];
uint8_t inner_pad[BLOCKLEN];
for (int i = 0; i < BLOCKLEN; i++) {
outer_pad[i] = 0x5c ^ hmac_key[i];
inner_pad[i] = 0x36 ^ hmac_key[i];
}
sodium_memzero(hmac_key, BLOCKLEN);
// Inner hash: BLAKE2b(inner_pad || data)
uint8_t inner_hash[HASHLEN];
{
size_t inner_len = BLOCKLEN + data_len;
std::vector<uint8_t> inner_buf(inner_len);
memcpy(inner_buf.data(), inner_pad, BLOCKLEN);
if (data_len > 0) {
memcpy(inner_buf.data() + BLOCKLEN, data, data_len);
}
crypto_generichash(inner_hash, HASHLEN, inner_buf.data(), inner_len, nullptr, 0);
}
sodium_memzero(inner_pad, BLOCKLEN);
// Outer hash: BLAKE2b(outer_pad || inner_hash)
{
uint8_t outer_buf[BLOCKLEN + HASHLEN];
memcpy(outer_buf, outer_pad, BLOCKLEN);
memcpy(outer_buf + BLOCKLEN, inner_hash, HASHLEN);
crypto_generichash(out, HASHLEN, outer_buf, BLOCKLEN + HASHLEN, nullptr, 0);
sodium_memzero(outer_buf, BLOCKLEN + HASHLEN);
}
sodium_memzero(outer_pad, BLOCKLEN);
sodium_memzero(inner_hash, HASHLEN);
}
void NoiseHandshake::_hkdf(const uint8_t *salt, size_t salt_len,
const uint8_t *ikm, size_t ikm_len,
uint8_t *out1, uint8_t *out2, uint8_t *out3) {
// HKDF-Extract: PRK = HMAC(salt, ikm)
uint8_t prk[HASHLEN];
_hmac(prk, salt, salt_len, ikm, ikm_len);
// HKDF-Expand: output blocks.
// T1 = HMAC(PRK, 0x01)
uint8_t counter;
counter = 1;
_hmac(out1, prk, HASHLEN, &counter, 1);
// T2 = HMAC(PRK, T1 || 0x02)
{
uint8_t t2_input[HASHLEN + 1];
memcpy(t2_input, out1, HASHLEN);
t2_input[HASHLEN] = 2;
_hmac(out2, prk, HASHLEN, t2_input, HASHLEN + 1);
}
if (out3) {
// T3 = HMAC(PRK, T2 || 0x03)
uint8_t t3_input[HASHLEN + 1];
memcpy(t3_input, out2, HASHLEN);
t3_input[HASHLEN] = 3;
_hmac(out3, prk, HASHLEN, t3_input, HASHLEN + 1);
}
sodium_memzero(prk, HASHLEN);
}
// ---- Split (derive tx/rx keys) ----
void NoiseHandshake::_split() {
uint8_t k1[HASHLEN];
uint8_t k2[HASHLEN];
uint8_t empty = 0;
_hkdf(_chaining_key, HASHLEN, &empty, 0, k1, k2);
if (_initiator) {
memcpy(_tx, k1, KEYBYTES);
memcpy(_rx, k2, KEYBYTES);
} else {
memcpy(_tx, k2, KEYBYTES);
memcpy(_rx, k1, KEYBYTES);
}
// Save handshake hash.
memcpy(_hash, _digest, HASHLEN);
_complete = true;
// Clear sensitive ephemeral state.
sodium_memzero(&_e, sizeof(NoiseKeypair));
sodium_memzero(_re, PKLEN);
sodium_memzero(_cipher_key, KEYBYTES);
sodium_memzero(_chaining_key, HASHLEN);
sodium_memzero(k1, HASHLEN);
sodium_memzero(k2, HASHLEN);
}