const std = @import("std");
const builtin = @import("builtin");
const crypto = std.crypto;
const debug = std.debug;
const math = std.math;
const mem = std.mem;

const Poly1305 = crypto.onetimeauth.Poly1305;
const Blake2b = crypto.hash.blake2.Blake2b;
const X25519 = crypto.dh.X25519;

const AuthenticationError = crypto.errors.AuthenticationError;
const IdentityElementError = crypto.errors.IdentityElementError;
const WeakPublicKeyError = crypto.errors.WeakPublicKeyError;

/// The Salsa cipher with 20 rounds.
pub const Salsa20 = Salsa(20);

/// The XSalsa cipher with 20 rounds.
pub const XSalsa20 = XSalsa(20);

fn SalsaVecImpl(comptime rounds: comptime_int) type {
    return struct {
        const Lane = @Vector(4, u32);
        const Half = @Vector(2, u32);
        const BlockVec = [4]Lane;

        fn initContext(key: [8]u32, d: [4]u32) BlockVec {
            const c = "expand 32-byte k";
            const constant_le = comptime [4]u32{
                mem.readInt(u32, c[0..4], .little),
                mem.readInt(u32, c[4..8], .little),
                mem.readInt(u32, c[8..12], .little),
                mem.readInt(u32, c[12..16], .little),
            };
            return BlockVec{
                Lane{ key[0], key[1], key[2], key[3] },
                Lane{ key[4], key[5], key[6], key[7] },
                Lane{ constant_le[0], constant_le[1], constant_le[2], constant_le[3] },
                Lane{ d[0], d[1], d[2], d[3] },
            };
        }

        fn salsaCore(x: *BlockVec, input: BlockVec, comptime feedback: bool) void {
            const n1n2n3n0 = Lane{ input[3][1], input[3][2], input[3][3], input[3][0] };
            const n1n2 = Half{ n1n2n3n0[0], n1n2n3n0[1] };
            const n3n0 = Half{ n1n2n3n0[2], n1n2n3n0[3] };
            const k0k1 = Half{ input[0][0], input[0][1] };
            const k2k3 = Half{ input[0][2], input[0][3] };
            const k4k5 = Half{ input[1][0], input[1][1] };
            const k6k7 = Half{ input[1][2], input[1][3] };
            const n0k0 = Half{ n3n0[1], k0k1[0] };
            const k0n0 = Half{ n0k0[1], n0k0[0] };
            const k4k5k0n0 = Lane{ k4k5[0], k4k5[1], k0n0[0], k0n0[1] };
            const k1k6 = Half{ k0k1[1], k6k7[0] };
            const k6k1 = Half{ k1k6[1], k1k6[0] };
            const n1n2k6k1 = Lane{ n1n2[0], n1n2[1], k6k1[0], k6k1[1] };
            const k7n3 = Half{ k6k7[1], n3n0[0] };
            const n3k7 = Half{ k7n3[1], k7n3[0] };
            const k2k3n3k7 = Lane{ k2k3[0], k2k3[1], n3k7[0], n3k7[1] };

            var diag0 = input[2];
            var diag1 = @shuffle(u32, k4k5k0n0, undefined, [_]i32{ 1, 2, 3, 0 });
            var diag2 = @shuffle(u32, n1n2k6k1, undefined, [_]i32{ 1, 2, 3, 0 });
            var diag3 = @shuffle(u32, k2k3n3k7, undefined, [_]i32{ 1, 2, 3, 0 });

            const start0 = diag0;
            const start1 = diag1;
            const start2 = diag2;
            const start3 = diag3;

            var i: usize = 0;
            while (i < rounds) : (i += 2) {
                diag3 ^= math.rotl(Lane, diag1 +% diag0, 7);
                diag2 ^= math.rotl(Lane, diag0 +% diag3, 9);
                diag1 ^= math.rotl(Lane, diag3 +% diag2, 13);
                diag0 ^= math.rotl(Lane, diag2 +% diag1, 18);

                diag3 = @shuffle(u32, diag3, undefined, [_]i32{ 3, 0, 1, 2 });
                diag2 = @shuffle(u32, diag2, undefined, [_]i32{ 2, 3, 0, 1 });
                diag1 = @shuffle(u32, diag1, undefined, [_]i32{ 1, 2, 3, 0 });

                diag1 ^= math.rotl(Lane, diag3 +% diag0, 7);
                diag2 ^= math.rotl(Lane, diag0 +% diag1, 9);
                diag3 ^= math.rotl(Lane, diag1 +% diag2, 13);
                diag0 ^= math.rotl(Lane, diag2 +% diag3, 18);

                diag1 = @shuffle(u32, diag1, undefined, [_]i32{ 3, 0, 1, 2 });
                diag2 = @shuffle(u32, diag2, undefined, [_]i32{ 2, 3, 0, 1 });
                diag3 = @shuffle(u32, diag3, undefined, [_]i32{ 1, 2, 3, 0 });
            }

            if (feedback) {
                diag0 +%= start0;
                diag1 +%= start1;
                diag2 +%= start2;
                diag3 +%= start3;
            }

            const x0x1x10x11 = Lane{ diag0[0], diag1[1], diag0[2], diag1[3] };
            const x12x13x6x7 = Lane{ diag1[0], diag2[1], diag1[2], diag2[3] };
            const x8x9x2x3 = Lane{ diag2[0], diag3[1], diag2[2], diag3[3] };
            const x4x5x14x15 = Lane{ diag3[0], diag0[1], diag3[2], diag0[3] };

            x[0] = Lane{ x0x1x10x11[0], x0x1x10x11[1], x8x9x2x3[2], x8x9x2x3[3] };
            x[1] = Lane{ x4x5x14x15[0], x4x5x14x15[1], x12x13x6x7[2], x12x13x6x7[3] };
            x[2] = Lane{ x8x9x2x3[0], x8x9x2x3[1], x0x1x10x11[2], x0x1x10x11[3] };
            x[3] = Lane{ x12x13x6x7[0], x12x13x6x7[1], x4x5x14x15[2], x4x5x14x15[3] };
        }

        fn hashToBytes(out: *[64]u8, x: BlockVec) void {
            var i: usize = 0;
            while (i < 4) : (i += 1) {
                mem.writeInt(u32, out[16 * i + 0 ..][0..4], x[i][0], .little);
                mem.writeInt(u32, out[16 * i + 4 ..][0..4], x[i][1], .little);
                mem.writeInt(u32, out[16 * i + 8 ..][0..4], x[i][2], .little);
                mem.writeInt(u32, out[16 * i + 12 ..][0..4], x[i][3], .little);
            }
        }

        fn salsaXor(out: []u8, in: []const u8, key: [8]u32, d: [4]u32) void {
            var ctx = initContext(key, d);
            var x: BlockVec = undefined;
            var buf: [64]u8 = undefined;
            var i: usize = 0;
            while (i + 64 <= in.len) : (i += 64) {
                salsaCore(x[0..], ctx, true);
                hashToBytes(buf[0..], x);
                var xout = out[i..];
                const xin = in[i..];
                var j: usize = 0;
                while (j < 64) : (j += 1) {
                    xout[j] = xin[j];
                }
                j = 0;
                while (j < 64) : (j += 1) {
                    xout[j] ^= buf[j];
                }
                ctx[3][2] +%= 1;
                if (ctx[3][2] == 0) {
                    ctx[3][3] += 1;
                }
            }
            if (i < in.len) {
                salsaCore(x[0..], ctx, true);
                hashToBytes(buf[0..], x);

                var xout = out[i..];
                const xin = in[i..];
                var j: usize = 0;
                while (j < in.len % 64) : (j += 1) {
                    xout[j] = xin[j] ^ buf[j];
                }
            }
        }

        fn hsalsa(input: [16]u8, key: [32]u8) [32]u8 {
            var c: [4]u32 = undefined;
            for (c, 0..) |_, i| {
                c[i] = mem.readInt(u32, input[4 * i ..][0..4], .little);
            }
            const ctx = initContext(keyToWords(key), c);
            var x: BlockVec = undefined;
            salsaCore(x[0..], ctx, false);
            var out: [32]u8 = undefined;
            mem.writeInt(u32, out[0..4], x[0][0], .little);
            mem.writeInt(u32, out[4..8], x[1][1], .little);
            mem.writeInt(u32, out[8..12], x[2][2], .little);
            mem.writeInt(u32, out[12..16], x[3][3], .little);
            mem.writeInt(u32, out[16..20], x[1][2], .little);
            mem.writeInt(u32, out[20..24], x[1][3], .little);
            mem.writeInt(u32, out[24..28], x[2][0], .little);
            mem.writeInt(u32, out[28..32], x[2][1], .little);
            return out;
        }
    };
}

