using System.Buffers.Text;
using System.Security.Cryptography;
using System.Text;

namespace DodoSSH.Client.Auth;

/// <summary>
/// A PKCE code verifier and its S256 challenge. See RFC 7636.
/// </summary>
/// <remarks>
/// <para>
/// A public client cannot keep a secret, so PKCE is what stops an authorization code being
/// redeemed by anyone who intercepts it — on a loopback redirect that means any other local
/// process that manages to receive the callback. The token endpoint only accepts the code
/// alongside the verifier whose hash it saw at authorization time.
/// </para>
/// <para>
/// <c>S256</c> only. The <c>plain</c> method is still in the RFC and offers no protection
/// whatsoever against an attacker who saw the authorization request.
/// </para>
/// </remarks>
public sealed class PkcePair
{
    /// <summary>The challenge method sent to the authorization endpoint.</summary>
    public const string Method = "S256";

    /// <summary>
    /// Entropy behind the verifier. 32 bytes renders as 43 base64url characters, the RFC's
    /// minimum length and comfortably beyond guessing.
    /// </summary>
    private const int VerifierEntropyBytes = 32;

    private PkcePair(string codeVerifier, string codeChallenge)
    {
        CodeVerifier = codeVerifier;
        CodeChallenge = codeChallenge;
    }

    /// <summary>The secret held until the token exchange. Never leaves the process.</summary>
    public string CodeVerifier { get; }

    /// <summary>The hash sent with the authorization request.</summary>
    public string CodeChallenge { get; }

    /// <summary>Generates a fresh pair.</summary>
    public static PkcePair Create()
    {
        // Base64url of random bytes, which satisfies the RFC's unreserved-character set without
        // any escaping. Generating characters directly from an alphabet would be one more place to
        // get a modulo bias wrong for no benefit.
        var verifier = Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(VerifierEntropyBytes));

        var challenge = Base64Url.EncodeToString(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));

        return new PkcePair(verifier, challenge);
    }
}
