use std::time::Duration;
use super::cipher::{EncryptedRefreshToken, TokenCipher};
use crate::pas_port::{CipherFailure, PasAuthPort, PasRefreshOutcome, pas_refresh};
const DEFAULT_TRANSIENT_RETRY_AFTER: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RevokeCause {
CipherFailure,
PasRejected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TransientCause {
PasServerError,
CipherEncryptFailed,
}
#[derive(Debug)]
#[must_use]
pub enum LivenessFailure {
Revoked { cause: RevokeCause },
Transient {
retry_after: Option<Duration>,
cause: TransientCause,
},
}
#[derive(Debug)]
#[must_use]
pub enum LivenessOutcome {
Fresh { rotated_ciphertext: Option<String> },
Failed(LivenessFailure),
}
pub async fn attempt_liveness_refresh<P: PasAuthPort>(
cipher: &TokenCipher,
port: &P,
ct: &EncryptedRefreshToken,
) -> LivenessOutcome {
match pas_refresh(cipher, port, ct).await {
Err(CipherFailure) => {
LivenessOutcome::Failed(LivenessFailure::Revoked { cause: RevokeCause::CipherFailure })
}
Ok(PasRefreshOutcome::Refreshed { tokens }) => match tokens.refresh_token.as_deref() {
Some(new_rt) => match cipher.encrypt(new_rt) {
Ok(ct) => LivenessOutcome::Fresh { rotated_ciphertext: Some(ct) },
Err(_) => LivenessOutcome::Failed(LivenessFailure::Transient {
retry_after: Some(DEFAULT_TRANSIENT_RETRY_AFTER),
cause: TransientCause::CipherEncryptFailed,
}),
},
None => LivenessOutcome::Fresh { rotated_ciphertext: None },
},
Ok(PasRefreshOutcome::Rejected { .. }) => {
LivenessOutcome::Failed(LivenessFailure::Revoked { cause: RevokeCause::PasRejected })
}
Ok(PasRefreshOutcome::Transient { .. }) => {
LivenessOutcome::Failed(LivenessFailure::Transient {
retry_after: Some(DEFAULT_TRANSIENT_RETRY_AFTER),
cause: TransientCause::PasServerError,
})
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::pas_port::MemoryPasAuth;
use base64::{Engine, engine::general_purpose::STANDARD};
#[tokio::test]
async fn decrypt_failure_short_circuits_to_revoked_cipher_failure() {
let key_b64 = STANDARD.encode([0u8; 32]);
let cipher = TokenCipher::from_base64_key(&key_b64).unwrap();
let garbage_ct = EncryptedRefreshToken::from_stored(STANDARD.encode([0u8; 64]));
let port = MemoryPasAuth::new();
let outcome = attempt_liveness_refresh(&cipher, &port, &garbage_ct).await;
assert!(matches!(
outcome,
LivenessOutcome::Failed(LivenessFailure::Revoked { cause: RevokeCause::CipherFailure })
));
}
}