//! Cross-implementation known-answer test (KAT) for NaCl `box` (X25519 +
//! XSalsa20-Poly1305), the AEAD construction `crypto_box::SalsaBox` provides.
//!
//! This is the one crypto primitive the engine relied on with **no Go
//! cross-validation** — disco (`ts_disco_protocol`, this crate) seals its
//! NAT-traversal messages with `crypto_box::SalsaBox::new(&their_pub, &my_secret)`
//! over a 24-byte nonce (see `packet.rs::encrypt_in_place` / `decrypt`), and the
//! DERP client/server handshake (`ts_derp::client`) seals its client-info /
//! server-info with the identical construction. Go's `disco` and DERP both use
//! `golang.org/x/crypto/nacl/box`. A divergence in the X25519→HSalsa20 key
//! derivation, the XSalsa20 keystream, or the Poly1305 MAC would let two Rust
//! nodes interoperate with each other while silently failing against **every** Go
//! peer and every Go DERP server — a total connectivity break invisible to a
//! self-round-trip test. This KAT pins the wire bytes in both directions.
//!
//! Provenance: the reference `sealed` bytes come from Go `nacl/box.Seal` over
//! fixed X25519 keypairs + fixed 24-byte nonces + fixed plaintexts, generated by
//! `tests/vectors/gen/nacl_box/main.go` (Go module pinned `golang.org/x/crypto
//! v0.52.0`) and recorded in `tests/vectors/nacl_box_kat.json`. The vectors are
//! embedded inline below (mirroring `blake2s_kat`/the WG handshake KAT, which
//! likewise inline their reference values). `box.Seal` returns the NaCl wire
//! layout `16-byte Poly1305 tag || ciphertext` — the authenticator PRECEDES the
//! ciphertext (the standard `crypto_secretbox`/NaCl convention, and exactly why
//! the disco header carries the tag in a dedicated field rather than appended).
//! `SalsaBox::encrypt_in_place_detached` returns the ciphertext in place with the
//! tag detached, so `tag || ct` must equal Go's `sealed`.

use crypto_box::{
    PublicKey, SalsaBox, SecretKey, Tag,
    aead::{AeadInPlace, generic_array::GenericArray},
};

/// One NaCl-box vector: a sender seals `pt` to the recipient under `nonce`.
struct Vector {
    desc: &'static str,
    sender_priv: &'static str,
    recipient_priv: &'static str,
    nonce: &'static str,
    pt: &'static str,
    /// Go `nacl/box.Seal` output: `16-byte Poly1305 tag || ciphertext` (NaCl layout).
    sealed: &'static str,
}

/// Generated by `tests/vectors/gen/nacl_box` from Go `golang.org/x/crypto/nacl/box`
/// (v0.52.0); recorded in `tests/vectors/nacl_box_kat.json`.
const VECTORS: &[Vector] = &[
    Vector {
        desc: "empty plaintext (tag-only)",
        sender_priv: "1111111111111111111111111111111111111111111111111111111111111111",
        recipient_priv: "2222222222222222222222222222222222222222222222222222222222222222",
        nonce: "000102030405060708090a0b0c0d0e0f1011121314151617",
        pt: "",
        sealed: "67021af553ef442bf090cb131f43c15a",
    },
    Vector {
        desc: "short disco-like plaintext (xyzzy)",
        sender_priv: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
        recipient_priv: "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
        nonce: "fedcba9876543210fedcba9876543210fedcba9876543210",
        pt: "78797a7a79",
        sealed: "c3fd88f61be3a3aa81d7e9cd6add0920be9a157b95",
    },
    Vector {
        desc: "longer plaintext (DERP client-info-like JSON blob)",
        sender_priv: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
        recipient_priv: "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe",
        nonce: "0000000000000000000000000000000000000000deadbeef",
        pt: "7b2276657273696f6e223a322c226d65736853657373696f6e223a747275657d",
        sealed: "d1cbf633e31ab0e376138e0c250bfc1419037c86bc9fd5c5ba83113f8125a1edd1bec2210d75db66004e214be16a14b1",
    },
];

fn unhex(s: &str) -> Vec<u8> {
    assert!(s.len().is_multiple_of(2), "odd hex length");
    (0..s.len() / 2)
        .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("valid hex byte"))
        .collect()
}

fn key32(s: &str) -> [u8; 32] {
    unhex(s).try_into().expect("32-byte key")
}

fn nonce24(s: &str) -> [u8; 24] {
    unhex(s).try_into().expect("24-byte nonce")
}

