// This file is dual licensed under the terms of the Apache License, Version
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.

use pyo3::types::{PyAnyMethods, PyListMethods};

use crate::buf::{CffiBuf, CffiMutBuf};
use crate::error::{CryptographyError, CryptographyResult};
use crate::exceptions;

fn check_length(data: &[u8]) -> CryptographyResult<()> {
    if data.len() > (i32::MAX as usize) {
        // This is OverflowError to match what cffi would raise
        return Err(CryptographyError::from(
            pyo3::exceptions::PyOverflowError::new_err(
                "Data or associated data too long. Max 2**31 - 1 bytes",
            ),
        ));
    }

    Ok(())
}

pub(crate) enum Aad<'a> {
    Single(CffiBuf<'a>),
    List(pyo3::Bound<'a, pyo3::types::PyList>),
}

pub(crate) enum AeadCipher {
    Static(&'static openssl::cipher::CipherRef),
    #[cfg(not(any(
        CRYPTOGRAPHY_IS_LIBRESSL,
        CRYPTOGRAPHY_IS_BORINGSSL,
        CRYPTOGRAPHY_IS_AWSLC
    )))]
    Fetched(openssl::cipher::Cipher),
}

impl std::ops::Deref for AeadCipher {
    type Target = openssl::cipher::CipherRef;

    fn deref(&self) -> &openssl::cipher::CipherRef {
        match self {
            AeadCipher::Static(c) => c,
            #[cfg(not(any(
                CRYPTOGRAPHY_IS_LIBRESSL,
                CRYPTOGRAPHY_IS_BORINGSSL,
                CRYPTOGRAPHY_IS_AWSLC
            )))]
            AeadCipher::Fetched(c) => c,
        }
    }
}

pub(crate) struct EvpCipherAead {
    cipher: AeadCipher,
    key: pyo3::Py<pyo3::PyAny>,
    base_encryption_ctx: openssl::cipher_ctx::CipherCtx,
    base_decryption_ctx: openssl::cipher_ctx::CipherCtx,
    tag_len: usize,
    tag_first: bool,
    is_ccm: bool,
}

impl EvpCipherAead {
    pub(crate) fn new(
        py: pyo3::Python<'_>,
        cipher: AeadCipher,
        key: pyo3::Py<pyo3::PyAny>,
        tag_len: usize,
        tag_first: bool,
        is_ccm: bool,
    ) -> CryptographyResult<EvpCipherAead> {
        let mut base_encryption_ctx = openssl::cipher_ctx::CipherCtx::new()?;
        let mut base_decryption_ctx = openssl::cipher_ctx::CipherCtx::new()?;
        // CCM requires the IV and tag lengths to be set before the key, so
        // contexts can't be pre-keyed. The base contexts are left
        // uninitialized and every operation initializes a context from
        // scratch.
        if !is_ccm {
            let key_buf = key.extract::<CffiBuf<'_>>(py)?;
            base_encryption_ctx.encrypt_init(Some(&cipher), Some(key_buf.as_bytes()), None)?;
            base_decryption_ctx.decrypt_init(Some(&cipher), Some(key_buf.as_bytes()), None)?;
        }

        Ok(EvpCipherAead {
            cipher,
            key,
            base_encryption_ctx,
            base_decryption_ctx,
            tag_len,
            tag_first,
            is_ccm,
        })
    }

    fn process_aad(
        ctx: &mut openssl::cipher_ctx::CipherCtx,
        aad: Option<Aad<'_>>,
    ) -> CryptographyResult<()> {
        match aad {
            Some(Aad::Single(ad)) => {
                check_length(ad.as_bytes())?;
                ctx.cipher_update(ad.as_bytes(), None)?;
            }
            Some(Aad::List(ads)) => {
                for ad in ads.iter() {
                    let ad = ad.extract::<CffiBuf<'_>>()?;
                    check_length(ad.as_bytes())?;
                    ctx.cipher_update(ad.as_bytes(), None)?;
                }
            }
            None => {}
        }

        Ok(())
    }

