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 6.8 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} 23 24impl AuthConfig { 25 pub fn init() -> &'static Self { 26 CONFIG.get_or_init(|| { 27 let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| { 28 if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 29 "test-jwt-secret-not-for-production".to_string() 30 } else { 31 panic!( 32 "JWT_SECRET environment variable must be set in production. \ 33 Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 34 ); 35 } 36 }); 37 38 let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| { 39 if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 40 "test-dpop-secret-not-for-production".to_string() 41 } else { 42 panic!( 43 "DPOP_SECRET environment variable must be set in production. \ 44 Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 45 ); 46 } 47 }); 48 49 if jwt_secret.len() < 32 50 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() 51 { 52 panic!("JWT_SECRET must be at least 32 characters"); 53 } 54 55 if dpop_secret.len() < 32 56 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() 57 { 58 panic!("DPOP_SECRET must be at least 32 characters"); 59 } 60 61 let mut hasher = Sha256::new(); 62 hasher.update(b"oauth-signing-key-derivation:"); 63 hasher.update(jwt_secret.as_bytes()); 64 let seed = hasher.finalize(); 65 66 let signing_key = SigningKey::from_slice(&seed).unwrap_or_else(|e| { 67 panic!( 68 "Failed to create signing key from seed: {}. This is a bug.", 69 e 70 ) 71 }); 72 73 let verifying_key = signing_key.verifying_key(); 74 let point = verifying_key.to_encoded_point(false); 75 76 let signing_key_x = URL_SAFE_NO_PAD.encode( 77 point 78 .x() 79 .expect("EC point missing X coordinate - this should never happen"), 80 ); 81 let signing_key_y = URL_SAFE_NO_PAD.encode( 82 point 83 .y() 84 .expect("EC point missing Y coordinate - this should never happen"), 85 ); 86 87 let mut kid_hasher = Sha256::new(); 88 kid_hasher.update(signing_key_x.as_bytes()); 89 kid_hasher.update(signing_key_y.as_bytes()); 90 let kid_hash = kid_hasher.finalize(); 91 let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]); 92 93 let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| { 94 if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() { 95 "test-master-key-not-for-production".to_string() 96 } else { 97 panic!( 98 "MASTER_KEY environment variable must be set in production. \ 99 Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing." 100 ); 101 } 102 }); 103 104 if master_key.len() < 32 105 && std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err() 106 { 107 panic!("MASTER_KEY must be at least 32 characters"); 108 } 109 110 let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes()); 111 let mut key_encryption_key = [0u8; 32]; 112 hk.expand(b"tranquil-pds-user-key-encryption", &mut key_encryption_key) 113 .expect("HKDF expansion failed"); 114 115 AuthConfig { 116 jwt_secret, 117 dpop_secret, 118 signing_key, 119 signing_key_id, 120 signing_key_x, 121 signing_key_y, 122 key_encryption_key, 123 } 124 }) 125 } 126 127 pub fn get() -> &'static Self { 128 CONFIG 129 .get() 130 .expect("AuthConfig not initialized - call AuthConfig::init() first") 131 } 132 133 pub fn jwt_secret(&self) -> &str { 134 &self.jwt_secret 135 } 136 137 pub fn dpop_secret(&self) -> &str { 138 &self.dpop_secret 139 } 140 141 pub fn encrypt_user_key(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> { 142 use rand::RngCore; 143 144 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key) 145 .map_err(|e| format!("Failed to create cipher: {}", e))?; 146 147 let mut nonce_bytes = [0u8; 12]; 148 rand::thread_rng().fill_bytes(&mut nonce_bytes); 149 150 #[allow(deprecated)] 151 let nonce = Nonce::from_slice(&nonce_bytes); 152 153 let ciphertext = cipher 154 .encrypt(nonce, plaintext) 155 .map_err(|e| format!("Encryption failed: {}", e))?; 156 157 let mut result = Vec::with_capacity(12 + ciphertext.len()); 158 result.extend_from_slice(&nonce_bytes); 159 result.extend_from_slice(&ciphertext); 160 161 Ok(result) 162 } 163 164 pub fn decrypt_user_key(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> { 165 if encrypted.len() < 12 { 166 return Err("Encrypted data too short".to_string()); 167 } 168 169 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key) 170 .map_err(|e| format!("Failed to create cipher: {}", e))?; 171 172 #[allow(deprecated)] 173 let nonce = Nonce::from_slice(&encrypted[..12]); 174 let ciphertext = &encrypted[12..]; 175 176 cipher 177 .decrypt(nonce, ciphertext) 178 .map_err(|e| format!("Decryption failed: {}", e)) 179 } 180} 181 182pub fn encrypt_key(plaintext: &[u8]) -> Result<Vec<u8>, String> { 183 AuthConfig::get().encrypt_user_key(plaintext) 184} 185 186pub fn decrypt_key(encrypted: &[u8], version: Option<i32>) -> Result<Vec<u8>, String> { 187 match version.unwrap_or(0) { 188 0 => Ok(encrypted.to_vec()), 189 1 => AuthConfig::get().decrypt_user_key(encrypted), 190 v => Err(format!("Unknown encryption version: {}", v)), 191 } 192}