OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / noise_handshake.cpp

461 lines · cpp ref:
#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);
}