proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
// Originally from appattest-rs by Ayodeji Akinola
// https://github.com/TheDhejavu/appattest-rs
// Licensed under the MIT License. See LICENSE-MIT in this directory.

use super::error::AppAttestError;
use byteorder::{BigEndian, ByteOrder};
use sha2::{Digest, Sha256};
use std::error::Error;

#[allow(dead_code)]
pub(crate) struct AuthenticatorData {
    pub(crate) bytes: Vec<u8>,
    pub(crate) rp_id_hash: Vec<u8>,
    pub(crate) flags: u8,
    pub(crate) counter: u32,
    pub(crate) aaguid: Option<AAGUID>,
    pub(crate) credential_id: Option<Vec<u8>>,
}

impl AuthenticatorData {
    pub(crate) fn new(auth_data_byte: Vec<u8>) -> Result<Self, AppAttestError> {
        if auth_data_byte.len() < 37 {
            return Err(AppAttestError::Message(
                "Authenticator data is too short".to_string(),
            ));
        }

        let mut auth_data = AuthenticatorData {
            bytes: auth_data_byte.clone(),
            rp_id_hash: auth_data_byte[0..32].to_vec(),
            flags: auth_data_byte[32],
            counter: BigEndian::read_u32(&auth_data_byte[33..37]),
            aaguid: None,
            credential_id: None,
        };

        auth_data
            .populate_optional_data()
            .map_err(|e| AppAttestError::Message(e.to_string()))?;

        Ok(auth_data)
    }
    fn populate_optional_data(&mut self) -> Result<(), Box<dyn Error>> {
        if self.bytes.len() < 55 {
            return Ok(());
        }

        let length = BigEndian::read_u16(&self.bytes[53..55]) as usize;
        let credential_id = self.bytes[55..55 + length].to_vec();
        let aaguid = AAGUID::new(self.bytes[37..53].to_vec())?;

        self.credential_id = Some(credential_id);
        self.aaguid = Some(aaguid);

        Ok(())
    }
    pub(crate) fn is_valid_aaguid(&self) -> bool {
        let expected_aaguid = APP_ATTEST.as_bytes().to_vec();
        let mut prod_aaguid = expected_aaguid.clone();
        prod_aaguid.extend(std::iter::repeat_n(0x00, 7));

        let dev_aaguid = APP_ATTEST_DEVELOP.as_bytes().to_vec();

        if let Some(aaguid) = &self.aaguid {
            return aaguid.bytes() == expected_aaguid
                || aaguid.bytes() == prod_aaguid
                || aaguid.bytes() == dev_aaguid;
        }

        false
    }

    pub(crate) fn verify_counter(&self) -> Result<(), AppAttestError> {
        if self.counter != 0 {
            return Err(AppAttestError::InvalidCounter);
        }
        Ok(())
    }

    pub(crate) fn verify_app_id(&self, app_id: &str) -> Result<(), AppAttestError> {
        let mut hasher = Sha256::new();
        hasher.update(app_id.as_bytes());
        if self.rp_id_hash != hasher.finalize().as_slice() {
            Err(AppAttestError::InvalidAppID)
        } else {
            Ok(())
        }
    }

    pub(crate) fn verify_key_id(&self, key_id: &[u8]) -> Result<(), AppAttestError> {
        if let Some(credential_id) = &self.credential_id {
            if credential_id == key_id {
                return Ok(());
            }
        }
        Err(AppAttestError::InvalidCredentialID)
    }
}

#[allow(clippy::upper_case_acronyms)]
pub(crate) struct AAGUID(String);

const APP_ATTEST: &str = "appattest";
const APP_ATTEST_DEVELOP: &str = "appattestdevelop";

