opensession_api_types/
crypto.rs1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
9use hmac::{Hmac, Mac};
10use pbkdf2::pbkdf2_hmac;
11use sha2::Sha256;
12
13use crate::ServiceError;
14
15const PBKDF2_ITERATIONS: u32 = 600_000;
16const SALT_LEN: usize = 16;
17const HASH_LEN: usize = 32;
18
19pub fn hash_password(password: &str) -> (String, String) {
23 let mut salt = [0u8; SALT_LEN];
24 getrandom::getrandom(&mut salt).expect("getrandom failed");
25
26 let mut hash = [0u8; HASH_LEN];
27 pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, PBKDF2_ITERATIONS, &mut hash);
28
29 (hex::encode(hash), hex::encode(salt))
30}
31
32pub fn verify_password(password: &str, hash_hex: &str, salt_hex: &str) -> bool {
34 let Ok(salt) = hex::decode(salt_hex) else {
35 return false;
36 };
37 let Ok(expected) = hex::decode(hash_hex) else {
38 return false;
39 };
40
41 let mut hash = [0u8; HASH_LEN];
42 pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, PBKDF2_ITERATIONS, &mut hash);
43
44 hash.len() == expected.len() && hash.iter().zip(expected.iter()).all(|(a, b)| a == b)
46}
47
48const JWT_HEADER: &str = r#"{"alg":"HS256","typ":"JWT"}"#;
52
53pub const JWT_EXPIRY_SECS: u64 = 3600;
55
56pub const REFRESH_EXPIRY_SECS: u64 = 7 * 24 * 3600;
58
59pub fn sign_jwt(user_id: &str, secret: &str, now_unix: u64) -> String {
61 let header_b64 = URL_SAFE_NO_PAD.encode(JWT_HEADER.as_bytes());
62
63 let payload = format!(
64 r#"{{"sub":"{}","iat":{},"exp":{}}}"#,
65 user_id,
66 now_unix,
67 now_unix + JWT_EXPIRY_SECS,
68 );
69 let payload_b64 = URL_SAFE_NO_PAD.encode(payload.as_bytes());
70
71 let signing_input = format!("{header_b64}.{payload_b64}");
72 let signature = hmac_sha256(secret.as_bytes(), signing_input.as_bytes());
73 let sig_b64 = URL_SAFE_NO_PAD.encode(signature);
74
75 format!("{signing_input}.{sig_b64}")
76}
77
78pub fn verify_jwt(token: &str, secret: &str, now_unix: u64) -> Result<String, ServiceError> {
80 let parts: Vec<&str> = token.split('.').collect();
81 if parts.len() != 3 {
82 return Err(ServiceError::Unauthorized("invalid JWT format".into()));
83 }
84
85 let signing_input = format!("{}.{}", parts[0], parts[1]);
87 let expected_sig = hmac_sha256(secret.as_bytes(), signing_input.as_bytes());
88 let actual_sig = URL_SAFE_NO_PAD
89 .decode(parts[2])
90 .map_err(|_| ServiceError::Unauthorized("invalid JWT signature encoding".into()))?;
91
92 if expected_sig.len() != actual_sig.len()
93 || !expected_sig
94 .iter()
95 .zip(actual_sig.iter())
96 .all(|(a, b)| a == b)
97 {
98 return Err(ServiceError::Unauthorized("invalid JWT signature".into()));
99 }
100
101 let payload_bytes = URL_SAFE_NO_PAD
103 .decode(parts[1])
104 .map_err(|_| ServiceError::Unauthorized("invalid JWT payload encoding".into()))?;
105 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
106 .map_err(|_| ServiceError::Unauthorized("invalid JWT payload".into()))?;
107
108 let exp = payload["exp"]
110 .as_u64()
111 .ok_or_else(|| ServiceError::Unauthorized("missing exp claim".into()))?;
112 if now_unix > exp {
113 return Err(ServiceError::Unauthorized("JWT expired".into()));
114 }
115
116 let sub = payload["sub"]
118 .as_str()
119 .ok_or_else(|| ServiceError::Unauthorized("missing sub claim".into()))?
120 .to_string();
121
122 Ok(sub)
123}
124
125pub fn generate_token() -> String {
127 let mut bytes = [0u8; 32];
128 getrandom::getrandom(&mut bytes).expect("getrandom failed");
129 hex::encode(bytes)
130}
131
132pub fn hash_token(token: &str) -> String {
134 use sha2::Digest;
135 let hash = sha2::Sha256::digest(token.as_bytes());
136 hex::encode(hash)
137}
138
139fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
142 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC can take key of any size");
143 mac.update(data);
144 mac.finalize().into_bytes().to_vec()
145}