// MIT License

// Copyright (c) 2020-2026 The orion Developers

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

//! Message authentication.
//!
//! # Use case:
//! `orion::auth` can be used to ensure message integrity and authenticity by
//! using a secret key.
//!
//! An example of this could be securing APIs by having a user of a given API
//! sign their API request and having the API server verify these signed API
//! requests.
//!
//! # About:
//! - Uses BLAKE2b-256 in keyed mode.
//!
//! # Parameters:
//! - `secret_key`: Secret key used to authenticate `data`.
//! - `data`: Data to be authenticated.
//! - `expected`: The expected authentication [`Tag`].
//!
//! # Errors:
//! An error will be returned if:
//! - The calculated [`Tag`] does not match the expected.
//! - The [`SecretKey`] supplied is less than 32 bytes or greater than 64 bytes.
//! - The expected [`Tag`] is not 32 bytes when verifying.
//!
//! # Panics:
//! A panic will occur if:
//! - More than 2*(2^64-1) bytes of data are authenticated.
//!
//! # Security:
//! - The secret key should always be generated using a CSPRNG.
//!   [`SecretKey::default()`] can be used for
//!   this; it will generate a [`SecretKey`] of 32 bytes.
//! - The required minimum length for a [`SecretKey`] is 32 bytes.
//!
//! # Example:
//! ```rust
//! use orion::auth;
//!
//! // There exists a shared key between the user and API server
//! let key = auth::SecretKey::default();
//!
//! // User generates message and authentication tag
//! let msg = "Some message.".as_bytes();
//! let expected_tag = auth::authenticate(&key, msg)?;
//!
//! // API server verifies the authenticity of the message with the tag
//! assert!(auth::authenticate_verify(&expected_tag, &key, &msg).is_ok());
//! # Ok::<(), orion::errors::UnknownCryptoError>(())
//! ```

#![cfg_attr(docsrs, doc(cfg(feature = "safe_api")))]

pub use super::hltypes::SecretKey;
pub use crate::hazardous::mac::blake2b::Tag;
use crate::{
    errors::UnknownCryptoError,
    hazardous::mac::blake2b::{self, Blake2b},
};

/// The Tag size (bytes) to be output by BLAKE2b in keyed mode.
const BLAKE2B_TAG_SIZE: usize = 32;
/// The minimum `SecretKey` size (bytes) to be used by BLAKE2b in keyed mode.
const BLAKE2B_MIN_KEY_SIZE: usize = 32;

#[must_use = "SECURITY WARNING: Ignoring a Result can have real security implications."]
/// Authenticate a message using BLAKE2b-256 in keyed mode.
pub fn authenticate(secret_key: &SecretKey, data: &[u8]) -> Result<Tag, UnknownCryptoError> {
    if secret_key.len() < BLAKE2B_MIN_KEY_SIZE {
        return Err(UnknownCryptoError);
    }
    let blake2b_secret_key = blake2b::SecretKey::from_slice(secret_key.unprotected_as_bytes())?;
    let mut state = Blake2b::new(&blake2b_secret_key, BLAKE2B_TAG_SIZE)?;
    state.update(data)?;
    state.finalize()
}

#[must_use = "SECURITY WARNING: Ignoring a Result can have real security implications."]
/// Authenticate and verify a message using BLAKE2b-256 in keyed mode.
pub fn authenticate_verify(
    expected: &Tag,
    secret_key: &SecretKey,
    data: &[u8],
) -> Result<(), UnknownCryptoError> {
    if secret_key.len() < BLAKE2B_MIN_KEY_SIZE || expected.len() != BLAKE2B_TAG_SIZE {
        return Err(UnknownCryptoError);
    }
    let key = blake2b::SecretKey::from_slice(secret_key.unprotected_as_bytes())?;
    Blake2b::verify(expected, &key, BLAKE2B_TAG_SIZE, data)
}

// Testing public functions in the module.
#[cfg(test)]
mod public {
    use super::*;

