this repo has no description
1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
2use hmac::Mac;
3use p256::ecdsa::SigningKey;
4use sha2::{Digest, Sha256};
5use subtle::ConstantTimeEq;
6
7use crate::CryptoError;
8
9type HmacSha256 = hmac::Hmac<Sha256>;
10
11pub struct SigningKeyPair {
12 #[allow(dead_code)]
13 signing_key: SigningKey,
14 pub key_id: String,
15 pub x: String,
16 pub y: String,
17}
18
19impl SigningKeyPair {
20 pub fn from_seed(seed: &[u8]) -> Result<Self, CryptoError> {
21 let mut hasher = Sha256::new();
22 hasher.update(b"oauth-signing-key-derivation:");
23 hasher.update(seed);
24 let hash = hasher.finalize();
25
26 let signing_key = SigningKey::from_slice(&hash)
27 .map_err(|e| CryptoError::InvalidKey(format!("Failed to create signing key: {}", e)))?;
28
29 let verifying_key = signing_key.verifying_key();
30 let point = verifying_key.to_encoded_point(false);
31
32 let x = URL_SAFE_NO_PAD.encode(
33 point
34 .x()
35 .ok_or_else(|| CryptoError::InvalidKey("Missing X coordinate".to_string()))?,
36 );
37 let y = URL_SAFE_NO_PAD.encode(
38 point
39 .y()
40 .ok_or_else(|| CryptoError::InvalidKey("Missing Y coordinate".to_string()))?,
41 );
42
43 let mut kid_hasher = Sha256::new();
44 kid_hasher.update(x.as_bytes());
45 kid_hasher.update(y.as_bytes());
46 let kid_hash = kid_hasher.finalize();
47 let key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
48
49 Ok(Self {
50 signing_key,
51 key_id,
52 x,
53 y,
54 })
55 }
56}
57
58pub struct DeviceCookieSigner {
59 key: [u8; 32],
60}
61
62impl DeviceCookieSigner {
63 pub fn new(key: [u8; 32]) -> Self {
64 Self { key }
65 }
66
67 pub fn sign(&self, device_id: &str) -> String {
68 let timestamp = std::time::SystemTime::now()
69 .duration_since(std::time::UNIX_EPOCH)
70 .unwrap_or_default()
71 .as_secs();
72
73 let message = format!("{}:{}", device_id, timestamp);
74 let mut mac =
75 <HmacSha256 as Mac>::new_from_slice(&self.key).expect("HMAC key size is valid");
76 mac.update(message.as_bytes());
77 let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
78
79 format!("{}.{}.{}", device_id, timestamp, signature)
80 }
81
82 pub fn verify(&self, cookie_value: &str, max_age_days: u64) -> Option<String> {
83 let parts: Vec<&str> = cookie_value.splitn(3, '.').collect();
84 if parts.len() != 3 {
85 return None;
86 }
87
88 let device_id = parts[0];
89 let timestamp_str = parts[1];
90 let provided_signature = parts[2];
91
92 let timestamp: u64 = timestamp_str.parse().ok()?;
93
94 let now = std::time::SystemTime::now()
95 .duration_since(std::time::UNIX_EPOCH)
96 .unwrap_or_default()
97 .as_secs();
98
99 if now.saturating_sub(timestamp) > max_age_days * 24 * 60 * 60 {
100 return None;
101 }
102
103 let message = format!("{}:{}", device_id, timestamp);
104 let mut mac =
105 <HmacSha256 as Mac>::new_from_slice(&self.key).expect("HMAC key size is valid");
106 mac.update(message.as_bytes());
107 let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
108
109 if provided_signature
110 .as_bytes()
111 .ct_eq(expected_signature.as_bytes())
112 .into()
113 {
114 Some(device_id.to_string())
115 } else {
116 None
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_signing_key_pair() {
127 let seed = b"test-seed-for-signing-key";
128 let kp = SigningKeyPair::from_seed(seed).unwrap();
129 assert!(!kp.key_id.is_empty());
130 assert!(!kp.x.is_empty());
131 assert!(!kp.y.is_empty());
132 }
133
134 #[test]
135 fn test_device_cookie_signer() {
136 let key = [0u8; 32];
137 let signer = DeviceCookieSigner::new(key);
138 let signed = signer.sign("device-123");
139 let verified = signer.verify(&signed, 400);
140 assert_eq!(verified, Some("device-123".to_string()));
141 }
142
143 #[test]
144 fn test_device_cookie_invalid() {
145 let key = [0u8; 32];
146 let signer = DeviceCookieSigner::new(key);
147 assert!(signer.verify("invalid", 400).is_none());
148 assert!(signer.verify("a.b.c", 400).is_none());
149 }
150}