    fn process_data(
        ctx: &mut openssl::cipher_ctx::CipherCtx,
        data: &[u8],
        out: &mut [u8],
        is_ccm: bool,
    ) -> CryptographyResult<()> {
        let bs = ctx.block_size();

        // For AEADs that operate as if they are streaming there's an easy
        // path. For AEADs that are more like block ciphers (notably, OCB),
        // this is a bit more complicated.
        if bs == 1 {
            let n = ctx.cipher_update(data, Some(out))?;
            assert_eq!(n, data.len());

            if !is_ccm {
                let mut final_block = [0];
                let n = ctx.cipher_final(&mut final_block)?;
                assert_eq!(n, 0);
            }
        } else {
            // Our algorithm here is: split the data into the full chunks, and
            // the remaining partial chunk. Feed the full chunks into OpenSSL
            // and let it write the results to `out`. Then feed the trailer
            // in, allowing it to write the results to a buffer on the
            // stack -- this never writes anything. Finally, finalize the AEAD
            // and let it write the results to the stack buffer, then copy
            // from the stack buffer over to `out`. The indirection via the
            // stack buffer is required because OpenSSL uses it as scratch
            // space, and `out` wouldn't be long enough.
            let (initial, trailer) = data.split_at((data.len() / bs) * bs);

            let n =
                // SAFETY: `initial.len()` is a precise multiple of the block
                // size, which means the space required in the output is
                // exactly `initial.len()`.
                unsafe { ctx.cipher_update_unchecked(initial, Some(&mut out[..initial.len()]))? };
            assert_eq!(n, initial.len());

            assert!(bs <= 16);
            let mut buf = [0; 32];
            let n = ctx.cipher_update(trailer, Some(&mut buf))?;
            assert_eq!(n, 0);

            let n = ctx.cipher_final(&mut buf)?;
            assert_eq!(n, trailer.len());
            out[initial.len()..].copy_from_slice(&buf[..n]);
        }

        Ok(())
    }

    pub(crate) fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        plaintext: &[u8],
        aad: Option<Aad<'_>>,
        nonce: Option<&[u8]>,
        buf: &mut [u8],
    ) -> CryptographyResult<()> {
        check_length(plaintext)?;

        let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
        if self.is_ccm || ctx.copy(&self.base_encryption_ctx).is_err() {
            // Copying the pre-keyed context isn't possible: either this is
            // CCM (whose base contexts are uninitialized), or this OpenSSL
            // doesn't support copying AEAD contexts (providers prior to
            // OpenSSL 3.2). Initialize a new context from scratch.
            ctx = openssl::cipher_ctx::CipherCtx::new()?;
            let key_buf = self.key.extract::<CffiBuf<'_>>(py)?;
            if self.is_ccm {
                ctx.encrypt_init(Some(&self.cipher), None, None)?;
                ctx.set_iv_length(nonce.as_ref().unwrap().len())?;
                ctx.set_tag_length(self.tag_len)?;
                ctx.encrypt_init(None, Some(key_buf.as_bytes()), nonce)?;
            } else {
                ctx.encrypt_init(Some(&self.cipher), Some(key_buf.as_bytes()), None)?;
            }
        }

        if self.is_ccm {
            ctx.set_data_len(plaintext.len())?;
        } else {
            if let Some(nonce) = nonce {
                ctx.set_iv_length(nonce.len())?;
            }
            ctx.encrypt_init(None, None, nonce)?;
        }

        Self::process_aad(&mut ctx, aad)?;

        let ciphertext;
        let tag;
        if self.tag_first {
            (tag, ciphertext) = buf.split_at_mut(self.tag_len);
        } else {
            (ciphertext, tag) = buf.split_at_mut(plaintext.len());
        }

        Self::process_data(&mut ctx, plaintext, ciphertext, self.is_ccm)?;

        ctx.tag(tag).map_err(CryptographyError::from)?;

        Ok(())
    }

    pub(crate) fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        ciphertext: &[u8],
        aad: Option<Aad<'_>>,
        nonce: Option<&[u8]>,
        buf: &mut [u8],
    ) -> CryptographyResult<()> {
        let tag;
        let ciphertext_data;
        if self.tag_first {
            // RFC 5297 defines the output as IV || C, where the tag we generate
            // is the "IV" and C is the ciphertext. This is the opposite of our
            // other AEADs, which are Ciphertext || Tag.
            (tag, ciphertext_data) = ciphertext.split_at(self.tag_len);
        } else {
            (ciphertext_data, tag) = ciphertext.split_at(ciphertext.len() - self.tag_len);
        }

        let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
        if self.is_ccm || ctx.copy(&self.base_decryption_ctx).is_err() {
            // Copying the pre-keyed context isn't possible: either this is
            // CCM (whose base contexts are uninitialized), or this OpenSSL
            // doesn't support copying AEAD contexts (providers prior to
            // OpenSSL 3.2). Initialize a new context from scratch.
            ctx = openssl::cipher_ctx::CipherCtx::new()?;
            let key_buf = self.key.extract::<CffiBuf<'_>>(py)?;
            if self.is_ccm {
                ctx.decrypt_init(Some(&self.cipher), None, None)?;
                ctx.set_iv_length(nonce.as_ref().unwrap().len())?;
                ctx.set_tag(tag)?;
                ctx.decrypt_init(None, Some(key_buf.as_bytes()), nonce)?;
            } else {
                ctx.decrypt_init(Some(&self.cipher), Some(key_buf.as_bytes()), None)?;
            }
        }

        if self.is_ccm {
            ctx.set_data_len(ciphertext_data.len())?;
        } else {
            if let Some(nonce) = nonce {
                ctx.set_iv_length(nonce.len())?;
            }

            ctx.decrypt_init(None, None, nonce)?;
            ctx.set_tag(tag)?;
        }

        Self::process_aad(&mut ctx, aad)?;

        Self::process_data(&mut ctx, ciphertext_data, buf, self.is_ccm)
            .map_err(|_| exceptions::InvalidTag::new_err(()))?;

        Ok(())
    }
}