/// `SalsaBox` (the construction disco + DERP use) must produce byte-identical
/// `tag || ciphertext` (the NaCl layout) to Go's `nacl/box.Seal`, and must open
/// Go's sealed bytes back to the original plaintext. A divergence here is a silent
/// interop break with every Go peer / DERP server.
#[test]
fn salsabox_matches_go_nacl_box_kat() {
    for v in VECTORS {
        let sender_priv = SecretKey::from(key32(v.sender_priv));
        let recipient_priv = SecretKey::from(key32(v.recipient_priv));
        let recipient_pub = recipient_priv.public_key();
        let nonce = nonce24(v.nonce);
        let pt = unhex(v.pt);
        let expected = unhex(v.sealed);

        // Seal direction: the sender boxes to the recipient's public key, exactly
        // as `Packet::encrypt_in_place` / DERP `encrypt_client_info` do.
        let seal_box = SalsaBox::new(&recipient_pub, &sender_priv);
        let mut buf = pt.clone();
        let tag = seal_box
            .encrypt_in_place_detached(&GenericArray::from(nonce), &[], &mut buf)
            .expect("seal must succeed");
        // Go `box.Seal` lays the 16-byte Poly1305 tag FIRST, then the ciphertext
        // (the NaCl convention). Assemble in that order to compare wire bytes.
        let mut got = tag.as_slice().to_vec();
        got.extend_from_slice(&buf);
        assert_eq!(
            got, expected,
            "vector '{}': SalsaBox tag||ct must equal Go nacl/box.Seal",
            v.desc
        );

        // Open direction: the recipient boxes from the sender's public key and
        // recovers the plaintext from Go's sealed bytes — proving we can read what
        // a Go peer/DERP server sends.
        let sender_pub = sender_priv.public_key();
        let open_box = SalsaBox::new(&sender_pub, &recipient_priv);
        // Go's `sealed` is tag(16) || ciphertext.
        let (tag_bytes, ct) = expected.split_at(16);
        let mut open_buf = ct.to_vec();
        open_box
            .decrypt_in_place_detached(
                &GenericArray::from(nonce),
                &[],
                &mut open_buf,
                Tag::from_slice(tag_bytes),
            )
            .expect("open of Go-sealed bytes must succeed");
        assert_eq!(
            open_buf, pt,
            "vector '{}': SalsaBox must open Go's sealed bytes to the original plaintext",
            v.desc
        );
    }
}

/// A tampered tag (or ciphertext) must fail to open — the Poly1305 authenticator
/// is load-bearing, and a "decrypt that ignores the tag" would be a silent
/// auth-bypass. Flips one bit of the trailing tag and asserts the open fails.
#[test]
fn salsabox_rejects_tampered_tag() {
    let v = &VECTORS[1]; // the short non-empty vector
    let sender_priv = SecretKey::from(key32(v.sender_priv));
    let recipient_priv = SecretKey::from(key32(v.recipient_priv));
    let nonce = nonce24(v.nonce);
    let mut sealed = unhex(v.sealed);

    // Flip the first byte (inside the leading 16-byte Poly1305 tag).
    sealed[0] ^= 0x01;

    let sender_pub = sender_priv.public_key();
    let open_box = SalsaBox::new(&sender_pub, &recipient_priv);
    // Go layout: tag(16) || ciphertext.
    let (tag_bytes, ct) = sealed.split_at(16);
    let mut open_buf = ct.to_vec();
    let res = open_box.decrypt_in_place_detached(
        &GenericArray::from(nonce),
        &[],
        &mut open_buf,
        Tag::from_slice(tag_bytes),
    );
    assert!(
        res.is_err(),
        "a tampered Poly1305 tag must fail authentication"
    );
}

/// Sanity: `PublicKey` derived from the private scalar matches the value Go
/// emitted (X25519 over the basepoint), so the keypairs the seal/open use are the
/// same ones Go used to produce the reference bytes.
#[test]
fn derived_public_keys_match_go() {
    // (recipient_priv, expected recipient_pub) from the generator's emitted JSON.
    let cases = [
        (
            "2222222222222222222222222222222222222222222222222222222222222222",
            "0faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20",
        ),
        (
            "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
            "358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254",
        ),
    ];
    for (priv_hex, pub_hex) in cases {
        let sk = SecretKey::from(key32(priv_hex));
        let pk: PublicKey = sk.public_key();
        assert_eq!(
            pk.as_bytes(),
            key32(pub_hex).as_slice(),
            "X25519 base-point public key must match Go"
        );
    }
}