fn SalsaNonVecImpl(comptime rounds: comptime_int) type {
    return struct {
        const BlockVec = [16]u32;

        fn initContext(key: [8]u32, d: [4]u32) BlockVec {
            const c = "expand 32-byte k";
            const constant_le = comptime [4]u32{
                mem.readInt(u32, c[0..4], .little),
                mem.readInt(u32, c[4..8], .little),
                mem.readInt(u32, c[8..12], .little),
                mem.readInt(u32, c[12..16], .little),
            };
            return BlockVec{
                constant_le[0], key[0],         key[1],         key[2],
                key[3],         constant_le[1], d[0],           d[1],
                d[2],           d[3],           constant_le[2], key[4],
                key[5],         key[6],         key[7],         constant_le[3],
            };
        }

        const QuarterRound = struct {
            a: usize,
            b: usize,
            c: usize,
            d: u6,
        };

        fn Rp(a: usize, b: usize, c: usize, d: u6) QuarterRound {
            return QuarterRound{
                .a = a,
                .b = b,
                .c = c,
                .d = d,
            };
        }

        fn salsaCore(x: *BlockVec, input: BlockVec, comptime feedback: bool) void {
            const arx_steps = comptime [_]QuarterRound{
                Rp(4, 0, 12, 7),   Rp(8, 4, 0, 9),    Rp(12, 8, 4, 13),   Rp(0, 12, 8, 18),
                Rp(9, 5, 1, 7),    Rp(13, 9, 5, 9),   Rp(1, 13, 9, 13),   Rp(5, 1, 13, 18),
                Rp(14, 10, 6, 7),  Rp(2, 14, 10, 9),  Rp(6, 2, 14, 13),   Rp(10, 6, 2, 18),
                Rp(3, 15, 11, 7),  Rp(7, 3, 15, 9),   Rp(11, 7, 3, 13),   Rp(15, 11, 7, 18),
                Rp(1, 0, 3, 7),    Rp(2, 1, 0, 9),    Rp(3, 2, 1, 13),    Rp(0, 3, 2, 18),
                Rp(6, 5, 4, 7),    Rp(7, 6, 5, 9),    Rp(4, 7, 6, 13),    Rp(5, 4, 7, 18),
                Rp(11, 10, 9, 7),  Rp(8, 11, 10, 9),  Rp(9, 8, 11, 13),   Rp(10, 9, 8, 18),
                Rp(12, 15, 14, 7), Rp(13, 12, 15, 9), Rp(14, 13, 12, 13), Rp(15, 14, 13, 18),
            };
            x.* = input;
            var j: usize = 0;
            while (j < rounds) : (j += 2) {
                inline for (arx_steps) |r| {
                    x[r.a] ^= math.rotl(u32, x[r.b] +% x[r.c], r.d);
                }
            }
            if (feedback) {
                j = 0;
                while (j < 16) : (j += 1) {
                    x[j] +%= input[j];
                }
            }
        }

        fn hashToBytes(out: *[64]u8, x: BlockVec) void {
            for (x, 0..) |w, i| {
                mem.writeInt(u32, out[i * 4 ..][0..4], w, .little);
            }
        }

        fn salsaXor(out: []u8, in: []const u8, key: [8]u32, d: [4]u32) void {
            var ctx = initContext(key, d);
            var x: BlockVec = undefined;
            var buf: [64]u8 = undefined;
            var i: usize = 0;
            while (i + 64 <= in.len) : (i += 64) {
                salsaCore(x[0..], ctx, true);
                hashToBytes(buf[0..], x);
                var xout = out[i..];
                const xin = in[i..];
                var j: usize = 0;
                while (j < 64) : (j += 1) {
                    xout[j] = xin[j];
                }
                j = 0;
                while (j < 64) : (j += 1) {
                    xout[j] ^= buf[j];
                }
                const ov = @addWithOverflow(ctx[8], 1);
                ctx[8] = ov[0];
                ctx[9] += ov[1];
            }
            if (i < in.len) {
                salsaCore(x[0..], ctx, true);
                hashToBytes(buf[0..], x);

                var xout = out[i..];
                const xin = in[i..];
                var j: usize = 0;
                while (j < in.len % 64) : (j += 1) {
                    xout[j] = xin[j] ^ buf[j];
                }
            }
        }

        fn hsalsa(input: [16]u8, key: [32]u8) [32]u8 {
            var c: [4]u32 = undefined;
            for (c, 0..) |_, i| {
                c[i] = mem.readInt(u32, input[4 * i ..][0..4], .little);
            }
            const ctx = initContext(keyToWords(key), c);
            var x: BlockVec = undefined;
            salsaCore(x[0..], ctx, false);
            var out: [32]u8 = undefined;
            mem.writeInt(u32, out[0..4], x[0], .little);
            mem.writeInt(u32, out[4..8], x[5], .little);
            mem.writeInt(u32, out[8..12], x[10], .little);
            mem.writeInt(u32, out[12..16], x[15], .little);
            mem.writeInt(u32, out[16..20], x[6], .little);
            mem.writeInt(u32, out[20..24], x[7], .little);
            mem.writeInt(u32, out[24..28], x[8], .little);
            mem.writeInt(u32, out[28..32], x[9], .little);
            return out;
        }
    };
}

