// Copyright (C) 2019 Kyle Den Hartog
// MIT License. See LICENSE file for details.

/**
 * Package xchacha20 implements XChaCha20 stream cipher.
 */

import { writeUint32LE } from "@stablelib/binary";
import { wipe } from "@stablelib/wipe";
import { streamXOR as chachaStreamXOR } from "@stablelib/chacha";

// Number of ChaCha rounds (ChaCha20).
const ROUNDS = 20;

/**
 * Encrypt src with XChaCha20 stream generated for the given 32-byte key and
 * 8-byte (as in original implementation) or 12-byte (as in RFC7539) nonce and
 * write the result into dst and return it.
 *
 * dst and src may be the same, but otherwise must not overlap.
 *
 * Nonce length is set in such a way that given it's generated via a CSPRNG
 * then there's little concern of collision for roughly 2^96 messages while
 * reusing a secret key and not encountering nonce reuse vulnerabilities.
 */
export function streamXOR(key: Uint8Array, nonce: Uint8Array, src: Uint8Array,
    dst: Uint8Array): Uint8Array {
    if (nonce.length !== 24) {
        throw new Error("XChaCha20 nonce must be 24 bytes");
    }

    // Use HChaCha one-way function to transform first 16 bytes of
    // 24-byte extended nonce and key into a new key for Salsa
    // stream -- "subkey".
    const subkey = hchacha(key, nonce.subarray(0, 16), new Uint8Array(32));


    // Use last 8 bytes of 24-byte extended nonce as an actual nonce prefixed by 4 zero bytes,
    // and a subkey derived in the previous step as key to encrypt.
    const modifiedNonce = new Uint8Array(12);
    modifiedNonce.set(nonce.subarray(16), 4);
    // If nonceInplaceCounterLength > 0, we'll still pass the correct
    // nonce || counter, as we don't limit the end of nonce subarray.
    const result = chachaStreamXOR(subkey, modifiedNonce, src, dst);

    // Clean subkey.
    wipe(subkey);

    return result;
}

/**
 * Generate XChaCha20 stream for the given 32-byte key and 12-byte
 * nonce (last 8 bytes of 24 byte nonce prefixed with 4 zero bytes)
 * and write it into dst and return it.
 *
 * Nonces MUST be generated using an CSPRNG to generate a sufficiently
 * random nonce such that a collision is highly unlikely to occur.
 *
 * stream is like streamXOR with all-zero src.
 */
export function stream(key: Uint8Array, nonce: Uint8Array,
    dst: Uint8Array): Uint8Array {
    wipe(dst);
    return streamXOR(key, nonce, dst, dst);
}

/**
 * HChaCha is a one-way function used in XChaCha to extend nonce.
 *
 * It takes 32-byte key and 16-byte src and writes 32-byte result
 * into dst and returns it.
 */
