this repo has no description
at main 4.4 kB view raw
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}