I've been saying "PDSes seem easy enough, they're what, some CRUD to a db? I can do that in my sleep". well i'm sleeping rn so let's go
at main 9.1 kB view raw
1#[allow(deprecated)] 2use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; 3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4use hkdf::Hkdf; 5use p256::ecdsa::SigningKey; 6use sha2::{Digest, Sha256}; 7use std::sync::OnceLock; 8 9static CONFIG: OnceLock<AuthConfig> = OnceLock::new(); 10 11pub const ENCRYPTION_VERSION: i32 = 1; 12 13pub struct AuthConfig { 14 jwt_secret: String, 15 dpop_secret: String, 16 #[allow(dead_code)] 17 signing_key: SigningKey, 18 pub signing_key_id: String, 19 pub signing_key_x: String, 20 pub signing_key_y: String, 21 key_encryption_key: [u8; 32], 22 device_cookie_key: [u8; 32], 23} 24 25impl AuthConfig { 26 pub fn init() -> &'static Self { 27 CONFIG.get_or_init(|| { 28 let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| { 29 if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 30 "test-jwt-secret-not-for-production".to_string() 31 } else { 32 panic!( 33 "JWT_SECRET environment variable must be set in production. \ 34 Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 35 ); 36 } 37 }); 38 39 let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| { 40 if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 41 "test-dpop-secret-not-for-production".to_string() 42 } else { 43 panic!( 44 "DPOP_SECRET environment variable must be set in production. \ 45 Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 46 ); 47 } 48 }); 49 50 if jwt_secret.len() < 32 51 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() 52 { 53 panic!("JWT_SECRET must be at least 32 characters"); 54 } 55 56 if dpop_secret.len() < 32 57 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() 58 { 59 panic!("DPOP_SECRET must be at least 32 characters"); 60 } 61 62 let mut hasher = Sha256::new(); 63 hasher.update(b"oauth-signing-key-derivation:"); 64 hasher.update(jwt_secret.as_bytes()); 65 let seed = hasher.finalize(); 66 67 let signing_key = SigningKey::from_slice(&seed).unwrap_or_else(|e| { 68 panic!( 69 "Failed to create signing key from seed: {}. This is a bug.", 70 e 71 ) 72 }); 73 74 let verifying_key = signing_key.verifying_key(); 75 let point = verifying_key.to_encoded_point(false); 76 77 let signing_key_x = URL_SAFE_NO_PAD.encode( 78 point 79 .x() 80 .expect("EC point missing X coordinate - this should never happen"), 81 ); 82 let signing_key_y = URL_SAFE_NO_PAD.encode( 83 point 84 .y() 85 .expect("EC point missing Y coordinate - this should never happen"), 86 ); 87 88 let mut kid_hasher = Sha256::new(); 89 kid_hasher.update(signing_key_x.as_bytes()); 90 kid_hasher.update(signing_key_y.as_bytes()); 91 let kid_hash = kid_hasher.finalize(); 92 let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]); 93 94 let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| { 95 if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 96 "test-master-key-not-for-production".to_string() 97 } else { 98 panic!( 99 "MASTER_KEY environment variable must be set in production. \ 100 Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 101 ); 102 } 103 }); 104 105 if master_key.len() < 32 106 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() 107 { 108 panic!("MASTER_KEY must be at least 32 characters"); 109 } 110 111 let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes()); 112 let mut key_encryption_key = [0u8; 32]; 113 hk.expand(b"tranquil-pds-user-key-encryption", &mut key_encryption_key) 114 .expect("HKDF expansion failed"); 115 116 let mut device_cookie_key = [0u8; 32]; 117 hk.expand( 118 b"tranquil-pds-device-cookie-signing", 119 &mut device_cookie_key, 120 ) 121 .expect("HKDF expansion failed"); 122 123 AuthConfig { 124 jwt_secret, 125 dpop_secret, 126 signing_key, 127 signing_key_id, 128 signing_key_x, 129 signing_key_y, 130 key_encryption_key, 131 device_cookie_key, 132 } 133 }) 134 } 135 136 pub fn get() -> &'static Self { 137 CONFIG 138 .get() 139 .expect("AuthConfig not initialized - call AuthConfig::init() first") 140 } 141 142 pub fn jwt_secret(&self) -> &str { 143 &self.jwt_secret 144 } 145 146 pub fn dpop_secret(&self) -> &str { 147 &self.dpop_secret 148 } 149 150 pub fn sign_device_cookie(&self, device_id: &str) -> String { 151 use hmac::Mac; 152 type HmacSha256 = hmac::Hmac<Sha256>; 153 154 let timestamp = std::time::SystemTime::now() 155 .duration_since(std::time::UNIX_EPOCH) 156 .unwrap_or_default() 157 .as_secs(); 158 159 let message = format!("{}:{}", device_id, timestamp); 160 let mut mac = <HmacSha256 as Mac>::new_from_slice(&self.device_cookie_key) 161 .expect("HMAC key size is valid"); 162 mac.update(message.as_bytes()); 163 let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); 164 165 format!("{}.{}.{}", device_id, timestamp, signature) 166 } 167 168 pub fn verify_device_cookie(&self, cookie_value: &str) -> Option<String> { 169 use hmac::Mac; 170 type HmacSha256 = hmac::Hmac<Sha256>; 171 172 let parts: Vec<&str> = cookie_value.splitn(3, '.').collect(); 173 if parts.len() != 3 { 174 return None; 175 } 176 177 let device_id = parts[0]; 178 let timestamp_str = parts[1]; 179 let provided_signature = parts[2]; 180 181 let timestamp: u64 = timestamp_str.parse().ok()?; 182 183 let now = std::time::SystemTime::now() 184 .duration_since(std::time::UNIX_EPOCH) 185 .unwrap_or_default() 186 .as_secs(); 187 188 let max_age_days = 400; 189 if now.saturating_sub(timestamp) > max_age_days * 24 * 60 * 60 { 190 return None; 191 } 192 193 let message = format!("{}:{}", device_id, timestamp); 194 let mut mac = <HmacSha256 as Mac>::new_from_slice(&self.device_cookie_key) 195 .expect("HMAC key size is valid"); 196 mac.update(message.as_bytes()); 197 let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); 198 199 use subtle::ConstantTimeEq; 200 if provided_signature 201 .as_bytes() 202 .ct_eq(expected_signature.as_bytes()) 203 .into() 204 { 205 Some(device_id.to_string()) 206 } else { 207 None 208 } 209 } 210 211 pub fn encrypt_user_key(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> { 212 use rand::RngCore; 213 214 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key) 215 .map_err(|e| format!("Failed to create cipher: {}", e))?; 216 217 let mut nonce_bytes = [0u8; 12]; 218 rand::thread_rng().fill_bytes(&mut nonce_bytes); 219 220 #[allow(deprecated)] 221 let nonce = Nonce::from_slice(&nonce_bytes); 222 223 let ciphertext = cipher 224 .encrypt(nonce, plaintext) 225 .map_err(|e| format!("Encryption failed: {}", e))?; 226 227 let mut result = Vec::with_capacity(12 + ciphertext.len()); 228 result.extend_from_slice(&nonce_bytes); 229 result.extend_from_slice(&ciphertext); 230 231 Ok(result) 232 } 233 234 pub fn decrypt_user_key(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> { 235 if encrypted.len() < 12 { 236 return Err("Encrypted data too short".to_string()); 237 } 238 239 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key) 240 .map_err(|e| format!("Failed to create cipher: {}", e))?; 241 242 #[allow(deprecated)] 243 let nonce = Nonce::from_slice(&encrypted[..12]); 244 let ciphertext = &encrypted[12..]; 245 246 cipher 247 .decrypt(nonce, ciphertext) 248 .map_err(|e| format!("Decryption failed: {}", e)) 249 } 250} 251 252pub fn encrypt_key(plaintext: &[u8]) -> Result<Vec<u8>, String> { 253 AuthConfig::get().encrypt_user_key(plaintext) 254} 255 256pub fn decrypt_key(encrypted: &[u8], version: Option<i32>) -> Result<Vec<u8>, String> { 257 match version.unwrap_or(0) { 258 0 => Ok(encrypted.to_vec()), 259 1 => AuthConfig::get().decrypt_user_key(encrypted), 260 v => Err(format!("Unknown encryption version: {}", v)), 261 } 262}