#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
struct EvpAead {
    ctx: cryptography_openssl::aead::AeadCtx,
    tag_len: usize,
}

#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
impl EvpAead {
    fn new(
        algorithm: cryptography_openssl::aead::AeadType,
        key: &[u8],
        tag_len: usize,
    ) -> CryptographyResult<EvpAead> {
        Ok(EvpAead {
            ctx: cryptography_openssl::aead::AeadCtx::new(algorithm, key)?,
            tag_len,
        })
    }

    fn encrypt_into(
        &self,
        _py: pyo3::Python<'_>,
        plaintext: &[u8],
        aad: Option<Aad<'_>>,
        nonce: Option<&[u8]>,
        buf: &mut [u8],
    ) -> CryptographyResult<()> {
        check_length(plaintext)?;

        let ad = if let Some(Aad::Single(ad)) = &aad {
            check_length(ad.as_bytes())?;
            ad.as_bytes()
        } else {
            assert!(aad.is_none());
            b""
        };
        self.ctx
            .encrypt(plaintext, nonce.unwrap_or(b""), ad, buf)
            .map_err(CryptographyError::from)?;
        Ok(())
    }

    fn decrypt_into(
        &self,
        _py: pyo3::Python<'_>,
        ciphertext: &[u8],
        aad: Option<Aad<'_>>,
        nonce: Option<&[u8]>,
        buf: &mut [u8],
    ) -> CryptographyResult<()> {
        let ad = if let Some(Aad::Single(ad)) = &aad {
            check_length(ad.as_bytes())?;
            ad.as_bytes()
        } else {
            assert!(aad.is_none());
            b""
        };

        self.ctx
            .decrypt(ciphertext, nonce.unwrap_or(b""), ad, buf)
            .map_err(|_| exceptions::InvalidTag::new_err(()))?;

        Ok(())
    }
}

#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.aead")]
pub(crate) struct ChaCha20Poly1305 {
    #[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
    ctx: EvpAead,
    #[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))]
    ctx: EvpCipherAead,
}

