using System.Security.Cryptography;

namespace DodoSSH.Client.Ssh;

/// <summary>
/// OpenSSH-style SSH host key fingerprints. See docs/crypto.md §8.
/// </summary>
/// <remarks>
/// <para>
/// A different thing from a DodoSSH identity fingerprint, and deliberately kept in the format
/// users already recognise: <c>SHA256:</c> followed by unpadded standard base64, exactly what
/// <c>ssh-keygen -lf</c> prints. A user comparing our string against the one their server
/// operator sent them must not have to wonder whether the encodings match.
/// </para>
/// <para>
/// Computed from the raw host key blob. SSH.NET exposes an MD5 <c>FingerPrint</c> property;
/// that is not used — MD5 is banned repo-wide, and no modern operator publishes an MD5 host
/// key fingerprint to compare against.
/// </para>
/// </remarks>
public static class SshHostKeyFingerprint
{
    /// <summary>Prefix identifying the hash algorithm, as OpenSSH writes it.</summary>
    public const string Prefix = "SHA256:";

    /// <summary>
    /// Formats a host key blob as <c>SHA256:&lt;base64&gt;</c>.
    /// </summary>
    /// <param name="hostKeyBlob">
    /// The raw key blob as it arrived on the wire — the same bytes OpenSSH hashes, not a parsed
    /// or re-encoded form.
    /// </param>
    public static string Format(ReadOnlySpan<byte> hostKeyBlob)
    {
        if (hostKeyBlob.IsEmpty)
        {
            throw new ArgumentException("A host key blob is required.", nameof(hostKeyBlob));
        }

        Span<byte> digest = stackalloc byte[SHA256.HashSizeInBytes];
        SHA256.HashData(hostKeyBlob, digest);

        // Standard base64, not base64url, and unpadded — OpenSSH's exact rendering. Using the
        // URL alphabet here would produce a string that looks right and never matches.
        return Prefix + Convert.ToBase64String(digest).TrimEnd('=');
    }

    /// <summary>
    /// Compares two fingerprints in constant time.
    /// </summary>
    /// <remarks>
    /// A fingerprint is not a secret, so this is not about a timing oracle. It is about refusing
    /// to let a mismatch be decided by ordinal string comparison somewhere that later gets
    /// "optimised" into a case-insensitive or culture-aware comparison — base64 is
    /// case-sensitive, and a case-insensitive match here would accept a different key.
    /// </remarks>
    public static bool Equal(string? left, string? right)
    {
        if (left is null || right is null || left.Length != right.Length)
        {
            return false;
        }

        return CryptographicOperations.FixedTimeEquals(
            System.Text.Encoding.UTF8.GetBytes(left),
            System.Text.Encoding.UTF8.GetBytes(right));
    }
}
