Немного обосрался с копипастой, вот полностью рабочая версия ChaCha с Википедии:
#define ROTL(a,b) (((a) << (b)) | ((a) >> (32 - (b))))
#define QR(a, b, c, d) (			\
a += b,  d ^= a,  d = ROTL(d,16),	\
c += d,  b ^= c,  b = ROTL(b,12),	\
a += b,  d ^= a,  d = ROTL(d, 8),	\
c += d,  b ^= c,  b = ROTL(b, 7))
#define ROUNDS 20
void chacha_block(uint32_t out[16], uint32_t const in[16])
{
int i;
uint32_t x[16];
for (i = 0; i < 16; ++i)	
	x[i] = in[i];
	
for (i = 0; i < ROUNDS; i += 2) {
	QR(x[0], x[4], x[ 8], x[12]);
	QR(x[1], x[5], x[ 9], x[13]);
	QR(x[2], x[6], x[10], x[14]);
	QR(x[3], x[7], x[11], x[15]); 
	QR(x[0], x[5], x[10], x[15]);
	QR(x[1], x[6], x[11], x[12]);
	QR(x[2], x[7], x[ 8], x[13]);
	QR(x[3], x[4], x[ 9], x[14]);
}
for (i = 0; i < 16; ++i)
	out[i] = x[i] + in[i];
}
Массив in[16] это Initial state of ChaCha. Меняется только Counter. Массив out[16] это поток, который XOR-ится к открытому тексту.
Сложение с начальным состоянием (x[i] + in[i]) нужно, чтобы сделать результат необратимым. 
Иначе можно было бы взять одну пару открытый-зашифрованый текст и провернуть раундовую функцию обратно, получив ключ.
Выглядит это очень слабо по сравнению с AES-подобными шифрами.