#[pyo3::pymethods]
impl ChaCha20Poly1305 {
    #[new]
    pub(crate) fn new(
        py: pyo3::Python<'_>,
        key: pyo3::Py<pyo3::PyAny>,
    ) -> CryptographyResult<Self> {
        let key_buf = key.extract::<CffiBuf<'_>>(py)?;
        if key_buf.as_bytes().len() != 32 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("ChaCha20Poly1305 key must be 32 bytes."),
            ));
        }
        if cryptography_openssl::fips::is_enabled() {
            return Err(CryptographyError::from(
                exceptions::UnsupportedAlgorithm::new_err((
                    "ChaCha20Poly1305 is not supported by this version of OpenSSL",
                    exceptions::Reasons::UNSUPPORTED_CIPHER,
                )),
            ));
        }

        cfg_if::cfg_if! {
            if #[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))] {
                Ok(ChaCha20Poly1305 {
                    ctx: EvpAead::new(
                        cryptography_openssl::aead::AeadType::ChaCha20Poly1305,
                        key_buf.as_bytes(),
                        16,
                    )?,
                })
            } else {
                Ok(ChaCha20Poly1305 {
                    ctx: EvpCipherAead::new(
                        py,
                        AeadCipher::Static(openssl::cipher::Cipher::chacha20_poly1305()),
                        key,
                        16,
                        false,
                        false,
                    )?,
                })
            }
        }
    }

    #[staticmethod]
    fn generate_key(
        py: pyo3::Python<'_>,
    ) -> CryptographyResult<pyo3::Bound<'_, pyo3::types::PyBytes>> {
        crate::backend::rand::get_rand_bytes(py, 32)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    pub(crate) fn encrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let data_bytes = data.as_bytes();
        check_length(data_bytes)?;
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() + self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.encrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    pub(crate) fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() != 12 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes"),
            ));
        }

        // Check this early so we know we can add tag_len without overflow
        // check_length requires that the length be 2 ** 31 - 1 or smaller.
        check_length(data_bytes)?;
        let expected_len = data_bytes.len() + 16;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .encrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
        Ok(expected_len)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    pub(crate) fn decrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        if nonce.as_bytes().len() != 12 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes"),
            ));
        }
        if data.as_bytes().len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data.as_bytes().len() - self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.decrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() != 12 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes"),
            ));
        }

        if data.as_bytes().len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        let expected_len = data_bytes.len() - self.ctx.tag_len;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;

        Ok(expected_len)
    }
}

// NO-COVERAGE-START
#[pyo3::pyclass(
    frozen,
    module = "cryptography.hazmat.bindings._rust.openssl.aead",
    name = "AESGCM"
)]
// NO-COVERAGE-END
pub(crate) struct AesGcm {
    ctx: EvpCipherAead,
}

#[pyo3::pymethods]
impl AesGcm {
    #[new]
    pub(crate) fn new(
        py: pyo3::Python<'_>,
        key: pyo3::Py<pyo3::PyAny>,
    ) -> CryptographyResult<AesGcm> {
        let key_buf = key.extract::<CffiBuf<'_>>(py)?;
        let cipher = match key_buf.as_bytes().len() {
            16 => openssl::cipher::Cipher::aes_128_gcm(),
            24 => openssl::cipher::Cipher::aes_192_gcm(),
            32 => openssl::cipher::Cipher::aes_256_gcm(),
            _ => {
                return Err(CryptographyError::from(
                    pyo3::exceptions::PyValueError::new_err(
                        "AESGCM key must be 128, 192, or 256 bits.",
                    ),
                ))
            }
        };

        Ok(AesGcm {
            ctx: EvpCipherAead::new(py, AeadCipher::Static(cipher), key, 16, false, false)?,
        })
    }

    #[staticmethod]
    fn generate_key(
        py: pyo3::Python<'_>,
        bit_length: usize,
    ) -> CryptographyResult<pyo3::Bound<'_, pyo3::types::PyBytes>> {
        if bit_length != 128 && bit_length != 192 && bit_length != 256 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("bit_length must be 128, 192, or 256"),
            ));
        }

        crate::backend::rand::get_rand_bytes(py, bit_length / 8)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    pub(crate) fn encrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let data_bytes = data.as_bytes();
        check_length(data_bytes)?;
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() + self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.encrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    pub(crate) fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 8 and 128 bytes"),
            ));
        }

        // Check this early so we know we can add tag_len without overflow
        // check_length requires that the length be 2 ** 31 - 1 or smaller.
        check_length(data_bytes)?;
        let expected_len = data_bytes.len() + 16;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .encrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
        Ok(expected_len)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    pub(crate) fn decrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();

        if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 8 and 128 bytes"),
            ));
        }

        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() - self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.decrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 8 and 128 bytes"),
            ));
        }

        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        let expected_len = data_bytes.len() - self.ctx.tag_len;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;

        Ok(expected_len)
    }
}

// NO-COVERAGE-START
#[pyo3::pyclass(
    frozen,
    module = "cryptography.hazmat.bindings._rust.openssl.aead",
    name = "AESCCM"
)]
// NO-COVERAGE-END
struct AesCcm {
    ctx: EvpCipherAead,
    tag_length: usize,
}

