OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / noise_ik.cpp

448 lines · cpp ref:
#include "noise_ik.h"

#include <sodium.h>
#include "debug_log.h"

// Protocol name for Noise_IK_Ed25519_ChaChaPoly_BLAKE2b
static const char *PROTOCOL_NAME = "Noise_IK_Ed25519_ChaChaPoly_BLAKE2b";

// ---- Construction / Destruction ----

NoiseIK::NoiseIK(bool initiator, const Ed25519KeyPair &static_key,
				 const uint8_t *prologue, const uint8_t *remote_pk)
	: _initiator(initiator), _message_index(0), _complete(false),
	  _e_generated(false), _has_key(false), _nonce(0) {

	if (sodium_init() < 0) {
		DHT_LOG("NoiseIK: sodium_init failed\n");
	}

	memcpy(&_s, &static_key, sizeof(Ed25519KeyPair));
	memset(_rs, 0, PKLEN);
	memset(_re, 0, PKLEN);
	memset(&_e, 0, sizeof(Ed25519KeyPair));
	memset(_cipher_key, 0, KEYBYTES);
	memset(_tx, 0, KEYBYTES);
	memset(_rx, 0, KEYBYTES);
	memset(_hash, 0, HASHLEN);

	// Initialise symmetric state with protocol name.
	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.
	memcpy(_chaining_key, _digest, HASHLEN);

	// mixHash(prologue).
	if (prologue) {
		_mix_hash(prologue, 32);
	} else {
		uint8_t empty = 0;
		_mix_hash(&empty, 0);
	}

	// IK preshare: mix the responder's static key into hash.
	if (_initiator) {
		// Initiator knows the remote (responder) public key.
		if (remote_pk) {
			memcpy(_rs, remote_pk, PKLEN);
			_mix_hash(_rs, PKLEN);
		}
	} else {
		// Responder preshares its own static key.
		_mix_hash(_s.public_key, PKLEN);
	}
}

NoiseIK::~NoiseIK() {
	sodium_memzero(&_s, sizeof(Ed25519KeyPair));
	sodium_memzero(&_e, sizeof(Ed25519KeyPair));
	sodium_memzero(_cipher_key, KEYBYTES);
	sodium_memzero(_tx, KEYBYTES);
	sodium_memzero(_rx, KEYBYTES);
	sodium_memzero(_digest, HASHLEN);
	sodium_memzero(_chaining_key, HASHLEN);
}

// ---- Key generation ----

Ed25519KeyPair NoiseIK::generate_keypair() {
	if (sodium_init() < 0) {
		DHT_LOG("NoiseIK: sodium_init failed\n");
	}
	Ed25519KeyPair kp;
	crypto_sign_keypair(kp.public_key, kp.secret_key);
	return kp;
}

void NoiseIK::compute_namespace(const char *ns_name, const uint8_t *ids,
								int count, uint8_t *out) {
	// ns = BLAKE2b(name_string), 32 bytes.
	uint8_t ns[33];
	crypto_generichash(ns, 32,
		(const uint8_t *)ns_name, strlen(ns_name),
		nullptr, 0);

	// For each ID: out[i] = BLAKE2b(ns || id_byte), 32 bytes.
	for (int i = 0; i < count; i++) {
		ns[32] = ids[i];
		crypto_generichash(out + i * 32, 32, ns, 33, nullptr, 0);
	}
}

// ---- Ed25519 DH ----

void NoiseIK::_ed25519_dh(uint8_t *output, const uint8_t *secret_key_64,
						   const uint8_t *remote_public_key) {
	// Derive scalar from Ed25519 secret key:
	// SHA512(seed) → clamp → first 32 bytes = scalar.
	uint8_t sk_hash[64];
	crypto_hash_sha512(sk_hash, secret_key_64, 32); // Hash only the seed (first 32 bytes).

	// Clamp.
	sk_hash[0] &= 248;
	sk_hash[31] &= 127;
	sk_hash[31] |= 64;

	// DH: output = scalarmult_ed25519_noclamp(scalar, publicKey).
	int rc = crypto_scalarmult_ed25519_noclamp(output, sk_hash, remote_public_key);
	(void)rc; // Can fail if point is identity — not expected in handshake context.

	sodium_memzero(sk_hash, 64);
}

// ---- Handshake: Send ----

std::vector<uint8_t> NoiseIK::send(const uint8_t *payload, size_t payload_len) {
	std::vector<uint8_t> out;
	int msg = _message_index++;

	if (msg == 0 && _initiator) {
		// IK Message 1 (Initiator → Responder): e, es, s, ss

		// e: generate and send ephemeral.
		crypto_sign_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);

		// es: DH(ephemeral, remote_static).
		_mix_key_from_dh(_rs, _e.secret_key);

		// s: encrypt and send static public key.
		_encrypt_and_hash(out, _s.public_key, PKLEN);

		// ss: DH(static, remote_static).
		_mix_key_from_dh(_rs, _s.secret_key);

		// Encrypt payload.
		_encrypt_and_hash(out, payload, payload_len);

	} else if (msg == 1 && !_initiator) {
		// IK Message 2 (Responder → Initiator): e, ee, se

		// e: generate and send ephemeral.
		crypto_sign_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(responder_ephemeral, initiator_ephemeral).
		_mix_key_from_dh(_re, _e.secret_key);

		// se: DH(initiator_static_pk, responder_ephemeral_sk).
		// IK msg 2 tokens: e, ee, se. We're responder.
		// se = DH(remote_static, our_ephemeral) = DH(_rs, _e.secret_key).
		_mix_key_from_dh(_rs, _e.secret_key);

		// Encrypt payload.
		_encrypt_and_hash(out, payload, payload_len);

		// Handshake complete.
		_split();
	}

	return out;
}

