1use crate::store::{hash_token, random_token};
4use hmac::{Hmac, Mac};
5use sova_core::{Error, Result};
6use sha2::Sha256;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9type HmacSha256 = Hmac<Sha256>;
10
11pub fn now_secs() -> u64 {
12 SystemTime::now()
13 .duration_since(UNIX_EPOCH)
14 .map(|d| d.as_secs())
15 .unwrap_or(0)
16}
17
18pub fn sign_payload(secret: &str, payload: &str) -> String {
19 let mut mac =
20 HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC key length");
21 mac.update(payload.as_bytes());
22 hex_encode(mac.finalize().into_bytes())
23}
24
25pub fn verify_payload(secret: &str, payload: &str, sig: &str) -> bool {
26 let expected = sign_payload(secret, payload);
27 constant_eq(expected.as_bytes(), sig.as_bytes())
28}
29
30fn constant_eq(a: &[u8], b: &[u8]) -> bool {
31 if a.len() != b.len() {
32 return false;
33 }
34 a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
35}
36
37fn hex_encode(bytes: impl AsRef<[u8]>) -> String {
38 bytes.as_ref().iter().map(|b| format!("{b:02x}")).collect()
39}
40
41pub fn make_verify_token(secret: &str, user_id: i64) -> String {
43 let exp = now_secs() + 86_400;
44 let payload = format!("verify:{user_id}:{exp}");
45 let sig = sign_payload(secret, &payload);
46 format!("{user_id}.{exp}.{sig}")
47}
48
49pub fn parse_verify_token(secret: &str, token: &str) -> Result<i64> {
50 let mut parts = token.split('.');
51 let id: i64 = parts
52 .next()
53 .and_then(|s| s.parse().ok())
54 .ok_or_else(|| Error::BadRequest("invalid verification token".into()))?;
55 let exp: u64 = parts
56 .next()
57 .and_then(|s| s.parse().ok())
58 .ok_or_else(|| Error::BadRequest("invalid verification token".into()))?;
59 let sig = parts
60 .next()
61 .ok_or_else(|| Error::BadRequest("invalid verification token".into()))?;
62 if parts.next().is_some() {
63 return Err(Error::BadRequest("invalid verification token".into()));
64 }
65 if exp < now_secs() {
66 return Err(Error::BadRequest("verification token expired".into()));
67 }
68 let payload = format!("verify:{id}:{exp}");
69 if !verify_payload(secret, &payload, sig) {
70 return Err(Error::BadRequest("invalid verification token".into()));
71 }
72 Ok(id)
73}
74
75pub fn generate_recovery_codes(n: usize) -> Vec<String> {
76 (0..n)
77 .map(|_| {
78 let t = random_token();
79 format!("{}-{}", &t[..4], &t[4..8])
80 })
81 .collect()
82}
83
84pub fn hash_recovery_codes(codes: &[String]) -> Result<String> {
85 let hashed: Vec<String> = codes.iter().map(|c| hash_token(c)).collect();
86 serde_json::to_string(&hashed).map_err(|e| Error::Internal(e.to_string()))
87}
88
89pub fn consume_recovery_code(codes_json: &str, raw: &str) -> Result<Option<String>> {
90 let mut hashed: Vec<String> =
91 serde_json::from_str(codes_json).map_err(|e| Error::Internal(e.to_string()))?;
92 let want = hash_token(raw.trim());
93 let Some(pos) = hashed.iter().position(|h| h == &want) else {
94 return Ok(None);
95 };
96 hashed.remove(pos);
97 Ok(Some(
98 serde_json::to_string(&hashed).map_err(|e| Error::Internal(e.to_string()))?,
99 ))
100}