const SalsaImpl = if (builtin.cpu.arch == .x86_64)
    SalsaVecImpl
else
    SalsaNonVecImpl;

fn keyToWords(key: [32]u8) [8]u32 {
    var k: [8]u32 = undefined;
    var i: usize = 0;
    while (i < 8) : (i += 1) {
        k[i] = mem.readInt(u32, key[i * 4 ..][0..4], .little);
    }
    return k;
}

fn extend(comptime rounds: comptime_int, key: [32]u8, nonce: [24]u8) struct { key: [32]u8, nonce: [8]u8 } {
    return .{
        .key = SalsaImpl(rounds).hsalsa(nonce[0..16].*, key),
        .nonce = nonce[16..24].*,
    };
}

/// The Salsa stream cipher.
pub fn Salsa(comptime rounds: comptime_int) type {
    return struct {
        /// Nonce length in bytes.
        pub const nonce_length = 8;
        /// Key length in bytes.
        pub const key_length = 32;

        /// Add the output of the Salsa stream cipher to `in` and stores the result into `out`.
        /// WARNING: This function doesn't provide authenticated encryption.
        /// Using the AEAD or one of the `box` versions is usually preferred.
        pub fn xor(out: []u8, in: []const u8, counter: u64, key: [key_length]u8, nonce: [nonce_length]u8) void {
            debug.assert(in.len == out.len);

            var d: [4]u32 = undefined;
            d[0] = mem.readInt(u32, nonce[0..4], .little);
            d[1] = mem.readInt(u32, nonce[4..8], .little);
            d[2] = @as(u32, @truncate(counter));
            d[3] = @as(u32, @truncate(counter >> 32));
            SalsaImpl(rounds).salsaXor(out, in, keyToWords(key), d);
        }
    };
}

