-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathtoken.rs
More file actions
65 lines (54 loc) · 1.79 KB
/
token.rs
File metadata and controls
65 lines (54 loc) · 1.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//! API token creation, hashing, and validation.
//!
//! Only the SHA-256 hash is stored — the raw token is shown once at creation.
//! Format: `sprout_<32-random-bytes-as-hex>` (71 characters).
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
const TOKEN_PREFIX: &str = "sprout_";
/// Generate a new random API token (CSPRNG, 32 bytes, hex-encoded with prefix).
pub fn generate_token() -> String {
let bytes: [u8; 32] = rand::random();
format!("{}{}", TOKEN_PREFIX, hex::encode(bytes))
}
/// SHA-256 hash of a raw token (the value stored in `api_tokens.token_hash`).
pub fn hash_token(token: &str) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
hasher.finalize().to_vec()
}
/// Constant-time verification that `raw_token` matches `expected_hash`.
pub fn verify_token_hash(raw_token: &str, expected_hash: &[u8]) -> bool {
let computed = hash_token(raw_token);
computed.ct_eq(expected_hash).into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_format_and_length() {
let token = generate_token();
assert!(token.starts_with("sprout_"));
assert_eq!(token.len(), 7 + 64);
}
#[test]
fn tokens_are_unique() {
assert_ne!(generate_token(), generate_token());
}
#[test]
fn hash_verify_round_trip() {
let token = generate_token();
let hash = hash_token(&token);
assert_eq!(hash.len(), 32);
assert!(verify_token_hash(&token, &hash));
}
#[test]
fn wrong_token_rejected() {
let hash = hash_token(&generate_token());
assert!(!verify_token_hash(&generate_token(), &hash));
}
#[test]
fn hash_is_deterministic() {
let token = "sprout_test_abc123";
assert_eq!(hash_token(token), hash_token(token));
}
}