impl AAGUID {
    fn new(b: Vec<u8>) -> Result<Self, AppAttestError> {
        let ids: [&str; 2] = [APP_ATTEST, APP_ATTEST_DEVELOP];
        for &id in ids.iter() {
            if id.as_bytes() == AAGUID::trim_trailing_zeros(b.as_slice()) {
                return Ok(AAGUID(id.to_string()));
            }
        }
        Err(AppAttestError::InvalidAAGUID)
    }

    fn bytes(&self) -> Vec<u8> {
        self.0.as_bytes().to_vec()
    }

    fn trim_trailing_zeros(bytes: &[u8]) -> &[u8] {
        let mut last_non_zero = None;
        for (index, &value) in bytes.iter().enumerate() {
            if value != 0 {
                last_non_zero = Some(index);
            }
        }

        match last_non_zero {
            Some(index) => &bytes[..=index],
            None => &[],
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_auth_data_new_valid() {
        let mut bytes = vec![0u8; 37];
        bytes[32] = 0b00000001;
        BigEndian::write_u32(&mut bytes[33..37], 1);

        let result = AuthenticatorData::new(bytes);
        assert!(result.is_ok());

        let auth_data = result.unwrap();
        assert_eq!(auth_data.counter, 1);
    }

    #[test]
    fn test_auth_data_new_too_short() {
        let bytes = vec![0u8; 36];
        let result = AuthenticatorData::new(bytes);
        assert!(result.is_err());
    }

    #[test]
    fn test_auth_data_new_exact_minimum() {
        let bytes = vec![0u8; 37];
        let result = AuthenticatorData::new(bytes);
        assert!(result.is_ok());
        let data = result.unwrap();
        assert_eq!(data.counter, 0);
        assert!(data.aaguid.is_none());
        assert!(data.credential_id.is_none());
    }

    #[test]
    fn test_auth_data_counter_value() {
        let mut bytes = vec![0u8; 37];
        BigEndian::write_u32(&mut bytes[33..37], 0xDEADBEEF);
        let data = AuthenticatorData::new(bytes).unwrap();
        assert_eq!(data.counter, 0xDEADBEEF);
    }

    #[test]
    fn test_auth_data_rp_id_hash() {
        let mut bytes = vec![0u8; 37];
        for i in 0..32 {
            bytes[i] = i as u8;
        }
        let data = AuthenticatorData::new(bytes).unwrap();
        assert_eq!(data.rp_id_hash.len(), 32);
        assert_eq!(data.rp_id_hash[0], 0);
        assert_eq!(data.rp_id_hash[31], 31);
    }

    #[test]
    fn test_aaguid_new_valid() {
        let appattest_bytes = APP_ATTEST.as_bytes().to_vec();
        let result = AAGUID::new(appattest_bytes);
        assert!(result.is_ok());
        assert_eq!(result.unwrap().0, APP_ATTEST);
    }

    #[test]
    fn test_aaguid_new_develop() {
        let develop_bytes = APP_ATTEST_DEVELOP.as_bytes().to_vec();
        let result = AAGUID::new(develop_bytes);
        assert!(result.is_ok());
        assert_eq!(result.unwrap().0, APP_ATTEST_DEVELOP);
    }

    #[test]
    fn test_aaguid_new_invalid() {
        let invalid_bytes = vec![0u8; 16];
        let result = AAGUID::new(invalid_bytes);
        assert!(result.is_err());
    }

    #[test]
    fn test_aaguid_new_with_trailing_zeros() {
        // "appattest" + trailing zeros (padded to 16 bytes)
        let mut bytes = APP_ATTEST.as_bytes().to_vec();
        bytes.resize(16, 0x00);
        let result = AAGUID::new(bytes);
        assert!(result.is_ok());
    }

    #[test]
    fn test_is_valid_aaguid_production() {
        let mut bytes = vec![0u8; 55 + 4]; // enough for optional data
                                           // Write appattest at byte 37..46 (aaguid position)
        let aaguid_bytes = APP_ATTEST.as_bytes();
        bytes[37..37 + aaguid_bytes.len()].copy_from_slice(aaguid_bytes);
        // Pad remaining aaguid bytes with zeros (up to 53)
        // Write credential id length = 4 at bytes 53..55
        BigEndian::write_u16(&mut bytes[53..55], 4);
        // Write some credential id bytes
        bytes[55] = 0xAA;
        bytes[56] = 0xBB;
        bytes[57] = 0xCC;
        bytes[58] = 0xDD;
        BigEndian::write_u32(&mut bytes[33..37], 0);

        let data = AuthenticatorData::new(bytes).unwrap();
        assert!(data.is_valid_aaguid());
    }

    #[test]
    fn test_is_valid_aaguid_develop() {
        let mut bytes = vec![0u8; 55 + 4];
        let aaguid_bytes = APP_ATTEST_DEVELOP.as_bytes();
        bytes[37..37 + aaguid_bytes.len()].copy_from_slice(aaguid_bytes);
        BigEndian::write_u16(&mut bytes[53..55], 4);
        BigEndian::write_u32(&mut bytes[33..37], 0);

        let data = AuthenticatorData::new(bytes).unwrap();
        assert!(data.is_valid_aaguid());
    }

    #[test]
    fn test_is_valid_aaguid_none() {
        // Auth data without aaguid (37 bytes, no optional data)
        let bytes = vec![0u8; 37];
        let data = AuthenticatorData::new(bytes).unwrap();
        assert!(!data.is_valid_aaguid());
    }

    #[test]
    fn test_verify_counter_zero_passes() {
        let bytes = vec![0u8; 37];
        let data = AuthenticatorData::new(bytes).unwrap();
        assert!(data.verify_counter().is_ok());
    }

    #[test]
    fn test_verify_counter_nonzero_fails() {
        let mut bytes = vec![0u8; 37];
        BigEndian::write_u32(&mut bytes[33..37], 5);
        let data = AuthenticatorData::new(bytes).unwrap();
        assert!(data.verify_counter().is_err());
    }

    #[test]
    fn test_verify_app_id() {
        let app_id = "app.apple.connect";
        let mut hasher = Sha256::new();
        hasher.update(app_id.as_bytes());
        let hash = hasher.finalize().to_vec();

        let auth_data = AuthenticatorData {
            bytes: vec![],
            rp_id_hash: hash,
            flags: 0,
            counter: 0,
            aaguid: None,
            credential_id: None,
        };

        assert!(auth_data.verify_app_id("app.apple.connect").is_ok());
        assert!(auth_data.verify_app_id("invalid.apple.connect").is_err());
    }

    #[test]
    fn test_verify_key_id() {
        let key_id = vec![1, 2, 3, 4];
        let auth_data = AuthenticatorData {
            bytes: vec![],
            rp_id_hash: vec![],
            flags: 0,
            counter: 0,
            aaguid: None,
            credential_id: Some(key_id.clone()),
        };

        assert!(auth_data.verify_key_id(&key_id).is_ok());
        assert!(auth_data.verify_key_id(&vec![4, 3, 2, 1]).is_err());
    }

    #[test]
    fn test_verify_key_id_none() {
        let auth_data = AuthenticatorData {
            bytes: vec![],
            rp_id_hash: vec![],
            flags: 0,
            counter: 0,
            aaguid: None,
            credential_id: None,
        };
        assert!(auth_data.verify_key_id(&[1, 2, 3]).is_err());
    }

    #[test]
    fn test_trim_trailing_zeros() {
        assert_eq!(AAGUID::trim_trailing_zeros(&[1, 2, 0, 0]), &[1, 2]);
        assert_eq!(AAGUID::trim_trailing_zeros(&[1, 2, 3]), &[1, 2, 3]);
        assert_eq!(AAGUID::trim_trailing_zeros(&[0, 0, 0]), &[] as &[u8]);
        assert_eq!(AAGUID::trim_trailing_zeros(&[]), &[] as &[u8]);
    }
}