#[pyo3::pymethods]
impl AesCcm {
    #[new]
    #[pyo3(signature = (key, tag_length=None))]
    fn new(
        py: pyo3::Python<'_>,
        key: pyo3::Py<pyo3::PyAny>,
        tag_length: Option<usize>,
    ) -> CryptographyResult<AesCcm> {
        cfg_if::cfg_if! {
            if #[cfg(CRYPTOGRAPHY_IS_BORINGSSL)] {
                let _ = py;
                let _ = key;
                let _ = tag_length;
                Err(CryptographyError::from(
                    exceptions::UnsupportedAlgorithm::new_err((
                        "AES-CCM is not supported by this version of OpenSSL",
                        exceptions::Reasons::UNSUPPORTED_CIPHER,
                    )),
                ))
            } else {
                let key_buf = key.extract::<CffiBuf<'_>>(py)?;
                let cipher = match key_buf.as_bytes().len() {
                    16 => openssl::cipher::Cipher::aes_128_ccm(),
                    24 => openssl::cipher::Cipher::aes_192_ccm(),
                    32 => openssl::cipher::Cipher::aes_256_ccm(),
                    _ => {
                        return Err(CryptographyError::from(
                            pyo3::exceptions::PyValueError::new_err(
                                "AESCCM key must be 128, 192, or 256 bits.",
                            ),
                        ))
                    }
                };
                let tag_length = tag_length.unwrap_or(16);
                if ![4, 6, 8, 10, 12, 14, 16].contains(&tag_length) {
                    return Err(CryptographyError::from(
                        pyo3::exceptions::PyValueError::new_err("Invalid tag_length"),
                    ));
                }

                Ok(AesCcm {
                    ctx: EvpCipherAead::new(
                        py,
                        AeadCipher::Static(cipher),
                        key,
                        tag_length,
                        false,
                        true,
                    )?,
                    tag_length
                })
            }
        }
    }

    #[staticmethod]
    fn generate_key(
        py: pyo3::Python<'_>,
        bit_length: usize,
    ) -> CryptographyResult<pyo3::Bound<'_, pyo3::types::PyBytes>> {
        if bit_length != 128 && bit_length != 192 && bit_length != 256 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("bit_length must be 128, 192, or 256"),
            ));
        }
        crate::backend::rand::get_rand_bytes(py, bit_length / 8)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    fn encrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let data_bytes = data.as_bytes();
        check_length(data_bytes)?;
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() + self.tag_length,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.encrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() < 7 || nonce_bytes.len() > 13 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 7 and 13 bytes"),
            ));
        }

        // Check this early so we know we can add tag_len without overflow
        // check_length requires that the length be 2 ** 31 - 1 or smaller.
        check_length(data_bytes)?;
        // For information about computing this, see
        // https://tools.ietf.org/html/rfc3610#section-2.1
        let l_val = 15 - nonce_bytes.len();
        let max_length = 1usize.checked_shl(8 * l_val as u32);
        // If `max_length` overflowed, then it's not possible for data to be
        // longer than it.
        if max_length.map(|v| v < data_bytes.len()).unwrap_or(false) {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Data too long for nonce"),
            ));
        }

        let expected_len = data_bytes.len() + self.tag_length;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .encrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
        Ok(expected_len)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    fn decrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();

        if nonce_bytes.len() < 7 || nonce_bytes.len() > 13 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 7 and 13 bytes"),
            ));
        }
        if data_bytes.len() < self.tag_length {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() - self.tag_length,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.decrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() < 7 || nonce_bytes.len() > 13 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 7 and 13 bytes"),
            ));
        }

        if data_bytes.len() < self.tag_length {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        // For information about computing this, see
        // https://tools.ietf.org/html/rfc3610#section-2.1
        let l_val = 15 - nonce_bytes.len();
        let max_length = 1usize.checked_shl(8 * l_val as u32);
        // If `max_length` overflowed, then it's not possible for data to be
        // longer than it.
        let expected_len = data_bytes.len() - self.tag_length;
        if max_length.map(|v| v < expected_len).unwrap_or(false) {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Data too long for nonce"),
            ));
        }

        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;

        Ok(expected_len)
    }
}

// NO-COVERAGE-START
#[pyo3::pyclass(
    frozen,
    module = "cryptography.hazmat.bindings._rust.openssl.aead",
    name = "AESSIV"
)]
// NO-COVERAGE-END
struct AesSiv {
    ctx: EvpCipherAead,
}

