// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build go1.21

package quic

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/binary"
	"net/netip"
	"time"

	"golang.org/x/crypto/chacha20poly1305"
)

// AEAD and nonce used to compute the Retry Integrity Tag.
// https://www.rfc-editor.org/rfc/rfc9001#section-5.8
var (
	retrySecret = []byte{0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e}
	retryNonce  = []byte{0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb}
	retryAEAD   = func() cipher.AEAD {
		c, err := aes.NewCipher(retrySecret)
		if err != nil {
			panic(err)
		}
		aead, err := cipher.NewGCM(c)
		if err != nil {
			panic(err)
		}
		return aead
	}()
)

// retryTokenValidityPeriod is how long we accept a Retry packet token after sending it.
const retryTokenValidityPeriod = 5 * time.Second

// retryState generates and validates an endpoint's retry tokens.
type retryState struct {
	aead cipher.AEAD
}

func (rs *retryState) init() error {
	// Retry tokens are authenticated using a per-server key chosen at start time.
	// TODO: Provide a way for the user to set this key.
	secret := make([]byte, chacha20poly1305.KeySize)
	if _, err := rand.Read(secret); err != nil {
		return err
	}
	aead, err := chacha20poly1305.NewX(secret)
	if err != nil {
		panic(err)
	}
	rs.aead = aead
	return nil
}

// Retry tokens are encrypted with an AEAD.
// The plaintext contains the time the token was created and
// the original destination connection ID.
// The additional data contains the sender's source address and original source connection ID.
// The token nonce is randomly generated.
// We use the nonce as the Source Connection ID of the Retry packet.
// Since the 24-byte XChaCha20-Poly1305 nonce is too large to fit in a 20-byte connection ID,
// we include the remaining 4 bytes of nonce in the token.
//
// Token {
//   Last 4 Bytes of Nonce (32),
//   Ciphertext (..),
// }
//
// Plaintext {
//   Timestamp (64),
//   Original Destination Connection ID,
// }
//
//
// Additional Data {
//   Original Source Connection ID Length (8),
//   Original Source Connection ID (..),
//   IP Address (32..128),
//   Port (16),
// }
//
// TODO: Consider using AES-256-GCM-SIV once crypto/tls supports it.

func (rs *retryState) makeToken(now time.Time, srcConnID, origDstConnID []byte, addr netip.AddrPort) (token, newDstConnID []byte, err error) {
	nonce := make([]byte, rs.aead.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, nil, err
	}

	var plaintext []byte
	plaintext = binary.BigEndian.AppendUint64(plaintext, uint64(now.Unix()))
	plaintext = append(plaintext, origDstConnID...)

	token = append(token, nonce[maxConnIDLen:]...)
	token = rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr))
	return token, nonce[:maxConnIDLen], nil
}

func (rs *retryState) validateToken(now time.Time, token, srcConnID, dstConnID []byte, addr netip.AddrPort) (origDstConnID []byte, ok bool) {
	tokenNonceLen := rs.aead.NonceSize() - maxConnIDLen
	if len(token) < tokenNonceLen {
		return nil, false
	}
	nonce := append([]byte{}, dstConnID...)
	nonce = append(nonce, token[:tokenNonceLen]...)
	ciphertext := token[tokenNonceLen:]

	plaintext, err := rs.aead.Open(nil, nonce, ciphertext, rs.additionalData(srcConnID, addr))
	if err != nil {
		return nil, false
	}
	if len(plaintext) < 8 {
		return nil, false
	}
	when := time.Unix(int64(binary.BigEndian.Uint64(plaintext)), 0)
	origDstConnID = plaintext[8:]

	// We allow for tokens created in the future (up to the validity period),
	// which likely indicates that the system clock was adjusted backwards.
	if d := abs(now.Sub(when)); d > retryTokenValidityPeriod {
		return nil, false
	}

	return origDstConnID, true
}

func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []byte {
	var additional []byte
	additional = appendUint8Bytes(additional, srcConnID)
	additional = append(additional, addr.Addr().AsSlice()...)
	additional = binary.BigEndian.AppendUint16(additional, addr.Port())
	return additional
}

func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) {
	// The retry token is at the start of an Initial packet's data.
	token, n := consumeUint8Bytes(p.data)
	if n < 0 {
		// We've already validated that the packet is at least 1200 bytes long,
		// so there's no way for even a maximum size token to not fit.
		// Check anyway.
		return nil, false
	}
	if len(token) == 0 {
		// The sender has not provided a token.
		// Send a Retry packet to them with one.
		e.sendRetry(now, p, addr)
		return nil, false
	}
	origDstConnID, ok = e.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr)
	if !ok {
		// This does not seem to be a valid token.
		// Close the connection with an INVALID_TOKEN error.
		// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5
		e.sendConnectionClose(p, addr, errInvalidToken)
		return nil, false
	}
	return origDstConnID, true
}

func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) {
	token, srcConnID, err := e.retry.makeToken(now, p.srcConnID, p.dstConnID, addr)
	if err != nil {
		return
	}
	b := encodeRetryPacket(p.dstConnID, retryPacket{
		dstConnID: p.srcConnID,
		srcConnID: srcConnID,
		token:     token,
	})
	e.sendDatagram(b, addr)
}

type retryPacket struct {
	dstConnID []byte
	srcConnID []byte
	token     []byte
}

func encodeRetryPacket(originalDstConnID []byte, p retryPacket) []byte {
	// Retry packets include an integrity tag, computed by AEAD_AES_128_GCM over
	// the original destination connection ID followed by the Retry packet
	// (less the integrity tag itself).
	// https://www.rfc-editor.org/rfc/rfc9001#section-5.8
	//
	// Create the pseudo-packet (including the original DCID), append the tag,
	// and return the Retry packet.
	var b []byte
	b = appendUint8Bytes(b, originalDstConnID) // Original Destination Connection ID
	start := len(b)                            // start of the Retry packet
	b = append(b, headerFormLong|fixedBit|longPacketTypeRetry)
	b = binary.BigEndian.AppendUint32(b, quicVersion1) // Version
	b = appendUint8Bytes(b, p.dstConnID)               // Destination Connection ID
	b = appendUint8Bytes(b, p.srcConnID)               // Source Connection ID
	b = append(b, p.token...)                          // Token
	b = retryAEAD.Seal(b, retryNonce, nil, b)          // Retry Integrity Tag
	return b[start:]
}

func parseRetryPacket(b, origDstConnID []byte) (p retryPacket, ok bool) {
	const retryIntegrityTagLength = 128 / 8

	lp, ok := parseGenericLongHeaderPacket(b)
	if !ok {
		return retryPacket{}, false
	}
	if len(lp.data) < retryIntegrityTagLength {
		return retryPacket{}, false
	}
	gotTag := lp.data[len(lp.data)-retryIntegrityTagLength:]

	// Create the pseudo-packet consisting of the original destination connection ID
	// followed by the Retry packet (less the integrity tag).
	// Use this to validate the packet integrity tag.
	pseudo := appendUint8Bytes(nil, origDstConnID)
	pseudo = append(pseudo, b[:len(b)-retryIntegrityTagLength]...)
	wantTag := retryAEAD.Seal(nil, retryNonce, nil, pseudo)
	if !bytes.Equal(gotTag, wantTag) {
		return retryPacket{}, false
	}

	token := lp.data[:len(lp.data)-retryIntegrityTagLength]
	return retryPacket{
		dstConnID: lp.dstConnID,
		srcConnID: lp.srcConnID,
		token:     token,
	}, true
}