export function hchacha(
    key: Uint8Array,
    src: Uint8Array,
    dst: Uint8Array
): Uint8Array {
    let j0 = 0x61707865; // "expa"  -- ChaCha's "sigma" constant
    let j1 = 0x3320646e; // "nd 3"     for 32-byte keys
    let j2 = 0x79622d32; // "2-by"
    let j3 = 0x6b206574; // "te k"
    let j4 = (key[3] << 24) | (key[2] << 16) | (key[1] << 8) | key[0];
    let j5 = (key[7] << 24) | (key[6] << 16) | (key[5] << 8) | key[4];
    let j6 = (key[11] << 24) | (key[10] << 16) | (key[9] << 8) | key[8];
    let j7 = (key[15] << 24) | (key[14] << 16) | (key[13] << 8) | key[12];
    let j8 = (key[19] << 24) | (key[18] << 16) | (key[17] << 8) | key[16];
    let j9 = (key[23] << 24) | (key[22] << 16) | (key[21] << 8) | key[20];
    let j10 = (key[27] << 24) | (key[26] << 16) | (key[25] << 8) | key[24];
    let j11 = (key[31] << 24) | (key[30] << 16) | (key[29] << 8) | key[28];
    let j12 = (src[3] << 24) | (src[2] << 16) | (src[1] << 8) | src[0];
    let j13 = (src[7] << 24) | (src[6] << 16) | (src[5] << 8) | src[4];
    let j14 = (src[11] << 24) | (src[10] << 16) | (src[9] << 8) | src[8];
    let j15 = (src[15] << 24) | (src[14] << 16) | (src[13] << 8) | src[12];

    let x0 = j0;
    let x1 = j1;
    let x2 = j2;
    let x3 = j3;
    let x4 = j4;
    let x5 = j5;
    let x6 = j6;
    let x7 = j7;
    let x8 = j8;
    let x9 = j9;
    let x10 = j10;
    let x11 = j11;
    let x12 = j12;
    let x13 = j13;
    let x14 = j14;
    let x15 = j15;

    for (let i = 0; i < ROUNDS; i += 2) {
        x0 = (x0 + x4) | 0;
        x12 ^= x0;
        x12 = (x12 >>> (32 - 16)) | (x12 << 16);
        x8 = (x8 + x12) | 0;
        x4 ^= x8;
        x4 = (x4 >>> (32 - 12)) | (x4 << 12);
        x1 = (x1 + x5) | 0;
        x13 ^= x1;
        x13 = (x13 >>> (32 - 16)) | (x13 << 16);
        x9 = (x9 + x13) | 0;
        x5 ^= x9;
        x5 = (x5 >>> (32 - 12)) | (x5 << 12);

        x2 = (x2 + x6) | 0;
        x14 ^= x2;
        x14 = (x14 >>> (32 - 16)) | (x14 << 16);
        x10 = (x10 + x14) | 0;
        x6 ^= x10;
        x6 = (x6 >>> (32 - 12)) | (x6 << 12);
        x3 = (x3 + x7) | 0;
        x15 ^= x3;
        x15 = (x15 >>> (32 - 16)) | (x15 << 16);
        x11 = (x11 + x15) | 0;
        x7 ^= x11;
        x7 = (x7 >>> (32 - 12)) | (x7 << 12);

        x2 = (x2 + x6) | 0;
        x14 ^= x2;
        x14 = (x14 >>> (32 - 8)) | (x14 << 8);
        x10 = (x10 + x14) | 0;
        x6 ^= x10;
        x6 = (x6 >>> (32 - 7)) | (x6 << 7);
        x3 = (x3 + x7) | 0;
        x15 ^= x3;
        x15 = (x15 >>> (32 - 8)) | (x15 << 8);
        x11 = (x11 + x15) | 0;
        x7 ^= x11;
        x7 = (x7 >>> (32 - 7)) | (x7 << 7);

        x1 = (x1 + x5) | 0;
        x13 ^= x1;
        x13 = (x13 >>> (32 - 8)) | (x13 << 8);
        x9 = (x9 + x13) | 0;
        x5 ^= x9;
        x5 = (x5 >>> (32 - 7)) | (x5 << 7);
        x0 = (x0 + x4) | 0;
        x12 ^= x0;
        x12 = (x12 >>> (32 - 8)) | (x12 << 8);
        x8 = (x8 + x12) | 0;
        x4 ^= x8;
        x4 = (x4 >>> (32 - 7)) | (x4 << 7);

        x0 = (x0 + x5) | 0;
        x15 ^= x0;
        x15 = (x15 >>> (32 - 16)) | (x15 << 16);
        x10 = (x10 + x15) | 0;
        x5 ^= x10;
        x5 = (x5 >>> (32 - 12)) | (x5 << 12);
        x1 = (x1 + x6) | 0;
        x12 ^= x1;
        x12 = (x12 >>> (32 - 16)) | (x12 << 16);
        x11 = (x11 + x12) | 0;
        x6 ^= x11;
        x6 = (x6 >>> (32 - 12)) | (x6 << 12);

        x2 = (x2 + x7) | 0;
        x13 ^= x2;
        x13 = (x13 >>> (32 - 16)) | (x13 << 16);
        x8 = (x8 + x13) | 0;
        x7 ^= x8;
        x7 = (x7 >>> (32 - 12)) | (x7 << 12);
        x3 = (x3 + x4) | 0;
        x14 ^= x3;
        x14 = (x14 >>> (32 - 16)) | (x14 << 16);
        x9 = (x9 + x14) | 0;
        x4 ^= x9;
        x4 = (x4 >>> (32 - 12)) | (x4 << 12);

        x2 = (x2 + x7) | 0;
        x13 ^= x2;
        x13 = (x13 >>> (32 - 8)) | (x13 << 8);
        x8 = (x8 + x13) | 0;
        x7 ^= x8;
        x7 = (x7 >>> (32 - 7)) | (x7 << 7);
        x3 = (x3 + x4) | 0;
        x14 ^= x3;
        x14 = (x14 >>> (32 - 8)) | (x14 << 8);
        x9 = (x9 + x14) | 0;
        x4 ^= x9;
        x4 = (x4 >>> (32 - 7)) | (x4 << 7);

        x1 = (x1 + x6) | 0;
        x12 ^= x1;
        x12 = (x12 >>> (32 - 8)) | (x12 << 8);
        x11 = (x11 + x12) | 0;
        x6 ^= x11;
        x6 = (x6 >>> (32 - 7)) | (x6 << 7);
        x0 = (x0 + x5) | 0;
        x15 ^= x0;
        x15 = (x15 >>> (32 - 8)) | (x15 << 8);
        x10 = (x10 + x15) | 0;
        x5 ^= x10;
        x5 = (x5 >>> (32 - 7)) | (x5 << 7);
    }
    writeUint32LE(x0, dst, 0);
    writeUint32LE(x1, dst, 4);
    writeUint32LE(x2, dst, 8);
    writeUint32LE(x3, dst, 12);
    writeUint32LE(x12, dst, 16);
    writeUint32LE(x13, dst, 20);
    writeUint32LE(x14, dst, 24);
    writeUint32LE(x15, dst, 28);
    return dst;
}