#[pyo3::pymethods]
impl AesSiv {
    #[new]
    fn new(py: pyo3::Python<'_>, key: pyo3::Py<pyo3::PyAny>) -> CryptographyResult<AesSiv> {
        let key_buf = key.extract::<CffiBuf<'_>>(py)?;
        let cipher_name = match key_buf.as_bytes().len() {
            32 => "aes-128-siv",
            48 => "aes-192-siv",
            64 => "aes-256-siv",
            _ => {
                return Err(CryptographyError::from(
                    pyo3::exceptions::PyValueError::new_err(
                        "AESSIV key must be 256, 384, or 512 bits.",
                    ),
                ))
            }
        };

        cfg_if::cfg_if! {
            if #[cfg(not(any(CRYPTOGRAPHY_IS_LIBRESSL, CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))] {
                if cryptography_openssl::fips::is_enabled() {
                    return Err(CryptographyError::from(
                        exceptions::UnsupportedAlgorithm::new_err((
                            "AES-SIV is not supported by this version of OpenSSL",
                            exceptions::Reasons::UNSUPPORTED_CIPHER,
                        )),
                    ));
                }

                let cipher = openssl::cipher::Cipher::fetch(None, cipher_name, None)?;
                Ok(AesSiv {
                    ctx: EvpCipherAead::new(
                        py,
                        AeadCipher::Fetched(cipher),
                        key,
                        16,
                        true,
                        false,
                    )?,
                })
            } else {
                _ = cipher_name;

                Err(CryptographyError::from(
                    exceptions::UnsupportedAlgorithm::new_err((
                        "AES-SIV is not supported by this version of OpenSSL",
                        exceptions::Reasons::UNSUPPORTED_CIPHER,
                    )),
                ))
            }
        }
    }

    #[staticmethod]
    fn generate_key(
        py: pyo3::Python<'_>,
        bit_length: usize,
    ) -> CryptographyResult<pyo3::Bound<'_, pyo3::types::PyBytes>> {
        if bit_length != 256 && bit_length != 384 && bit_length != 512 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("bit_length must be 256, 384, or 512"),
            ));
        }

        crate::backend::rand::get_rand_bytes(py, bit_length / 8)
    }

    #[pyo3(signature = (data, associated_data))]
    fn encrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        data: CffiBuf<'_>,
        associated_data: Option<pyo3::Bound<'p, pyo3::types::PyList>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        check_length(data.as_bytes())?;
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data.as_bytes().len() + self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.encrypt_into(py, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (data, associated_data, buf))]
    fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<pyo3::Bound<'_, pyo3::types::PyList>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::List);

        #[cfg(not(CRYPTOGRAPHY_OPENSSL_350_OR_GREATER))]
        if data_bytes.is_empty() {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("data must not be zero length"),
            ));
        };

        // Check this early so we know we can add tag_len without overflow
        // check_length requires that the length be 2 ** 31 - 1 or smaller.
        check_length(data_bytes)?;
        let expected_len = data_bytes.len() + self.ctx.tag_len;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .encrypt_into(py, data_bytes, aad, None, buf.as_mut_bytes())?;

        Ok(expected_len)
    }

    #[pyo3(signature = (data, associated_data))]
    fn decrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        data: CffiBuf<'_>,
        associated_data: Option<pyo3::Bound<'_, pyo3::types::PyList>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        if data.as_bytes().len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data.as_bytes().len() - self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.decrypt_into(py, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (data, associated_data, buf))]
    fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<pyo3::Bound<'_, pyo3::types::PyList>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::List);

        // We need to do this check early to prevent underflow when computing expected_len
        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        let expected_len = data_bytes.len() - self.ctx.tag_len;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .decrypt_into(py, data_bytes, aad, None, buf.as_mut_bytes())?;

        Ok(expected_len)
    }
}

// NO-COVERAGE-START
#[pyo3::pyclass(
    frozen,
    module = "cryptography.hazmat.bindings._rust.openssl.aead",
    name = "AESOCB3"
)]
// NO-COVERAGE-END
struct AesOcb3 {
    ctx: EvpCipherAead,
}