/// The XSalsa stream cipher.
pub fn XSalsa(comptime rounds: comptime_int) type {
    return struct {
        /// Nonce length in bytes.
        pub const nonce_length = 24;
        /// Key length in bytes.
        pub const key_length = 32;

        /// Add the output of the XSalsa stream cipher to `in` and stores the result into `out`.
        /// WARNING: This function doesn't provide authenticated encryption.
        /// Using the AEAD or one of the `box` versions is usually preferred.
        pub fn xor(out: []u8, in: []const u8, counter: u64, key: [key_length]u8, nonce: [nonce_length]u8) void {
            const extended = extend(rounds, key, nonce);
            Salsa(rounds).xor(out, in, counter, extended.key, extended.nonce);
        }
    };
}

/// The XSalsa stream cipher, combined with the Poly1305 MAC
pub const XSalsa20Poly1305 = struct {
    /// Authentication tag length in bytes.
    pub const tag_length = Poly1305.mac_length;
    /// Nonce length in bytes.
    pub const nonce_length = XSalsa20.nonce_length;
    /// Key length in bytes.
    pub const key_length = XSalsa20.key_length;

    const rounds = 20;

    /// c: ciphertext: output buffer should be of size m.len
    /// tag: authentication tag: output MAC
    /// m: message
    /// ad: Associated Data
    /// npub: public nonce
    /// k: private key
    pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void {
        debug.assert(c.len == m.len);
        const extended = extend(rounds, k, npub);
        var block0 = [_]u8{0} ** 64;
        const mlen0 = @min(32, m.len);
        @memcpy(block0[32..][0..mlen0], m[0..mlen0]);
        Salsa20.xor(block0[0..], block0[0..], 0, extended.key, extended.nonce);
        @memcpy(c[0..mlen0], block0[32..][0..mlen0]);
        Salsa20.xor(c[mlen0..], m[mlen0..], 1, extended.key, extended.nonce);
        var mac = Poly1305.init(block0[0..32]);
        mac.update(ad);
        mac.update(c);
        mac.final(tag);
    }

    /// `m`: Message
    /// `c`: Ciphertext
    /// `tag`: Authentication tag
    /// `ad`: Associated data
    /// `npub`: Public nonce
    /// `k`: Private key
    /// Asserts `c.len == m.len`.
    ///
    /// Contents of `m` are undefined if an error is returned.
    pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) AuthenticationError!void {
        debug.assert(c.len == m.len);
        const extended = extend(rounds, k, npub);
        var block0 = [_]u8{0} ** 64;
        const mlen0 = @min(32, c.len);
        @memcpy(block0[32..][0..mlen0], c[0..mlen0]);
        Salsa20.xor(block0[0..], block0[0..], 0, extended.key, extended.nonce);
        var mac = Poly1305.init(block0[0..32]);
        mac.update(ad);
        mac.update(c);
        var computed_tag: [tag_length]u8 = undefined;
        mac.final(&computed_tag);

        const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
        if (!verify) {
            crypto.secureZero(u8, &computed_tag);
            @memset(m, undefined);
            return error.AuthenticationFailed;
        }
        @memcpy(m[0..mlen0], block0[32..][0..mlen0]);
        Salsa20.xor(m[mlen0..], c[mlen0..], 1, extended.key, extended.nonce);
    }
};