// ---- Handshake: Receive ----

std::vector<uint8_t> NoiseIK::recv(const uint8_t *data, size_t len) {
	size_t offset = 0;
	int msg = _message_index++;

	if (msg == 0 && !_initiator) {
		// Receive IK Message 1 (Initiator → Responder): e, es, s, ss
		if (len < PKLEN) return {};

		// e: read remote ephemeral.
		memcpy(_re, data + offset, PKLEN);
		offset += PKLEN;
		_mix_hash(_re, PKLEN);

		// es: DH(initiator_ephemeral, responder_static).
		// We're responder: DH(_re, _s.secret_key).
		_mix_key_from_dh(_re, _s.secret_key);

		// s: decrypt remote 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);

		// ss: DH(responder_static, initiator_static).
		// We're responder: DH(_rs, _s.secret_key).
		_mix_key_from_dh(_rs, _s.secret_key);

		// Decrypt payload.
		return _decrypt_and_hash(data + offset, len - offset);

	} else if (msg == 1 && _initiator) {
		// Receive IK Message 2 (Responder → Initiator): e, ee, se
		if (len < PKLEN) return {};

		// e: read remote ephemeral.
		memcpy(_re, data + offset, PKLEN);
		offset += PKLEN;
		_mix_hash(_re, PKLEN);

		// ee: DH(initiator_ephemeral, responder_ephemeral).
		_mix_key_from_dh(_re, _e.secret_key);

		// se: DH(initiator_static, responder_ephemeral).
		// We're initiator: DH(_re, _s.secret_key).
		_mix_key_from_dh(_re, _s.secret_key);

		// Decrypt payload.
		auto payload = _decrypt_and_hash(data + offset, len - offset);

		// Handshake complete.
		_split();

		return payload;
	}

	return {};
}

// ---- Holepunch secret derivation ----

void NoiseIK::derive_holepunch_secret(const uint8_t *ns_peer_holepunch,
									  uint8_t *out_32) const {
	// holepunchSecret = BLAKE2b(key=handshake_hash, data=NS_PEER_HOLEPUNCH).
	// Matches JS: sodium.crypto_generichash(out, NS.PEER_HOLEPUNCH, this.handshake.hash)
	// Note: JS generichash(output, input, key) → BLAKE2b with key.
	crypto_generichash(out_32, 32, ns_peer_holepunch, 32, _hash, HASHLEN);
}

// ---- Symmetric state operations ----

void NoiseIK::_mix_hash(const uint8_t *data, size_t len) {
	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 NoiseIK::_mix_key_from_dh(const uint8_t *remote_pk, const uint8_t *local_sk_64) {
	uint8_t dh_output[DHLEN];
	_ed25519_dh(dh_output, local_sk_64, remote_pk);

	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);

	sodium_memzero(dh_output, DHLEN);
	sodium_memzero(new_ck, HASHLEN);
	sodium_memzero(temp_key, HASHLEN);
}

void NoiseIK::_initialise_key(const uint8_t *key) {
	memcpy(_cipher_key, key, KEYBYTES);
	_has_key = true;
	_nonce = 0;
}

void NoiseIK::_encrypt_and_hash(std::vector<uint8_t> &out,
								const uint8_t *pt, size_t pt_len) {
	if (!_has_key) {
		out.insert(out.end(), pt, pt + pt_len);
		_mix_hash(pt, pt_len);
	} else {
		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_hash(out.data() + old_size, ct_len);
	}
}

std::vector<uint8_t> NoiseIK::_decrypt_and_hash(const uint8_t *ct, size_t ct_len) {
	if (!_has_key) {
		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_hash(ct, ct_len);
		return pt;
	}
}

// ---- AEAD (ChaCha20-Poly1305 IETF) ----

void NoiseIK::_encrypt(uint8_t *ct, const uint8_t *pt, size_t pt_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 ct_len_out;
	crypto_aead_chacha20poly1305_ietf_encrypt(
		ct, &ct_len_out,
		pt, pt_len,
		ad, ad_len,
		nullptr, nonce, _cipher_key);

	_nonce++;
}

bool NoiseIK::_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;
}

// ---- HKDF (HMAC-BLAKE2b) ----

void NoiseIK::_hmac(uint8_t *out, const uint8_t *key, size_t key_len,
					const uint8_t *data, size_t data_len) {
	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);

	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);

	{
		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 NoiseIK::_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) {
	uint8_t prk[HASHLEN];
	_hmac(prk, salt, salt_len, ikm, ikm_len);

	uint8_t counter;
	counter = 1;
	_hmac(out1, prk, HASHLEN, &counter, 1);

	{
		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) {
		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);
}

void NoiseIK::_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);
	}

	memcpy(_hash, _digest, HASHLEN);
	_complete = true;

	sodium_memzero(&_e, sizeof(Ed25519KeyPair));
	sodium_memzero(_re, PKLEN);
	sodium_memzero(_cipher_key, KEYBYTES);
	sodium_memzero(_chaining_key, HASHLEN);

	sodium_memzero(k1, HASHLEN);
	sodium_memzero(k2, HASHLEN);
}