#[pyo3::pymethods]
impl AesOcb3 {
    #[new]
    fn new(py: pyo3::Python<'_>, key: pyo3::Py<pyo3::PyAny>) -> CryptographyResult<AesOcb3> {
        let key_buf = key.extract::<CffiBuf<'_>>(py)?;
        cfg_if::cfg_if! {
            if #[cfg(any(CRYPTOGRAPHY_IS_LIBRESSL, CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))] {
                _ = key_buf;
                _ = key;

                Err(CryptographyError::from(
                    exceptions::UnsupportedAlgorithm::new_err((
                        "AES-OCB3 is not supported by this version of OpenSSL",
                        exceptions::Reasons::UNSUPPORTED_CIPHER,
                    )),
                ))
            } else {
                if cryptography_openssl::fips::is_enabled() {
                    return Err(CryptographyError::from(
                        exceptions::UnsupportedAlgorithm::new_err((
                            "AES-OCB3 is not supported by this version of OpenSSL",
                            exceptions::Reasons::UNSUPPORTED_CIPHER,
                        )),
                    ));
                }

                let cipher = match key_buf.as_bytes().len() {
                    16 => openssl::cipher::Cipher::aes_128_ocb(),
                    24 => openssl::cipher::Cipher::aes_192_ocb(),
                    32 => openssl::cipher::Cipher::aes_256_ocb(),
                    _ => {
                        return Err(CryptographyError::from(
                            pyo3::exceptions::PyValueError::new_err(
                                "AESOCB3 key must be 128, 192, or 256 bits.",
                            ),
                        ))
                    }
                };

                Ok(AesOcb3 {
                    ctx: EvpCipherAead::new(
                        py,
                        AeadCipher::Static(cipher),
                        key,
                        16,
                        false,
                        false,
                    )?,
                })
            }
        }
    }

    #[staticmethod]
    fn generate_key(
        py: pyo3::Python<'_>,
        bit_length: usize,
    ) -> CryptographyResult<pyo3::Bound<'_, pyo3::types::PyBytes>> {
        if bit_length != 128 && bit_length != 192 && bit_length != 256 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("bit_length must be 128, 192, or 256"),
            ));
        }

        crate::backend::rand::get_rand_bytes(py, bit_length / 8)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    fn encrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let data_bytes = data.as_bytes();
        check_length(data_bytes)?;
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() + 16,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.encrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() < 12 || nonce_bytes.len() > 15 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 12 and 15 bytes"),
            ));
        }

        // Check this early so we know we can add tag_len without overflow
        // check_length requires that the length be 2 ** 31 - 1 or smaller.
        check_length(data_bytes)?;
        let expected_len = data_bytes.len() + 16;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .encrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
        Ok(expected_len)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    fn decrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();

        if nonce_bytes.len() < 12 || nonce_bytes.len() > 15 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 12 and 15 bytes"),
            ));
        }

        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() - self.ctx.tag_len,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.decrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() < 12 || nonce_bytes.len() > 15 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be between 12 and 15 bytes"),
            ));
        }

        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        let expected_len = data_bytes.len() - self.ctx.tag_len;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;

        Ok(expected_len)
    }
}

// NO-COVERAGE-START
#[pyo3::pyclass(
    frozen,
    module = "cryptography.hazmat.bindings._rust.openssl.aead",
    name = "AESGCMSIV"
)]
// NO-COVERAGE-END
struct AesGcmSiv {
    #[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
    ctx: EvpAead,
    #[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))]
    ctx: EvpCipherAead,
}