    mod test_auth_and_verify {
        use super::*;
        #[test]
        fn test_authenticate_verify_bad_key() {
            let sec_key_correct = SecretKey::generate(64).unwrap();
            let sec_key_false = SecretKey::default();
            let msg = "what do ya want for nothing?".as_bytes().to_vec();
            let mac_bob = authenticate(&sec_key_correct, &msg).unwrap();

            assert!(authenticate_verify(&mac_bob, &sec_key_correct, &msg).is_ok());
            assert!(authenticate_verify(&mac_bob, &sec_key_false, &msg).is_err());
        }

        #[test]
        fn test_authenticate_verify_bad_msg() {
            let sec_key = SecretKey::generate(64).unwrap();
            let msg = "what do ya want for nothing?".as_bytes().to_vec();
            let mac_bob = authenticate(&sec_key, &msg).unwrap();

            assert!(authenticate_verify(&mac_bob, &sec_key, &msg).is_ok());
            assert!(authenticate_verify(&mac_bob, &sec_key, b"bad msg").is_err());
        }

        #[test]
        fn test_authenticate_key_too_small() {
            let sec_key = SecretKey::generate(31).unwrap();
            let msg = "what do ya want for nothing?".as_bytes().to_vec();

            assert!(authenticate(&sec_key, &msg).is_err());
        }

        #[test]
        fn test_authenticate_verify_key_too_small() {
            let sec_key = SecretKey::generate(31).unwrap();
            let msg = "what do ya want for nothing?".as_bytes().to_vec();
            let mac = Tag::from_slice(&[0u8; 32][..]).unwrap();

            assert!(authenticate_verify(&mac, &sec_key, &msg).is_err());
        }
    }

    #[quickcheck]
    #[cfg(feature = "safe_api")]
    /// Authentication and verifying that tag with the same parameters
    /// should always be true.
    fn prop_authenticate_verify(input: Vec<u8>) -> bool {
        let sk = SecretKey::default();
        let tag = authenticate(&sk, &input[..]).unwrap();
        authenticate_verify(&tag, &sk, &input[..]).is_ok()
    }

    #[quickcheck]
    #[cfg(feature = "safe_api")]
    /// Authentication and verifying that tag with a different key should
    /// never be true.
    fn prop_verify_fail_diff_key(input: Vec<u8>) -> bool {
        let sk = SecretKey::default();
        let sk2 = SecretKey::default();
        let tag = authenticate(&sk, &input[..]).unwrap();

        authenticate_verify(&tag, &sk2, &input[..]).is_err()
    }

    #[quickcheck]
    #[cfg(feature = "safe_api")]
    /// Authentication and verifying that tag with different input should
    /// never be true.
    fn prop_verify_fail_diff_input(input: Vec<u8>) -> bool {
        let sk = SecretKey::default();
        let tag = authenticate(&sk, &input[..]).unwrap();

        authenticate_verify(&tag, &sk, b"Completely wrong input").is_err()
    }

    use crate::hazardous::hash::blake2::blake2b_core::BLAKE2B_KEYSIZE;

    #[quickcheck]
    #[cfg(feature = "safe_api")]
    /// Verify the bounds of 32..=64 (inclusive) for the `SecretKey` used
    /// in `authenticate/authenticate_verify`.
    fn prop_authenticate_key_size(input: Vec<u8>) -> bool {
        let sec_key_res = SecretKey::from_slice(&input);
        if input.is_empty() || input.len() >= u32::MAX as usize {
            return sec_key_res.is_err();
        }
        let sec_key = sec_key_res.unwrap();
        let msg = "what do ya want for nothing?".as_bytes().to_vec();
        let auth_res = authenticate(&sec_key, &msg);
        if input.len() >= BLAKE2B_MIN_KEY_SIZE && input.len() <= BLAKE2B_KEYSIZE {
            auth_res.is_ok()
        } else {
            auth_res.is_err()
        }
    }
}
