forked from
lewis.moe/bspds-sandbox
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
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}