#[pyo3::pymethods]
impl AesGcmSiv {
    #[new]
    fn new(py: pyo3::Python<'_>, key: pyo3::Py<pyo3::PyAny>) -> CryptographyResult<AesGcmSiv> {
        let key_buf = key.extract::<CffiBuf<'_>>(py)?;
        let cipher_name = match key_buf.as_bytes().len() {
            16 => "aes-128-gcm-siv",
            24 => "aes-192-gcm-siv",
            32 => "aes-256-gcm-siv",
            _ => {
                return Err(CryptographyError::from(
                    pyo3::exceptions::PyValueError::new_err(
                        "AES-GCM-SIV key must be 128, 192 or 256 bits.",
                    ),
                ))
            }
        };

        cfg_if::cfg_if! {
            if #[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))] {
                let _ = cipher_name;
                let aead_type = match key_buf.as_bytes().len() {
                    16 => cryptography_openssl::aead::AeadType::Aes128GcmSiv,
                    32 => cryptography_openssl::aead::AeadType::Aes256GcmSiv,
                    _ => return Err(CryptographyError::from(
                        exceptions::UnsupportedAlgorithm::new_err((
                            "Only 128-bit and 256-bit keys are supported for AES-GCM-SIV with AWS-LC or BoringSSL",
                            exceptions::Reasons::UNSUPPORTED_CIPHER,
                        )),
                    ))
                };
                Ok(AesGcmSiv {
                    ctx: EvpAead::new(aead_type, key_buf.as_bytes(), 16)?,
                })
            } else if #[cfg(not(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER))] {
                let _ = cipher_name;
                Err(CryptographyError::from(
                    exceptions::UnsupportedAlgorithm::new_err((
                        "AES-GCM-SIV is not supported by this version of OpenSSL",
                        exceptions::Reasons::UNSUPPORTED_CIPHER,
                    )),
                ))
            } else {
                if cryptography_openssl::fips::is_enabled() {
                    return Err(CryptographyError::from(
                        exceptions::UnsupportedAlgorithm::new_err((
                            "AES-GCM-SIV is not supported by this version of OpenSSL",
                            exceptions::Reasons::UNSUPPORTED_CIPHER,
                        )),
                    ));
                }
                let cipher = openssl::cipher::Cipher::fetch(None, cipher_name, None)?;
                Ok(AesGcmSiv {
                    ctx: EvpCipherAead::new(
                        py,
                        AeadCipher::Fetched(cipher),
                        key,
                        16,
                        false,
                        false,
                    )?,
                })
            }
        }
    }

    #[staticmethod]
    fn generate_key(
        py: pyo3::Python<'_>,
        bit_length: usize,
    ) -> CryptographyResult<pyo3::Bound<'_, pyo3::types::PyBytes>> {
        if bit_length != 128 && bit_length != 192 && bit_length != 256 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("bit_length must be 128, 192, or 256"),
            ));
        }

        crate::backend::rand::get_rand_bytes(py, bit_length / 8)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    fn encrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let data_bytes = data.as_bytes();
        check_length(data_bytes)?;
        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() + 16,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.encrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn encrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        #[cfg(not(any(
            CRYPTOGRAPHY_OPENSSL_350_OR_GREATER,
            CRYPTOGRAPHY_IS_BORINGSSL,
            CRYPTOGRAPHY_IS_AWSLC
        )))]
        if data_bytes.is_empty() {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("data must not be zero length"),
            ));
        };
        if nonce_bytes.len() != 12 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes long"),
            ));
        }

        // Check this early so we know we can add tag_len without overflow
        // check_length requires that the length be 2 ** 31 - 1 or smaller.
        check_length(data_bytes)?;
        let expected_len = data_bytes.len() + 16;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .encrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
        Ok(expected_len)
    }

    #[pyo3(signature = (nonce, data, associated_data))]
    fn decrypt<'p>(
        &self,
        py: pyo3::Python<'p>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
    ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();

        if nonce_bytes.len() != 12 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes long"),
            ));
        }

        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        Ok(pyo3::types::PyBytes::new_with(
            py,
            data_bytes.len() - 16,
            |b| {
                let buf = CffiMutBuf::from_bytes(py, b);
                self.decrypt_into(py, nonce, data, associated_data, buf)?;
                Ok(())
            },
        )?)
    }

    #[pyo3(signature = (nonce, data, associated_data, buf))]
    fn decrypt_into(
        &self,
        py: pyo3::Python<'_>,
        nonce: CffiBuf<'_>,
        data: CffiBuf<'_>,
        associated_data: Option<CffiBuf<'_>>,
        mut buf: CffiMutBuf<'_>,
    ) -> CryptographyResult<usize> {
        let nonce_bytes = nonce.as_bytes();
        let data_bytes = data.as_bytes();
        let aad = associated_data.map(Aad::Single);

        if nonce_bytes.len() != 12 {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes long"),
            ));
        }

        if data_bytes.len() < self.ctx.tag_len {
            return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
        }

        let expected_len = data_bytes.len() - self.ctx.tag_len;
        if buf.as_mut_bytes().len() != expected_len {
            return Err(CryptographyError::from(
                pyo3::exceptions::PyValueError::new_err(format!(
                    "buffer must be {} bytes",
                    expected_len
                )),
            ));
        }

        self.ctx
            .decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;

        Ok(expected_len)
    }
}

#[pyo3::pymodule(gil_used = false)]
pub(crate) mod aead {
    #[pymodule_export]
    use super::{AesCcm, AesGcm, AesGcmSiv, AesOcb3, AesSiv, ChaCha20Poly1305};
}