/// NaCl-compatible secretbox API.
///
/// A secretbox contains both an encrypted message and an authentication tag to verify that it hasn't been tampered with.
/// A secret key shared by all the recipients must be already known in order to use this API.
///
/// Nonces are 192-bit large and can safely be chosen with a random number generator.
pub const SecretBox = struct {
    /// Key length in bytes.
    pub const key_length = XSalsa20Poly1305.key_length;
    /// Nonce length in bytes.
    pub const nonce_length = XSalsa20Poly1305.nonce_length;
    /// Authentication tag length in bytes.
    pub const tag_length = XSalsa20Poly1305.tag_length;

    /// Encrypt and authenticate `m` using a nonce `npub` and a key `k`.
    /// `c` must be exactly `tag_length` longer than `m`, as it will store both the ciphertext and the authentication tag.
    pub fn seal(c: []u8, m: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void {
        debug.assert(c.len == tag_length + m.len);
        XSalsa20Poly1305.encrypt(c[tag_length..], c[0..tag_length], m, "", npub, k);
    }

    /// Verify and decrypt `c` using a nonce `npub` and a key `k`.
    /// `m` must be exactly `tag_length` smaller than `c`, as `c` includes an authentication tag in addition to the encrypted message.
    pub fn open(m: []u8, c: []const u8, npub: [nonce_length]u8, k: [key_length]u8) AuthenticationError!void {
        if (c.len < tag_length) {
            return error.AuthenticationFailed;
        }
        debug.assert(m.len == c.len - tag_length);
        return XSalsa20Poly1305.decrypt(m, c[tag_length..], c[0..tag_length].*, "", npub, k);
    }
};

/// NaCl-compatible box API.
///
/// A secretbox contains both an encrypted message and an authentication tag to verify that it hasn't been tampered with.
/// This construction uses public-key cryptography. A shared secret doesn't have to be known in advance by both parties.
/// Instead, a message is encrypted using a sender's secret key and a recipient's public key,
/// and is decrypted using the recipient's secret key and the sender's public key.
///
/// Nonces are 192-bit large and can safely be chosen with a random number generator.
pub const Box = struct {
    /// Public key length in bytes.
    pub const public_length = X25519.public_length;
    /// Secret key length in bytes.
    pub const secret_length = X25519.secret_length;
    /// Shared key length in bytes.
    pub const shared_length = XSalsa20Poly1305.key_length;
    /// Seed (for key pair creation) length in bytes.
    pub const seed_length = X25519.seed_length;
    /// Nonce length in bytes.
    pub const nonce_length = XSalsa20Poly1305.nonce_length;
    /// Authentication tag length in bytes.
    pub const tag_length = XSalsa20Poly1305.tag_length;

    /// A key pair.
    pub const KeyPair = X25519.KeyPair;

    /// Compute a secret suitable for `secretbox` given a recipient's public key and a sender's secret key.
    pub fn createSharedSecret(public_key: [public_length]u8, secret_key: [secret_length]u8) (IdentityElementError || WeakPublicKeyError)![shared_length]u8 {
        const p = try X25519.scalarmult(secret_key, public_key);
        const zero = [_]u8{0} ** 16;
        return SalsaImpl(20).hsalsa(zero, p);
    }

    /// Encrypt and authenticate a message using a recipient's public key `public_key` and a sender's `secret_key`.
    pub fn seal(c: []u8, m: []const u8, npub: [nonce_length]u8, public_key: [public_length]u8, secret_key: [secret_length]u8) (IdentityElementError || WeakPublicKeyError)!void {
        const shared_key = try createSharedSecret(public_key, secret_key);
        return SecretBox.seal(c, m, npub, shared_key);
    }

    /// Verify and decrypt a message using a recipient's secret key `public_key` and a sender's `public_key`.
    pub fn open(m: []u8, c: []const u8, npub: [nonce_length]u8, public_key: [public_length]u8, secret_key: [secret_length]u8) (IdentityElementError || WeakPublicKeyError || AuthenticationError)!void {
        const shared_key = try createSharedSecret(public_key, secret_key);
        return SecretBox.open(m, c, npub, shared_key);
    }
};

/// libsodium-compatible sealed boxes
///
/// Sealed boxes are designed to anonymously send messages to a recipient given their public key.
/// Only the recipient can decrypt these messages, using their private key.
/// While the recipient can verify the integrity of the message, it cannot verify the identity of the sender.
///
/// A message is encrypted using an ephemeral key pair, whose secret part is destroyed right after the encryption process.
pub const SealedBox = struct {
    pub const public_length = Box.public_length;
    pub const secret_length = Box.secret_length;
    pub const seed_length = Box.seed_length;
    pub const seal_length = Box.public_length + Box.tag_length;

    /// A key pair.
    pub const KeyPair = Box.KeyPair;

    fn createNonce(pk1: [public_length]u8, pk2: [public_length]u8) [Box.nonce_length]u8 {
        var hasher = Blake2b(Box.nonce_length * 8).init(.{});
        hasher.update(&pk1);
        hasher.update(&pk2);
        var nonce: [Box.nonce_length]u8 = undefined;
        hasher.final(&nonce);
        return nonce;
    }

    /// Encrypt a message `m` for a recipient whose public key is `public_key`.
    /// `c` must be `seal_length` bytes larger than `m`, so that the required metadata can be added.
    pub fn seal(io: std.Io, c: []u8, m: []const u8, public_key: [public_length]u8) (WeakPublicKeyError || IdentityElementError)!void {
        debug.assert(c.len == m.len + seal_length);
        var ekp = KeyPair.generate(io);
        const nonce = createNonce(ekp.public_key, public_key);
        c[0..public_length].* = ekp.public_key;
        try Box.seal(c[Box.public_length..], m, nonce, public_key, ekp.secret_key);
        crypto.secureZero(u8, ekp.secret_key[0..]);
    }

    /// Decrypt a message using a key pair.
    /// `m` must be exactly `seal_length` bytes smaller than `c`, as `c` also includes metadata.
    pub fn open(m: []u8, c: []const u8, keypair: KeyPair) (IdentityElementError || WeakPublicKeyError || AuthenticationError)!void {
        if (c.len < seal_length) {
            return error.AuthenticationFailed;
        }
        const epk = c[0..public_length];
        const nonce = createNonce(epk.*, keypair.public_key);
        return Box.open(m, c[public_length..], nonce, epk.*, keypair.secret_key);
    }
};

const htest = @import("test.zig");

test "(x)salsa20" {
    if (builtin.cpu.has(.riscv, .v) and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/24299

    const key = [_]u8{0x69} ** 32;
    const nonce = [_]u8{0x42} ** 8;
    const msg = [_]u8{0} ** 20;
    var c: [msg.len]u8 = undefined;

    Salsa20.xor(&c, msg[0..], 0, key, nonce);
    try htest.assertEqual("30ff9933aa6534ff5207142593cd1fca4b23bdd8", c[0..]);

    const extended_nonce = [_]u8{0x42} ** 24;
    XSalsa20.xor(&c, msg[0..], 0, key, extended_nonce);
    try htest.assertEqual("b4ab7d82e750ec07644fa3281bce6cd91d4243f9", c[0..]);
}

test "xsalsa20poly1305" {
    const io = std.testing.io;
    var msg: [100]u8 = undefined;
    var msg2: [msg.len]u8 = undefined;
    var c: [msg.len]u8 = undefined;
    var key: [XSalsa20Poly1305.key_length]u8 = undefined;
    var nonce: [XSalsa20Poly1305.nonce_length]u8 = undefined;
    var tag: [XSalsa20Poly1305.tag_length]u8 = undefined;
    io.random(&msg);
    io.random(&key);
    io.random(&nonce);

    XSalsa20Poly1305.encrypt(c[0..], &tag, msg[0..], "ad", nonce, key);
    try XSalsa20Poly1305.decrypt(msg2[0..], c[0..], tag, "ad", nonce, key);
}

test "xsalsa20poly1305 secretbox" {
    const io = std.testing.io;
    var msg: [100]u8 = undefined;
    var msg2: [msg.len]u8 = undefined;
    var key: [XSalsa20Poly1305.key_length]u8 = undefined;
    var nonce: [Box.nonce_length]u8 = undefined;
    var boxed: [msg.len + Box.tag_length]u8 = undefined;
    io.random(&msg);
    io.random(&key);
    io.random(&nonce);

    SecretBox.seal(boxed[0..], msg[0..], nonce, key);
    try SecretBox.open(msg2[0..], boxed[0..], nonce, key);
}

test "xsalsa20poly1305 box" {
    if (builtin.cpu.has(.riscv, .v) and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/24299

    const io = std.testing.io;
    var msg: [100]u8 = undefined;
    var msg2: [msg.len]u8 = undefined;
    var nonce: [Box.nonce_length]u8 = undefined;
    var boxed: [msg.len + Box.tag_length]u8 = undefined;
    io.random(&msg);
    io.random(&nonce);

    const kp1 = Box.KeyPair.generate(io);
    const kp2 = Box.KeyPair.generate(io);
    try Box.seal(boxed[0..], msg[0..], nonce, kp1.public_key, kp2.secret_key);
    try Box.open(msg2[0..], boxed[0..], nonce, kp2.public_key, kp1.secret_key);
}

test "xsalsa20poly1305 sealedbox" {
    if (builtin.cpu.has(.riscv, .v) and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/24299

    const io = std.testing.io;
    var msg: [100]u8 = undefined;
    var msg2: [msg.len]u8 = undefined;
    var boxed: [msg.len + SealedBox.seal_length]u8 = undefined;
    io.random(&msg);

    const kp = Box.KeyPair.generate(io);
    try SealedBox.seal(io, boxed[0..], msg[0..], kp.public_key);
    try SealedBox.open(msg2[0..], boxed[0..], kp);
}

test "secretbox twoblocks" {
    const key = [_]u8{ 0xc9, 0xc9, 0x4d, 0xcf, 0x68, 0xbe, 0x00, 0xe4, 0x7f, 0xe6, 0x13, 0x26, 0xfc, 0xc4, 0x2f, 0xd0, 0xdb, 0x93, 0x91, 0x1c, 0x09, 0x94, 0x89, 0xe1, 0x1b, 0x88, 0x63, 0x18, 0x86, 0x64, 0x8b, 0x7b };
    const nonce = [_]u8{ 0xa4, 0x33, 0xe9, 0x0a, 0x07, 0x68, 0x6e, 0x9a, 0x2b, 0x6d, 0xd4, 0x59, 0x04, 0x72, 0x3e, 0xd3, 0x8a, 0x67, 0x55, 0xc7, 0x9e, 0x3e, 0x77, 0xdc };
    const msg = [_]u8{'a'} ** 97;
    var ciphertext: [msg.len + SecretBox.tag_length]u8 = undefined;
    SecretBox.seal(&ciphertext, &msg, nonce, key);
    try htest.assertEqual("b05760e217288ba079caa2fd57fd3701784974ffcfda20fe523b89211ad8af065a6eb37cdb29d51aca5bd75dafdd21d18b044c54bb7c526cf576c94ee8900f911ceab0147e82b667a28c52d58ceb29554ff45471224d37b03256b01c119b89ff6d36855de8138d103386dbc9d971f52261", &ciphertext);
}
