use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
use base64::Engine;
use futures::future::{BoxFuture, Shared};
use futures::FutureExt;
use tokio_util::sync::CancellationToken;
use super::store::SecureStore;
use super::transport::Transport;
const CLOCK_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 60);
const TIME_URL: &str = "https://google.com";
const TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
const CLOCK_SKEW_TOLERANCE_MS: i64 = 5 * 60 * 1000;
const LAST_SEEN_KEY: &str = "lastSeen";
pub trait Clock: Send + Sync + 'static {
fn now(&self) -> SystemTime;
fn now_ms(&self) -> i64 {
to_millis(self.now())
}
}
pub(crate) struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> SystemTime {
SystemTime::now()
}
}
pub(crate) fn to_millis(time: SystemTime) -> i64 {
match time.duration_since(UNIX_EPOCH) {
Ok(d) => i64::try_from(d.as_millis()).unwrap_or(i64::MAX),
Err(e) => -i64::try_from(e.duration().as_millis()).unwrap_or(i64::MAX),
}
}
pub(crate) fn from_millis(ms: i64) -> SystemTime {
if ms >= 0 {
UNIX_EPOCH + Duration::from_millis(ms.unsigned_abs())
} else {
UNIX_EPOCH - Duration::from_millis(ms.unsigned_abs())
}
}
pub(crate) struct ClockGuard {
inner: Arc<GuardInner>,
ready: Shared<BoxFuture<'static, ()>>,
cancel: CancellationToken,
}
struct GuardInner {
store: SecureStore,
clock: Arc<dyn Clock>,
transport: Arc<dyn Transport>,
tampered: AtomicBool,
}
impl ClockGuard {
pub(crate) fn new(
store: SecureStore,
clock: Arc<dyn Clock>,
transport: Arc<dyn Transport>,
runtime: &tokio::runtime::Handle,
) -> Self {
let inner = Arc::new(GuardInner {
store,
clock,
transport,
tampered: AtomicBool::new(false),
});
let ready = {
let inner = inner.clone();
async move { inner.check().await }.boxed().shared()
};
runtime.spawn(ready.clone());
let cancel = CancellationToken::new();
{
let inner = inner.clone();
let cancel = cancel.clone();
runtime.spawn(async move {
loop {
tokio::select! {
() = tokio::time::sleep(CLOCK_CHECK_INTERVAL) => inner.check().await,
() = cancel.cancelled() => break,
}
}
});
}
Self {
inner,
ready,
cancel,
}
}
pub(crate) async fn ready(&self) {
self.ready.clone().await;
}
pub(crate) fn is_tampered(&self) -> bool {
self.inner.tampered.load(Ordering::SeqCst)
}
pub(crate) fn dispose(&self) {
self.cancel.cancel();
}
}
impl GuardInner {
async fn check(&self) {
if let Some(network_now) = self.network_time().await {
let skew = (self.clock.now_ms() - network_now).abs();
self.tampered
.store(skew > CLOCK_SKEW_TOLERANCE_MS, Ordering::SeqCst);
self.store
.set_async(LAST_SEEN_KEY, encode_timestamp(network_now))
.await;
return;
}
let now = self.clock.now_ms();
let previous = decode_timestamp(self.store.get_async(LAST_SEEN_KEY).await.as_deref());
if previous.is_some_and(|previous| now < previous) {
self.tampered.store(true, Ordering::SeqCst);
return;
}
if previous.is_none_or(|previous| now > previous) {
self.store
.set_async(LAST_SEEN_KEY, encode_timestamp(now))
.await;
}
}
async fn network_time(&self) -> Option<i64> {
let header = tokio::time::timeout(TIME_REQUEST_TIMEOUT, self.transport.head_date(TIME_URL))
.await
.ok()?
.ok()??;
httpdate::parse_http_date(header.trim()).ok().map(to_millis)
}
}
pub(crate) fn encode_timestamp(ms: i64) -> String {
STANDARD.encode(ms.to_string())
}
pub(crate) fn decode_timestamp(value: Option<&str>) -> Option<i64> {
let value = value.filter(|v| !v.is_empty())?;
let bytes = STANDARD
.decode(value)
.or_else(|_| STANDARD_NO_PAD.decode(value.trim_end_matches('=')))
.ok()?;
parse_int(&String::from_utf8_lossy(&bytes))
}
fn parse_int(s: &str) -> Option<i64> {
let s = s.trim_start();
let (negative, digits) = match s.as_bytes().first() {
Some(b'-') => (true, &s[1..]),
Some(b'+') => (false, &s[1..]),
_ => (false, s),
};
let end = digits
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(digits.len());
let value: i64 = digits[..end].parse().ok()?;
Some(if negative { -value } else { value })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn golden_last_seen_encoding() {
assert_eq!(encode_timestamp(1_767_225_600_000), "MTc2NzIyNTYwMDAwMA==");
assert_eq!(
decode_timestamp(Some("MTc2NzIyNTYwMDAwMA==")),
Some(1_767_225_600_000)
);
}
#[test]
fn decodes_like_parse_int() {
assert_eq!(decode_timestamp(None), None);
assert_eq!(decode_timestamp(Some("")), None);
assert_eq!(decode_timestamp(Some(&STANDARD.encode("12abc"))), Some(12));
assert_eq!(decode_timestamp(Some(&STANDARD.encode(" -5"))), Some(-5));
assert_eq!(decode_timestamp(Some(&STANDARD.encode("+7"))), Some(7));
assert_eq!(decode_timestamp(Some(&STANDARD.encode("abc"))), None);
assert_eq!(decode_timestamp(Some("MTIz")), Some(123));
assert_eq!(decode_timestamp(Some("%%%")), None);
}
#[test]
fn millis_round_trip() {
for ms in [0, 1, 1_767_225_600_000, -1_000] {
assert_eq!(to_millis(from_millis(ms)), ms);
}
assert!(SystemClock.now_ms() > 1_767_225_600_000 - 365 * 86_400_000);
}
}