using System.Security.Cryptography;
using NSec.Cryptography;

namespace DodoSSH.Crypto;

/// <summary>
/// Vault keys and the grants that wrap them to members. See docs/crypto.md §3 and §6.
/// </summary>
/// <remarks>
/// <para>
/// A vault key is 32 random bytes, one per vault per generation. It never leaves a client in
/// plaintext: each member holds it sealed to their own X25519 key, which is why the server can store
/// every grant and read none of them.
/// </para>
/// <para>
/// Sealing is anonymous-sender by construction, so a grant proves nothing about who created it. That
/// is why every grant additionally carries a detached Ed25519 signature from the granter — without
/// one, a server could fabricate a grant containing a key of its own choosing and the recipient
/// would have no way to tell. The signature makes it detectable and attributable; it cannot make it
/// impossible, because verification would require the server to hold the key.
/// </para>
/// </remarks>
public static class VaultKeys
{
    /// <summary>
    /// Generates a fresh vault key.
    /// </summary>
    /// <remarks>
    /// Returned as an array the caller owns and must zero. There is no wrapper type here on purpose:
    /// the key is needed as a raw span by every item operation, and a type that had to be unwrapped
    /// at each call site would be unwrapped carelessly.
    /// </remarks>
    public static byte[] Create() =>
        RandomNumberGenerator.GetBytes(CryptoSpec.SymmetricKeySize);

    /// <summary>
    /// Seals a vault key to a member's encryption key.
    /// </summary>
    /// <param name="vaultKey">The 32-byte vault key.</param>
    /// <param name="recipientEncryptionPublicKey">
    /// The recipient's X25519 public key. <b>Verify this key before calling.</b> Wrapping to an
    /// unverified key is the one mistake that undoes end-to-end encryption entirely — check the
    /// identity-provider binding, the pinned fingerprint and the key log head first. See ADR 0001.
    /// </param>
    /// <param name="vaultId">The vault, which the AAD binds this grant to.</param>
    /// <param name="keyGeneration">
    /// The generation in force. Part of the AAD, so a server cannot re-serve a superseded
    /// generation's grant after a rekey.
    /// </param>
    public static byte[] WrapTo(
        ReadOnlySpan<byte> vaultKey,
        ReadOnlySpan<byte> recipientEncryptionPublicKey,
        Guid vaultId,
        uint keyGeneration)
    {
        RequireVaultKey(vaultKey);

        return DshCrypto.SealTo(
            recipientEncryptionPublicKey,
            vaultKey,
            DshAad.VaultKeyGrant(vaultId, keyGeneration));
    }

    /// <summary>
    /// Opens a grant with the recipient's private key.
    /// </summary>
    /// <returns>
    /// The vault key, or <see langword="null"/> if the grant is not for this key, not for this vault,
    /// or not for this generation. A malicious granter can seal garbage; that surfaces here as a
    /// null, and the grant's signature names who did it.
    /// </returns>
    public static byte[]? TryUnwrap(
        Key recipientKey,
        ReadOnlySpan<byte> envelope,
        Guid vaultId,
        uint keyGeneration)
    {
        var plaintext = DshCrypto.OpenSealed(
            recipientKey,
            envelope,
            DshAad.VaultKeyGrant(vaultId, keyGeneration));

        if (plaintext is null)
        {
            return null;
        }

        // A grant that opens but does not contain a key-sized value is not a vault key. Failing here
        // rather than passing it on means the error names the grant instead of surfacing later as an
        // unexplained item decryption failure.
        if (plaintext.Length != CryptoSpec.SymmetricKeySize)
        {
            CryptographicOperations.ZeroMemory(plaintext);
            return null;
        }

        return plaintext;
    }

    internal static void RequireVaultKey(ReadOnlySpan<byte> vaultKey)
    {
        if (vaultKey.Length != CryptoSpec.SymmetricKeySize)
        {
            throw new ArgumentException(
                $"A vault key is {CryptoSpec.SymmetricKeySize} bytes, got {vaultKey.Length}.",
                nameof(vaultKey));
        }
    }
}
