monoprotocol 0.2.0

Mono sync protocol reference types and crypto (spec is canonical in spec/)
Documentation
use crate::envelope::{TwoFaMethod, TwoFaProof};
use crate::webauthn::{verify_sms_code, verify_totp_code, verify_webauthn_assertion, WebAuthnRegistry};

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum TwoFaError {
    #[error("missing 2FA proof")]
    Missing,
    #[error("empty 2FA assertion")]
    EmptyAssertion,
    #[error("unsupported 2FA method")]
    UnsupportedMethod,
    #[error("invalid 2FA assertion")]
    InvalidAssertion,
}

pub fn verify_twofa_proof(proof: &TwoFaProof) -> Result<(), TwoFaError> {
    verify_twofa_proof_with_registry(proof, &WebAuthnRegistry::default(), None)
}

pub fn verify_twofa_proof_with_registry(
    proof: &TwoFaProof,
    registry: &WebAuthnRegistry,
    expected_challenge: Option<&str>,
) -> Result<(), TwoFaError> {
    if proof.assertion.trim().is_empty() {
        return Err(TwoFaError::EmptyAssertion);
    }
    match proof.method {
        TwoFaMethod::WebAuthn => {
            verify_webauthn_assertion(registry, proof, expected_challenge)
        }
        TwoFaMethod::Totp => {
            let secret = std::env::var("MONO_TOTP_SECRET").ok();
            verify_totp_code(&proof.assertion, secret.as_deref())
        }
        TwoFaMethod::Sms => {
            let expected = std::env::var("MONO_SMS_CODE").ok();
            verify_sms_code(&proof.assertion, expected.as_deref())
        }
    }
}

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

    #[test]
    fn totp_six_digits_passes() {
        verify_twofa_proof(&TwoFaProof {
            method: TwoFaMethod::Totp,
            assertion: "123456".into(),
            credential_id: None,
        })
        .unwrap();
    }

    #[test]
    fn totp_wrong_length_fails() {
        assert_eq!(
            verify_twofa_proof(&TwoFaProof {
                method: TwoFaMethod::Totp,
                assertion: "12345".into(),
                credential_id: None,
            }),
            Err(TwoFaError::InvalidAssertion)
        );
    }

    #[test]
    fn sms_six_digits_passes() {
        verify_twofa_proof(&TwoFaProof {
            method: TwoFaMethod::Sms,
            assertion: "654321".into(),
            credential_id: None,
        })
        .unwrap();
    }

    #[test]
    fn empty_assertion_fails() {
        assert_eq!(
            verify_twofa_proof(&TwoFaProof {
                method: TwoFaMethod::Totp,
                assertion: "   ".into(),
                credential_id: None,
            }),
            Err(TwoFaError::EmptyAssertion)
        );
    }
}