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